|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
IntroductionThis article reviews a WPF This article assumes that the reader is already familiar with data binding and templates, binding a TreeView to a ViewModel, and attached properties. BackgroundIt is very common to have a The Devil is in the DetailsThis sounds too good to be true, and it is. Making the Functional RequirementsBefore we start to examine how this demo program works, first we will review what it does. Here is a screenshot of the demo application in action:
Requirement 1: Each item in the tree must display a checkbox that displays the text and check state of an underlying data object. Requirement 2: Upon an item being checked or unchecked, all of its child items should be checked or unchecked, respectively. Requirement 3: If an item’s descendants do not all have the same check state, that item’s check state must be ‘indeterminate.’ Requirement 4: Navigating from item to item should require only one press of an arrow key. Requirement 5: Pressing the Spacebar or Enter keys should toggle the check state of the selected item. Requirement 6: Clicking on an item’s checkbox should toggle its check state, but not select the item. Requirement 7: Clicking on an item’s display text should select the item, but not toggle its check state. Requirement 8: All items in the tree should be in the expanded state by default. I suggest you copy those requirements and paste them into your favorite text editor, such as Notepad, because we will reference them throughout the rest of the article by number. Putting the Smarts in a ViewModelAs explained in my ‘Simplifying the WPF TreeView by Using the ViewModel Pattern’ article, the interface IFooViewModel : INotifyPropertyChanged
{
List<FooViewModel> Children { get; }
bool? IsChecked { get; set; }
bool IsInitiallySelected { get; }
string Name { get; }
}
The most interesting aspect of this ViewModel class is the logic behind the /// <summary>
/// Gets/sets the state of the associated UI toggle (ex. CheckBox).
/// The return value is calculated based on the check state of all
/// child FooViewModels. Setting this property to true or false
/// will set all children to the same check state, and setting it
/// to any value will cause the parent to verify its check state.
/// </summary>
public bool? IsChecked
{
get { return _isChecked; }
set { this.SetIsChecked(value, true, true); }
}
void SetIsChecked(bool? value, bool updateChildren, bool updateParent)
{
if (value == _isChecked)
return;
_isChecked = value;
if (updateChildren && _isChecked.HasValue)
this.Children.ForEach(c => c.SetIsChecked(_isChecked, true, false));
if (updateParent && _parent != null)
_parent.VerifyCheckState();
this.OnPropertyChanged("IsChecked");
}
void VerifyCheckState()
{
bool? state = null;
for (int i = 0; i < this.Children.Count; ++i)
{
bool? current = this.Children[i].IsChecked;
if (i == 0)
{
state = current;
}
else if (state != current)
{
state = null;
break;
}
}
this.SetIsChecked(state, false, true);
}
This strategy is specific to the functional requirements I imposed upon myself. If you have different rules regarding how and when items should update their check state, simply adjust the logic in those methods to suit your needs. TreeView ConfigurationNow it is time to see how the <TreeView
x:Name="tree"
ItemContainerStyle="{StaticResource TreeViewItemStyle}"
ItemsSource="{Binding Mode=OneTime}"
ItemTemplate="{StaticResource CheckBoxItemTemplate}"
/>
The
<HierarchicalDataTemplate
x:Key="CheckBoxItemTemplate"
ItemsSource="{Binding Children, Mode=OneTime}"
>
<StackPanel Orientation="Horizontal">
<!-- These elements are bound to a FooViewModel object. -->
<CheckBox
Focusable="False"
IsChecked="{Binding IsChecked}"
VerticalAlignment="Center"
/>
<ContentPresenter
Content="{Binding Name, Mode=OneTime}"
Margin="2,0"
/>
</StackPanel>
</HierarchicalDataTemplate>
There are several points of interest in that template. The template includes a The We will examine the Turning a TreeViewItem into a ToggleButtonIn the previous section, we quickly considered an interesting question. If the This is a tricky problem: we cannot let the The Doctor’s solution uses what John Gossman refers to as “attached behavior.” The idea is that you set an attached property on an element so that you can gain access to the element from the class that exposes the attached property. Once that class has access to the element, it can hook events on it and, in response to those events firing, make the element do things that it normally would not do. It is a very convenient alternative to creating and using subclasses, and is very XAML-friendly. In this article, we see how to give a Before going any further, I should point out that I fully recognize that this is crazy. The fact that this is the cleanest way to implement a In this demo, we do not make use of all features in Dr. WPF’s Here is the property-changed callback method for the attached /// <summary>
/// Handles changes to the IsVirtualToggleButton property.
/// </summary>
private static void OnIsVirtualToggleButtonChanged(
DependencyObject d, DependencyPropertyChangedEventArgs e)
{
IInputElement element = d as IInputElement;
if (element != null)
{
if ((bool)e.NewValue)
{
element.MouseLeftButtonDown += OnMouseLeftButtonDown;
element.KeyDown += OnKeyDown;
}
else
{
element.MouseLeftButtonDown -= OnMouseLeftButtonDown;
element.KeyDown -= OnKeyDown;
}
}
}
When a private static void OnKeyDown(object sender, KeyEventArgs e)
{
if (e.OriginalSource == sender)
{
if (e.Key == Key.Space)
{
// ignore alt+space which invokes the system menu
if ((Keyboard.Modifiers & ModifierKeys.Alt) == ModifierKeys.Alt)
return;
UpdateIsChecked(sender as DependencyObject);
e.Handled = true;
}
else if (e.Key == Key.Enter &&
(bool)(sender as DependencyObject)
.GetValue(KeyboardNavigation.AcceptsReturnProperty))
{
UpdateIsChecked(sender as DependencyObject);
e.Handled = true;
}
}
}
private static void UpdateIsChecked(DependencyObject d)
{
Nullable<bool> isChecked = GetIsChecked(d);
if (isChecked == true)
{
SetIsChecked(d,
GetIsThreeState(d) ?
(Nullable<bool>)null :
(Nullable<bool>)false);
}
else
{
SetIsChecked(d, isChecked.HasValue);
}
}
The <Style x:Key="TreeViewItemStyle" TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="True" />
<Setter Property="IsSelected" Value="{Binding IsInitiallySelected, Mode=OneTime}" />
<Setter Property="KeyboardNavigation.AcceptsReturn" Value="True" />
<Setter Property="dw:VirtualToggleButton.IsVirtualToggleButton" Value="True" />
<Setter Property="dw:VirtualToggleButton.IsChecked" Value="{Binding IsChecked}" />
</Style>
This piece ties the entire puzzle together. Note that the attached CheckBox Bug in Aero ThemeI must point out one strange, and disappointing, issue. The Aero theme for WPF’s
Revision History
| ||||||||||||||||||||