Introduction
As was mentioned in WPF Control Patterns. (WPF and XAML Patterns of Code Reuse in Easy Samples. Part 1) - WPF framework gave rise to completely new programming concepts which can also be used outside of WPF for purely non-visual programming. From my point of view, these concepts form a step above the usual OOP at least of the same magnitude as the OOP was a step above the procedural programming.
There are various WPF/XAML patterns that can be built around these new concepts. Together these patterns allow to achieve enormous code reuse and separation of concerns within the application's functionality.
In these series of articles I am trying to present and clarify these patterns by using simple WPF examples.
In the first article from the series - WPF Control Patterns. (WPF and XAML Patterns of Code Reuse in Easy Samples. Part 1) - I presented a number of patterns that deal with WPF Control
s and ControlTemplate
s. The patterns described in that article were completely agnostic of a very special DataContext
property of the WPF Control
s and were not dealing with the capability of ContentControl
s or ItemControl
s to turn a non-visual object into a visual object. This article is going to fill that gap.
I call the Patterns described in this article "Implementational" since they are applied for implementing certain visual behaviors. More complex patterns (I call them "Architectural") are left for the third installment of the series and I hope to publish an article describing them soon.
Using the XAML/WPF paradigms described here and in the previous article, I managed to program with great speed at the same time producing high quality and high density code (code with large reuse and large amount of functionality per unit of code).
Very often as the code base expands the developer productivity falls - the developers become a bit confused about their own code and it takes longer for them to figure out the best way of reusing the old functionalty. Applying the principles described in these series of articles, in fact makes the productivity and the code reuse rise as the code base expands since the developers, in fact, are building a reusable framework with the same patterns and ideas throughout it.
This article talks primarily about the patterns based on the relationship between the Views and the View Models - about making visual objects mimic completely non-visual ones.
This article is not for the beginners - I assume that the readers are familiar with basic WPF concepts - dependency and attached properties, bindings, styles, templates, data templates, triggers.
Visual and Non-Visual Objects
Visual and Non-Visual Objects
In the WPF Control Patterns. (WPF and XAML Patterns of Code Reuse in Easy Samples. Part 1) we talked about Skeleton-Meat paradigm. Lookless controls played role of a Skeleton and ControlTemplate
s were the Meat. Note, that the lookless controls still had to derive from an object of WPF's Control
type (or one of its subclasses).
In this article we are going to talk mostly about visual objects providing Meat for completely non-visual and even non-WPF Skeletons. This is the View-View Model part of the MVVM paradigm. Here are not going to be concerned about the Model part of the MVVM - it can be easily merged into or replicated by the View Model.
This section is mainly a refresher - but it is still highly recommended that you read it carefully since it discusses the known WPF controls from a different angle than most WPF text books.
DataContext Property
Every FrameworkElement
object has DataContext
dependency property. Unless overridden, this property is inherited from the object's parents within the logical tree. DataContext
object is the default source object for the WPF Binding
in a sense that if neither RelativeSource
, nor Source
, nor ElementName
is specified for a binding, the binding assumes that the its Source
object is provided by the DataContext
of the binding's target. This was done on purpose by the Microsoft people who implemented WPF, in order to make it easier for a visual object to bind its properties to a non-visual one provided by its DataContext
property.
ContentControl
Some WPF text books state that you can place any XAML code within WPF ContentControl
's Content
property. E.g. you can have a Button
(which is a descendant of a ContentControl
) containing a StackPanel
object which in turn contains several Image
objects:
<Button>
<StackPanel>
<Image Source="Image1">
<Image Source="Image2">
/<StackPanel>
/<Button>
While this is true, this is not the best way to use the Content
property of a ContentControl
and it should be avoided as much as possible.
A more useful definition of the ContentControl
should be - a control that allows to 'marry' a non-visual object pointed by its Content
property and a DataTemplate
pointed by its ContentTemplate
property into a visual.
Project SimpleContentControlSample illustrates such use of the ContentControl
s.
Non-visual object is represented by the class TextContainer
. It has just one property TheText
and a method ChangeText()
that toggles TheText
property between "Hello World" and "Bye World":
public class TextContainer : INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
protected void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#region TheText Property
private string _text = "Hello World";
public string TheText
{
get
{
return this._text;
}
set
{
if (this._text == value)
{
return;
}
this._text = value;
this.OnPropertyChanged("TheText");
}
}
#endregion TheText Property
bool _showOriginalText = true;
public void ChangeText()
{
_showOriginalText = !_showOriginalText;
if (_showOriginalText)
{
TheText = "Hello World";
}
else
{
TheText = "Bye World";
}
}
}
Note, that OnPropertyChanged
function fires PropertyChanged
event declared within INotifyPropertyChanged
interface, to inform the WPF Binding
s that the property changed.
In the Resources
section of the Window
within MainWindow.xaml file, we define the non-visual object and the DataTemplate
:
<!---->
<this:TextContainer x:Key="TheTextContainer" />
<!---->
<DataTemplate x:Key="TextContainerDataTemplate">
<StackPanel x:Name="TopLevelDataTemplatePanel"
Orientation="Horizontal">
<TextBlock Text="{Binding Path=TheText}"
VerticalAlignment="Center"/>
<Button x:Name="ChangeTextButton"
Width="120"
Height="25"
Content="Change Text"
Margin="20,5"
VerticalAlignment="Center">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<ei:CallMethodAction MethodName="ChangeText"
TargetObject="{Binding }"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</StackPanel>
</DataTemplate>
Text
property of the TextBlock
is bound to the TheText
property of the TextContainer
object. Click on ChangeTextButton
(via MS Expression Blend SDK plumbing) will result in method ChangeText()
being called.
Note: As was stated in WPF Control Patterns. (WPF and XAML Patterns of Code Reuse in Easy Samples. Part 1) you do not have to install MS Expression Blend in order to use its SDK. It can be downloaded from MS Expression Blend SDK and used for free. On top of that, we only need two dll files from the SDK: Microsoft.Expression.Interactions.dll and System.Windows.Interactivity.dll and we provide these files together with our code under MSExpressionBlendSDKDlls folder.
Since you download these files from the internet, you'll probably have to unblock them. In order to do it, right click on each of the files, choose Properties menu item and click "Unblock" button.
Finally, within the Window
we create the ContentControl
whose Content
property is set to the TextContainer
object and whose ContentTemplate
property is set to the TextContainerDataTemplate
:
<ContentControl HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="{StaticResource TheTextContainer}"
ContentTemplate="{StaticResource TextContainerDataTemplate}"/>
Here is what we get when we run the project:
Pressing "Change Text" button will toggle the text between "Hello World" and "Bye World".
Note that all the meaningful functionality to display the text and to address switching between the "Hello World" and "Bye World" strings is defined within TextContainer
object, which plays role of a View Model for our simple application. The View, defined in MainWindow.xaml file by TextContainerDataTemplate
only accounts for Visual aspects of the application and provides a wrapper or a shell around the logic defined within the TextContainer
class.
Important Note:
On top of the Content
property, the ContentControl
has the usual DataContext
property, whose value is propagated from parent to child within the logical tree as was mentioned above. The Content
property is not automatically set to the DataContext
property - if you want it to be set to the DataContext
property - you need to provide a binding: Content={Binding}
. Note, that you do not have to specify the binding path - by default {Binding}
will bind to the DataContext
property of the same object.
The Content
object of the ContentControl
becomes the DataContext
for the top level object within the data template - in our case "TopLevelDataTemplatePanel" StackPanel
gets its DataContext
property set to the TextContainer
View Model object. Correspondingly the same object gets passed as the DataContext
down the logical tree within the DataTemplate
to the TextBlock
and the Button
elements.
ItemsControl
While ContentControl
is great for displaying a non-visual object, ItemsControl
is used for displaying a collection of non-visual objects. ItemsSource
property of ItemsControl
is set (usually via binding) to a collection of non-visual objects. Its ItemTemplate
property is set to a DataTemplate
that provides visual representation for every item within the collection:
Note that ItemsSource
can be set to any collection, but only if the ItemsSource
collection implements INotifyCollectionChanged
interface (or implements ObservableCollection<T>
) the visuals will update when items are added or removed from the ItemsSource
collection. Otherwise the visuals will only change when ItemsSource
itself is reset.
Project SimpleItemsControlSample
demonstrates how to use ItemsControl
.
Its TextContainer
class represents a single non-visual (View Model) object while TextContainerTestCollection
class represents a collection of such non-visual objects.
TextContainer
class is different from the one in the previous sample. Instead of hardcoding the the strings to show before and after the button is clicked, we allow to pass those strings within the TextContainer
's constructor:
string _originalText = null;
string _alternateText = null;
public TextContainer(string originalText, string alternateText)
{
_originalText = originalText;
_alternateText = alternateText;
}
TheText
property implementation also slightly changed simply to demonstrate a more elegant way of implementing it. It no longer has a Setter and it returns either _originalText
or _alternativeText
string depending on the value of _showOriginalString
flag:
#region TheText Property
public string TheText
{
get
{
if (_showOriginalText)
{
return _originalText;
}
else
{
return _alternateText;
}
}
}
#endregion TheText Property
Method ChangeText()
toggles the _showOriginalString
flag and calls OnPropertyChanged("TheText")
in order to notify the UI that TheText
property has changed:
bool _showOriginalText = true;
public void ChangeText()
{
_showOriginalText = !_showOriginalText;
OnPropertyChanged("TheText");
}
TextContainerTestCollection
class is derived from ObservableCollection<TextContainer>
. Its constructor adds 3 TextContainer
items to itself:
public TextContainerTestCollection()
{
Add(new TextContainer("Hi World", "Bye World"));
Add(new TextContainer("Hi Friend", "Bye Friend"));
Add(new TextContainer("Hi WPF", "Bye WPF"));
}
A TextContainerTestCollection
object is defined in the Window.Resources
section of the XAML. We also define there TextContainerDataTemplate
in exactly the same way as we did in the previous sample:
<Window.Resources>
<!-- non-visual object -->
<this:TextContainerTestCollection x:Key="TheTextContainerTestCollection" />
<!-- data template built around the TextContainer -->
<DataTemplate x:Key="TextContainerDataTemplate">
<StackPanel x:Name="TopLevelDataTemplatePanel"
Orientation="Horizontal">
<TextBlock Text="{Binding Path=TheText}"
VerticalAlignment="Center" />
<Button Width="120"
Height="25"
Content="Change Text"
Margin="20,5"
VerticalAlignment="Center">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<ei:CallMethodAction MethodName="ChangeText"
TargetObject="{Binding }" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</StackPanel>
</DataTemplate>
</Window.Resources>
Finally, we define the ItemsControl
setting its ItemsSource
to the non-visual collection and its ItemTemplate
to the data template:
<!---->
<ItemsControl HorizontalAlignment="Center"
VerticalAlignment="Center"
ItemsSource="{StaticResource TheTextContainerTestCollection}"
ItemTemplate="{StaticResource TextContainerDataTemplate}"/>
Here is how the application looks when we run it:
Pressing the button at each item will change the item's text to its alternative text. Pressing it again will change it back.
Using ControlTemplate for ContentControl
The final topic, I'd like to discuss in this section is how to use ControlTemplate
for changing the look of a content control.
The solution for this sub-section is called CustomToggleControl
. It consist of the main project (also of the same name) and it also reference a project CustomControls
.
The purpose of the project is to define a ToggleButton
as a ContentControl
and to show how to provide several different ControlTemplate
s for it.
The MyToggleButton
class represents the Lookless
Skeleton for the Toggle button. It derives from ContentControl
class. It defines one Boolean dependency property IsChecked
:
#region IsChecked Dependency Property
public bool IsChecked
{
get { return (bool)GetValue(IsCheckedProperty); }
set { SetValue(IsCheckedProperty, value); }
}
public static readonly DependencyProperty IsCheckedProperty =
DependencyProperty.Register
(
"IsChecked",
typeof(bool),
typeof(MyToggleButton),
new PropertyMetadata(false)
);
#endregion IsChecked Dependency Property
It also sets an event handler for MouseUp
event to toggle the IsChecked
property on MouseUp
:
public MyToggleButton()
{
this.MouseUp += MyToggleButton_MouseUp;
}
void MyToggleButton_MouseUp(object sendr, System.Windows.Input.MouseButtonEventArgs e)
{
this.IsChecked = !this.IsChecked;
}
In CustomControls.xaml file, we define two control templates for the same MyToggleButton
control within two different styles - "MyToggleButtonCheckBoxStyle" that displays the toggle button as a check box and "ToggleButton_ButtonStyle" that display the toggle button as a button.
Before describing the templates, let us take a look at the main file for the application. It contains two instances of the MyToggleButton
control within a vertical StackPanel
:
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center">
<customControls:MyToggleButton Content="My Toggle Button - Check Box Style:"
Style="{StaticResource MyToggleButtonCheckBoxStyle}"
Foreground="White"
Background="Black"
Margin="10,10" />
<customControls:MyToggleButton Content="My Toggle Button - Button Style:"
Foreground="White"
Background="Black"
Style="{StaticResource ToggleButton_ButtonStyle}"
Margin="10,10" />
</StackPanel>
When we run the application, we get the following:
The check box style button displays a check mark within the square on its right hand side when its IsChecked
property is true, while the button style one changes the background to darker.
Now let us take a look at the corresponding ControlTemplate
s.
The check box template has a horizontal StackPanel
that contains a ContentPresenter
and a Border
. The border represents the square with the checkbox. ContentPresenter
is a special object that controls where the DataTemplate
of the ContentControl
is going to be rendered. It adopts the shape and form defined by the DataTemplate
of the ContentControl
. If the DataTemplate
is not defined, it will simply render the ContentPresenter
as a TextBlock
displaying the string representation of the Content
property of the ContentControl
:
<ControlTemplate TargetType="this:MyToggleButton">
<Grid Background="Transparent">
<StackPanel Orientation="Horizontal"
Background="{TemplateBinding Background}">
<ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
<!-- the square on the right hand side-->
<Border Width="20"
Height="20"
BorderThickness="1"
BorderBrush="{TemplateBinding Foreground}"
Background="{TemplateBinding Background}"
Margin="10, 1, 0, 1">
<!-- this the the check mark. It is visible when
IsChecked property of the control is true and
invisible otherwise -->
<TextBlock Text="V"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{TemplateBinding Foreground}"
Visibility="{Binding Path=IsChecked,
Converter={StaticResource TheBoolToVisConverter},
RelativeSource={RelativeSource AncestorType=this:MyToggleButton}}" />
</Border>
</StackPanel>
</Grid>
</ControlTemplate>
The button style toggle button has Grid
panel containing the border with a ContentPresenter
. It also has an opaque Border
object that overlays the original border. By default the opaque border has opacity 0.5. Using triggers we reduce this opacity on MouseOver to 0.3 and when the control is checked, the opacity is further reduced to 0:
<ControlTemplate TargetType="this:MyToggleButton">
<Grid Background="Transparent"
x:Name="ItemPanel">
<Border x:Name="TheItemBorder"
Background="Black"
BorderBrush="White"
BorderThickness="1">
<ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="10, 5" />
</Border>
<!-- provide some opacity for the control -->
<Border x:Name="TheOpacityBorder"
Background="White"
Opacity="0.5" />
</Grid>
<ControlTemplate.Triggers>
<!-- reduce the opacity of the opacity border
to 0.3 on Mouse Over -->
<Trigger Property="IsMouseOver"
Value="True">
<Setter TargetName="TheOpacityBorder"
Property="Opacity"
Value="0.3" />
</Trigger>
<!-- reduce the opacity of the opacity border
to 0 when the button is checked -->
<Trigger Property="IsChecked"
Value="True">
<Setter TargetName="TheOpacityBorder"
Property="Opacity"
Value="0" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
The main idea that I wanted to convey within this sub-section is that the ControlTemplate
for a ContentControl
can define a ContentPresenter
object whose visual will be determined by the DataTemplate
of the ContentControl
.
MVVM based Patterns
Now we are finally going to discuss the MVVM based patterns.
Single Selection View Model Pattern
I like the basic WPF controls e.g. ContentControl
and ItemsControl
. I like the derived WPF controls (e.g. RadioButton
or ListView
and ListBox
) considerably less - from my point of view they were created before the WPF best practices became known and are not taking full advantage of them.
In this sub-section we are going to show how to use the View Model concepts for creating an ItemsControl
with single selection, mimicking the RadioButton
functionality - i.e. allowing to select at most one item out of many at a time: if an item is already selected and the user selects another item, the first selected items becomes unselected.
Later we shall show how to create a more general functionality for keeping 2 or more last items selected.
Note that ListView
and ListBox
already have the functionality for single item selection, but they cannot be easily generalized to keep 2 or more items selected.
Note also that since the item selection functionality is defined in a non-visual View Model, we can easily unit test it as will be shown below.
The solution containing the code is called "UniqueSelectionPatternSample". Its main project has the same name and it references two other projects within the same solution: ViewModels
, TestViewModels
. The solution also contains MS Unit Test project SelectionTests
.
ViewModels
project defines the non-visual objects for this and several other samples. In this sample we'll be looking at interface ISelectable
and classes SelectableItemBase
, SelectableItemWithDisplayName
and CollectionWithUniqueItemSelection
defined within ViewModels
project.
TestViewModels
project contains a number of classes representing the collections defined in ViewModels
project populated with some test data. In this sample we'll be using MyTestButtonsWithSelectionVM
class from it.
SelectionTests
project provides the unit tests for non-visual classes defined under ViewModels
project.
Let us start describing the sample by showing what it does. Make UniqueSelectionPatternSample
project the start-up project of the solution. Then run the solution. You will see a row of four buttons. Try clicking the buttons - you'll see that when a button is selected, button that had been selected before is unselected:
The functionality that controls selection/unselection is located within a purely non-visual class CollectionWithUniqueItemSelection<T>
defined under ViewModels
project. This collection extends ObservableCollection<T>
and its generic argument T
has to implement ISelectable
interface.
ISelectable
declares IsItemSelected
property and IsItemSelectedChangedEvent
event:
public interface ISelectable
{
bool IsItemSelected { get; set; }
event Action<iselectable> ItemSelectedChangedEvent;
}
</iselectable>
The implementations of this interface have to fire the ItemSelectedChangedEvent
after IsItemSelected
property changes.
SelectableItemBase
is a basic implementation of this interface. It fires the ItemSelectionChangedEvent
when the IsItemSelected
property changes; it also fires the PropertyChanged
event so that the UI could detect the property change:
public class SelectableItemBase : ISelectable, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropChanged(string propName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
public event Action<ISelectable> ItemSelectedChangedEvent;
bool _isItemSelected = false;
public bool IsItemSelected
{
get
{
return _isItemSelected;
}
set
{
if (_isItemSelected == value)
return;
_isItemSelected = value;
if (ItemSelectedChangedEvent != null)
ItemSelectedChangedEvent(this);
OnPropChanged("IsItemSelected");
}
}
}
Class SelectableItemWithDisplayName
extends SelectableItemBase
by adding a property DisplayName
to it and a method ToggleSelection()
. This method simply toggles the selection state of the item:
public class SelectableItemWithDisplayName : SelectableItemBase
{
public SelectableItemWithDisplayName(string displayName)
{
DisplayName = displayName;
}
public string DisplayName
{
get;
private set;
}
public void ToggleSelection()
{
this.IsItemSelected = !this.IsItemSelected;
}
}
Now let us take a close look at CollectionWithUniqueItemSelection<T>
class. It defines SelectedItem
property that should contain item which is currently selected. If the collection contains no selected item, this property should be null. When SelectedItem
is set to some item within the collection, this item's IsItemSelected
property is set to true
, while the previous selected item's IsItemSelected
property is set to false
:
T _selectedItem = null;
public T SelectedItem
{
get
{
return _selectedItem;
}
set
{
if (_selectedItem == value)
return;
if (_selectedItem != null) {
_selectedItem.IsItemSelected = false;
}
_selectedItem = value;
if (_selectedItem != null) {
_selectedItem.IsItemSelected = true;
}
OnPropertyChanged(new PropertyChangedEventArgs("SelectedItem"));
}
}
Whenever item is added to the collection, we add the method item_ItemSelectedChagnedEvent(ISelectable item)
to be the handler for the item's ItemSelectedChangedEvent
. Whenever the item is removed from the collection, we remove the corresponding handler. Adding the handler is achieved by ConnectItems
method:
void ConnectItems(IEnumerable items)
{
if (items == null)
return;
foreach (T item in items)
{
item.ItemSelectedChangedEvent += item_ItemSelectedChangedEvent;
}
}
This method is called by the Init()
method (for the original items within the collection):
void Init()
{
ConnectItems(this);
this.CollectionChanged += CollectionWithUniqueItemSelection_CollectionChanged;
}
It is also called in the handler to the CollectionChanged
event for the new items within the collection:
void CollectionWithUniqueItemSelection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
DisconnectItems(e.OldItems);
ConnectItems(e.NewItems);
}
DisconnectItems(IEnumerable items)
method is called to disconnect the handlers for the items removed from the collection:
void DisconnectItems(IEnumerable items)
{
if (items == null)
return;
foreach (T item in items)
{
item.ItemSelectedChangedEvent -= item_ItemSelectedChangedEvent;
}
}
As was shown above it is called by CollectionWithUniqueItemSelection_CollectionChanged
method (which is a handler for the CollectionChanged
event) to disconnect the items that are removed from the collection.
The method item_ItemSelectedChangedEvent(ISelectable item)
(which is assigned to be a IsItemSelectedChangedEvent
handler for every item within the collection) takes care of setting SelectedItem
property for the currently selected item:
void item_ItemSelectedChangedEvent(ISelectable item)
{
if (item.IsItemSelected)
{
this.SelectedItem = (T)item;
}
else
{
this.SelectedItem = null;
}
}
Thus, there are two equivalent ways of choosing selected item within the CollectionWithUniqueItemSelection
class:
- By setting the corresponding item's
IsItemSelected
property
- By setting the collection's
SelectedItem
property to the corresponding item.
Besides, the two methods are completely equivalent - if you select (or unselect) an item using one of the above methods, the other method's condition will also be satisfied. To verify that it is true, I've created a unit test UniqueSelectionTests
class within SelectionTests
project.
In the constructor of the class, we create _testCollection
as a CollectionWithUniqueItemSelection<SelectableItemWithDisplayName>
and populated it with three items with DisplayName
s "1", "2" and "3":
public UniqueSelectionTests()
{
_testCollection = new CollectionWithUniqueItemSelection<SelectableItemWithDisplayName>();
_testCollection.Add(new SelectableItemWithDisplayName("1"));
_testCollection.Add(new SelectableItemWithDisplayName("2"));
_testCollection.Add(new SelectableItemWithDisplayName("3"));
}
Method TestAllUnselected
verifies that there is no selected item within the collection - it checks that SelectedItem
property is set to null and all the individual items within the collection have IsItemSelected
property set to false:
void TestAllUnselected()
{
Assert.IsNull(_testCollection.SelectedItem);
foreach (SelectableItemWithDisplayName selectableItem in _testCollection)
{
Assert.IsFalse(selectableItem.IsItemSelected);
}
return;
}
Method TestSelected(string selectedItemDisplayName)
tests that an item with the corresponding display name is selected and only that item is selected - it checks that SelecteItem
has the required display name and also that only the item with that display name has IsItemSelected
property set to true
while the rest of the items have it set to false
:
void TestSelected(string selectedItemDisplayName)
{
Assert.IsNotNull(_testCollection.SelectedItem);
Assert.AreEqual<string>(_testCollection.SelectedItem.DisplayName, selectedItemDisplayName);
Assert.IsTrue(_testCollection.SelectedItem.IsItemSelected);
foreach (SelectableItemWithDisplayName selectableItem in _testCollection)
{
if (selectedItemDisplayName.Equals(selectableItem.DisplayName))
{
Assert.IsTrue(selectableItem.IsItemSelected);
}
else
{
Assert.IsFalse(selectableItem.IsItemSelected);
}
}
}
</string>
There are two methods in charge of selecting items and two methods in change of unselecting items - one of each pair corresponds to selecting (or unselecting) via SelectedItem
property while the other - via the IsItemSelected
flag:
void SelectItemViaSettingSelectedItem(string itemDisplayName)
{
SelectableItemWithDisplayName itemToSelect = FindItemByDisplayName(itemDisplayName);
_testCollection.SelectedItem = itemToSelect;
}
void UnselectItemViaUnsettingSelectedItem()
{
_testCollection.SelectedItem = null;
}
void SelectItemViaFlag(string itemDisplayName)
{
SelectOrUnselectItemViaFlag(itemDisplayName, true);
}
void UnselectItemViaFlag(string itemDisplayName)
{
SelectOrUnselectItemViaFlag(itemDisplayName, false);
}
The IsItemSelected
flag changing functions call SelectOrUnselectItemViaFlag
utility method that in turns calls FindItemByDisplayName
utility method:
SelectableItemWithDisplayName FindItemByDisplayName(string itemDisplayName)
{
SelectableItemWithDisplayName itemToSelect =
_testCollection.
Where((item) => (item as SelectableItemWithDisplayName).DisplayName.Equals(itemDisplayName)).First();
return itemToSelect;
}
void SelectOrUnselectItemViaFlag(string itemDisplayName, bool selectOrUnselect)
{
SelectableItemWithDisplayName itemToSelect = FindItemByDisplayName(itemDisplayName);
itemToSelect.IsItemSelected = selectOrUnselect;
}
The "main" test method is TestSelectingItem()
it calls various selection/unselection methods followed by method that test that the corresponding item is indeed selected (or unselected):
[TestMethod]
public void TestSelectingItem()
{
TestAllUnselected();
SelectItemViaFlag("2");
TestSelected("2");
SelectItemViaFlag("1");
TestSelected("1");
UnselectItemViaFlag("1");
TestAllUnselected();
SelectItemViaSettingSelectedItem("2");
TestSelected("2");
SelectItemViaSettingSelectedItem("3");
TestSelected("3");
UnselectItemViaUnsettingSelectedItem();
TestAllUnselected();
}
You can run this method by right mouse clicking on its declaration and choosing "Run Tests" or "Debug Tests" option.
Finally let us talk about "main" UniqueSelectionPatternSample
project. All its code is located within MainWindow.xaml file. It uses MyTestButtonsWithSelectionVM
object (defined in TestViewModels
) project as its View Model:
<testVMs:MyTestButtonsWithSelectionVM x:Key="TheTestButtonsWithSelectionVM" />
MyTestButtonsWithSelectionVM
is simply a CollectionWithUniqueItemSelection<SelectableItemWithDisplayName>
that populates itself with four items:
public class MyTestButtonsWithSelectionVM : CollectionWithUniqueItemSelection<SelectableItemWithDisplayName>
{
public MyTestButtonsWithSelectionVM()
{
Add(new SelectableItemWithDisplayName("Button 1"));
Add(new SelectableItemWithDisplayName("Button 2"));
Add(new SelectableItemWithDisplayName("Button 3"));
Add(new SelectableItemWithDisplayName("Button 4"));
}
}
Coming back to MainWindow.xaml file - let us take a look at the ItemsControl
that represents the selectable buttons:
<ItemsControl x:Name="SingleButtonSelectionControl"
ItemsSource="{StaticResource TheTestButtonsWithSelectionVM}"
ItemTemplate="{StaticResource ItemButtonDataTemplate}"
VerticalAlignment="Center"
HorizontalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<!-- arrange the items horizontally -->
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<!-- make sure that each item has margin 5 pixels to the left and right of it-->
<Style TargetType="FrameworkElement">
<Setter Property="Margin"
Value="5,0" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
The ItemControl
's ItemsSource
property is set to the TheTestButtonsWithSelectionVM
resource object (that contains the TestButtonsWithSelectionVM
collection), its ItemTemplate
property is pointing to ItemButtonDataTemplate
resource (to be discussed shortly).
Here is the DataTemplate
for individual items within the ItemsControl
:
<DataTemplate x:Key="ItemButtonDataTemplate">
<Grid Background="Transparent"
x:Name="ItemPanel">
<Border x:Name="TheItemBorder"
Background="Black"
BorderBrush="White"
BorderThickness="1">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="White"
Text="{Binding Path=DisplayName}"
Margin="10, 5" />
</Border>
<Border x:Name="TheOpacityBorder"
Background="White"
Opacity="0.5" />
<i:Interaction.Triggers>
<!-- Call ToggleSelection method when when MouseDown event
is fired on the item -->
<i:EventTrigger EventName="MouseDown">
<ei:CallMethodAction MethodName="ToggleSelection"
TargetObject="{Binding Path=DataContext, ElementName=ItemPanel}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Grid>
<DataTemplate.Triggers>
<!-- make TheOpacityBorder less opaque when the mouse is over-->
<Trigger Property="IsMouseOver"
Value="True">
<Setter TargetName="TheOpacityBorder"
Property="Opacity"
Value="0.3" />
</Trigger>
<DataTrigger Binding="{Binding Path=IsItemSelected}"
Value="True">
<!-- make TheOpacityBorder completely transparent when the
item is selected-->
<Setter TargetName="TheOpacityBorder"
Property="Opacity"
Value="0" />
<!-- make the ItemPanel non-responsive to the event when
the item is selected (this is to prevent unselection
when clicking on the same item again) -->
<Setter TargetName="ItemPanel"
Property="IsHitTestVisible"
Value="False" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
The visual item itself consist of a Border
(named TheItemBorder
) object with TextBlock
object within it. The Text
property of the TextBlock
is bound to DisplayName
property of its own DataContext
(which contains the corresponding item within the collection). There is also TheOpacityBorder
Border
that overlays TheItemBorder
object. By default its opacity is 0.5. The DataTemplate
's triggers ensure that when the mouse is over the visual item, its opacity is reduced to 0.3 and then the item is selected, its opacity is further reduced to 0. Using MS Expression Blend SDK plumbing we ensure that every time the item is clicked, method ToggleSelection()
is called on the corresponding item's View Model object.
There is also a trigger to set IsHitTestVisible
value to False
when the item is selected - this is done in order to prevent item unselection on click - to mimic the RadioButton
functionality.
Single Selection Implementation with Custom Controls
The above sample implements single selection with the ItemTemplate
created from scratch. This might create some problems for reuse. For example if we want to reuse the above DataTemplate
for different View Model, the new View Model will also have to have the same property and method names, i.e. whatever text we want to display within the item, should be provided by DisplayName
property, the selection state should be given by IsItemSelected
property and it will need a method ToggleSelection()
in order to change the selection state.
Instead of creating such templates from scratch, it is better to use a custom control that has properties that reflect the text to show and the selection state. Even if we use a View Model with similar properties named differently all we need to do is to change the bindings on such control for the properties to be consumed.
We showed above how to create MyToggleButton
custom control. This control can be directly used for our single selection implementation. Its Content
property can be bound to DisplayName
property of the View Model in order to display text contained in it, while its IsChecked
property can be bound to IsItemSelected
property of the View Model.
The solution that shows how to use MyToggleButton
custom control for single selection is called "UniqueSelectionWithCustomControl". Its main project (of the same name) differs from the one described above only in how TheItemButtonDataTemplate
is constructed within MainWindow.xaml file's Window.Resources
section:
<DataTemplate x:Key="ItemButtonDataTemplate">
<customControls:MyToggleButton x:Name="TheToggleButton"
Content="{Binding Path=DisplayName}"
Foreground="White"
Background="Black"
IsChecked="{Binding Path=IsItemSelected, Mode=TwoWay}"
Style="{StaticResource ToggleButton_ButtonStyle}"/>
<DataTemplate.Triggers>
<Trigger SourceName="TheToggleButton"
Property="IsChecked"
Value="True">
<Setter Property="IsHitTestVisible"
Value="False" />
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
MyToggleButton
class provides an adequate implementation for the selectable buttons with its Content
property bound to DisplayName
and its IsChecked
property bound to IsItemSelected
property of the View Model object.
When you run this project you'll get exactly the same visuals and behavior as in the previous sample.
Now change the MyToggleButton
's style to MyToggleButtonCheckBoxStyle
. You will see check boxes instead of buttons with the same single selection principle - when a checkbox is checked, the previously selected check box is unselected:
Note that replacing the selectable buttons by the check boxes was achieved by changing a single word within the XAML file.
Non-Visual Behaviors Pattern
In WPF Control Patterns. (WPF and XAML Patterns of Code Reuse in Easy Samples. Part 1) we gave an example of a behavior pattern for the WPF controls as a non-invasive way of modifying the control's behavior. Here we are going to present the non-visual behaviors as an (almost) non-invasive way of modifying the non-visual object's behavior.
Note that CollectionWithUniqueItemSelection<T>
class that we used in the previous samples to create the non-visual single selection, has a lot of code dedicated specifically to single selection functionality. In fact almost all of its code (about 90 lines) is dealing with single selection. Behaviors would allow to move all of this code outside of the class into the behavior class. Then we can reuse the same collection class with different behaviors resulting in completely different selection algorithms.
Single Selection Behavior
NonVisualBehaviorsSample solution demonstrates creating a behavior for single selection. Instead of using CollectionWithUniqueSelection<T>
for our View Model collection, we use SelectableItemCollection<T>
. It is much simpler than CollectionWithUniqueSelection<T>
. Just like CollectionWithUniqueSelection<T>
, it extends ObservableCollection<T>
but its functionality consists of only one property TheBehavior
of the type IBehavior
. This property serves for attaching the behavior to the collection:
public class SelectableItemCollection<T> : ObservableCollection<T>
where T : class, ISelectable
{
IBehavior _behavior = null;
public IBehavior TheBehavior
{
get
{
return _behavior;
}
set
{
if (_behavior == value)
return;
if (_behavior != null) _behavior.OnDetach();
_behavior = value;
if (_behavior != null) _behavior.OnAttach(this);
}
}
}
IBehavior
is a very simple interface defined in the same project ViewModels
:
public interface IBehavior
{
void OnAttach(IEnumerable collectionToAttachTo);
void OnDetach();
}
Most of the selection behavior functionality is located within SelectionBehaviorBase<T>
class. The two selection behaviors that we present in this article are derived from it.
This class ensures that every ISelectable
item within the collection to which the behavior is attached, gets its ItemSelectedChangedEvent
handled by OnItemSelectionChanged(ISelectable item)
function. It also takes care of removing the event handler if the corresponding item is removed from the collection.
Similar to CollectionWithUniqueSelection<T>
, the functionality for removing and adding the even handler is provided by the methods DisconnectItems(IEnumerable items)
and ConnectItems(IEnumerable items)
:
void DisconnectItems(IEnumerable items)
{
if (items == null)
return;
foreach (ISelectable item in items)
{
item.ItemSelectedChangedEvent -= OnItemSelectionChanged;
}
}
void ConnectItems(IEnumerable items)
{
if (items == null)
return;
foreach (ISelectable item in items)
{
item.ItemSelectedChangedEvent += OnItemSelectionChanged;
}
}
These methods are called when the TheCollection
property of the behavior is reset:
ObservableCollection<t> _collection = null;
ObservableCollection<t> TheCollection
{
get
{
return _collection;
}
set
{
if (_collection == value)
return;
if (_collection != null)
{
_collection.CollectionChanged -= _collection_CollectionChanged;
}
DisconnectItems(_collection);
_collection = value;
ConnectItems(_collection);
if (_collection != null)
{
_collection.CollectionChanged += _collection_CollectionChanged;
}
}
}
</t></t>
Also they are called for the items removed or added to the collection:
void _collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
DisconnectItems(e.OldItems);
ConnectItems(e.NewItems);
}
IBehavior
methods OnAttach(IEnumerable selectableItemCollectionToAttachTo)
and OnDetach()
simply set the TheCollection
propert of the behavior:
public void OnAttach(IEnumerable selectableItemCollectionToAttachTo)
{
if (TheCollection != null)
throw new Exception("Programming Error: Selection Behavior cannot attach to more than one collection");
TheCollection = selectableItemCollectionToAttachTo as ObservableCollection<t>;
}
public void OnDetach()
{
TheCollection = null;
}
</t>
The selection event handler provided by OnItemSelectionChanged(ISelectableItem item)
function is not implemented within the SelectionBehaviorBase<T>
class - it is declared as an abstract method to be overridden within the subclasses.
UniqueSelectionBehavior<T>
class extends SelectionBehaviorBase<T>
providing the functionality for single item selection. Its SelectedItem
property allows selection by choosing an item (just like CollectionWithUniqueSelection<T>
) and its implementation of OnItemSelectionChanged(ISelectable item)
event handler method allows to select or deselect an item by changing its IsItemSelected
property:
public class UniqueSelectionBehavior<T> : SelectionBehaviorBase<T>
where T : class, ISelectable
{
T _selectedItem = null;
public T SelectedItem
{
get
{
return _selectedItem;
}
set
{
if (_selectedItem == value)
return;
if (_selectedItem != null)
{
_selectedItem.IsItemSelected = false;
}
_selectedItem = value;
if (_selectedItem != null)
{
_selectedItem.IsItemSelected = true;
}
OnPropChanged("SelectedItem");
}
}
protected override void OnItemSelectionChanged(ISelectable item)
{
if (item.IsItemSelected)
{
this.SelectedItem = (T)item;
}
else
{
this.SelectedItem = null;
}
}
}
As a view model for the collection of items we use MyTestButtonsWithSelectionAndBehavior
class defined under TestViewModels
project. It extends SelectableItemCollection<SelectableItemWithDisplayName>
collection and adds four items to it:
public class MyTestButtonsWithSelectionAndBehavior : SelectableItemCollection<SelectableItemWithDisplayName>
{
public MyTestButtonsWithSelectionAndBehavior()
{
Add(new SelectableItemWithDisplayName("Button 1"));
Add(new SelectableItemWithDisplayName("Button 2"));
Add(new SelectableItemWithDisplayName("Button 3"));
Add(new SelectableItemWithDisplayName("Button 4"));
}
}
And here is how the behavior and the collection is defined in MainWindow.xaml
file of the main project:
<!---->
<viewModels:UniqueSelectionBehaviorOnSelectableWithDisplayName x:Key="TheUniqueSelectionBehavior" />
<!---->
<testViewModels:MyTestButtonsWithSelectionAndBehavior x:Key="TheButtonsWithSelectionBehaviorVM"
TheBehavior="{StaticResource TheUniqueSelectionBehavior}"/>
The rest of the MainWindow.xaml
file is almost identical to the one of the previous example aside from the fact that we use "TheButtonsWithSelectionBehaviorVM" collection as the ItemsSource
for the ItemsControl
.
As a result, we obtain a radio button behavior identical to the one of the previous sample:
Two Last Items Selection Behavior
This sample implements a different behavior on the same collection. According to this behavior, we allow to have at most two items selected at the same time within the collection. When a user selects a third item, the item that has been selected longest gets unselected.
This sample is located in "TwoLastItemsSelectionBehaviorSample" solution. Its code is almost identical to the code of the previous sample aside from the behavior. Instead of UniqueSelectionBehaviorOnSelectableWithDisplayName
, we use TwoLastItemSelectionBehaviorOnSelectableWithDisplayName
, which is derived from TwoLastItemsSelectionBehavior<SelectableItemWithDisplayName>
class.
All of the behavior's code is located in the superclass TwoLastItemsSelectionBehavior<T>
. Just like UniqueSelectionBehavior<T>
it is derived from SelectionBehaviorBase<T>
and overrides the OnItemSelectionChanged(ISelectable item)
method (as a reminder - this method is called whenever the IsItemSelected
property changes on any item within the collection).
TwoLastItemsSelectionBehavior<T>
has a List<T>
field _selectedItems
which controls selection and unselection. It contains the currently selected items with items that have been selected longer being closer to the end of the collection. The method that controls adding a new item to the _selectedItems
collection is AddSelectedItem(T itemToAdd)
:
List<T> _selectedItems = new List<T>();
int NumberSelectedItems
{
get
{
return _selectedItems.Count;
}
}
void AddSelectedItem(T itemToAdd)
{
if (NumberSelectedItems >= 2)
{
T itemToRemove = _selectedItems.Last();
_selectedItems.Remove(itemToRemove);
itemToRemove.IsItemSelected = false;
}
_selectedItems.Insert(0, itemToAdd);
}
AddSelectedItem(T itemToAdd)
is called by OnItemSelectionChanged(ISelectable item)
method:
protected override void OnItemSelectionChanged(ISelectable item)
{
if (!item.IsItemSelected)
{
_selectedItems.Remove((T)item);
}
else
{
AddSelectedItem((T)item);
}
OnPropChanged("SelectedItems");
OnPropChanged("NumberSelectedItems");
}
Now, the only minor changes to the MainWindow.xaml file (in comparison to the previous sample) is defining TwoLastItemSelectionBehaviorOnSelectableWithDisplayName
object and using it as the behavior on the MyTestButtonsWithSelectionAndBehavior
object instead of UniqueSelectionBehaviorOnSelectableWithDisplayName
:
<viewModels:TwoLastItemSelectionBehaviorOnSelectableWithDisplayName x:Key="TheTwoLastItemsSelectionBehavior" />
<testViewModels:MyTestButtonsWithSelectionAndBehavior x:Key="TheButtonsWithSelectionBehaviorVM"
TheBehavior="{StaticResource TheTwoLastItemsSelectionBehavior}" />
When we run the sample, we'll get the needed behavior - if two buttons are selected and you click another one, the button that had been selected the longest will get unselected:
Note that we changed the behavior without changing either the View or the View Model, only behavior attached to the View Model was changed. Thus we showed how to modify the behavior of a collection of items with minimal invasiveness.
Note, also, that simply by changing a style/template for the toggle buttons, we can produce the same behavior with check boxes:
The above was achieved by replacing a reference to ToggleButton_ButtonStyle
by a reference to MyToggleButtonCheckBoxStyle
in ItemButtonDataTemplate
defined within MainWindow.xaml file.
We can easily generalize 2 last items selection to N last items selection just by replacing the number 2 within the behavior by an integer parameter. Also we can easily create considerably more complex selection behaviors based on the same principle.
I used 2 last items selection behavior for displaying mulitple charts within the same Dev Express'es ChartControl
object corresponding to two different Y axes - one axis on the left and one on the right. If you have more than 2 charts, you can allow the users to choose any two of them to display, by using the 2 last items selection principle.
Recursive Data Templates
A lot of concepts whether in real world or in software development have a recursive tree structure where items can be represented by tree nodes - each node might have multiple child nodes and one parent node. The nodes without child nodes are called the leaves of the tree. There can be only one node without a parent and it is called the root of the tree.
Example of such trees would be a file system, a reporting structure within an organization, WPF's logical and visual trees.
It turns out that such trees within the View can be easily represented by WPF's DataTemplates that refer to themselves - i.e. recursive data templates.
Recursive data template sample is located within RecursiveDatateTemplatesSample solution with the main project that has the same name. If you run the solution and expand all its nodes, here is what you are going to see:
You can see that this is a tree representation of some mocked up file system (which does not have any to do with the file system on your machine). It has folder Root that contains two folders Documents and Pictures and file MySystemFile. Documents folder contains two files Document1 and Document2, while Picture folder contains Picture1 and Picture2.
As you will see below, we are not using WPF's TreeView
control to display this file system.
First, let us explain the View Model that represents this tree structure.
Class TreeNodeBase
under ViewModels
project is the basic class for a tree node. It has Parent
and Children
properties to represent the parent and children of the tree node correspondingly. It also has a property HasChildren
that is false
if Children
collection is null or empty and true
otherwise:
public class TreeNodeBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
protected void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#region Parent Property
private TreeNodeBase _parent;
public TreeNodeBase Parent
{
get
{
return this._parent;
}
set
{
if (this._parent == value)
{
return;
}
this._parent = value;
this.OnPropertyChanged("Parent");
}
}
#endregion Parent Property
#region Children Property
private ObservableCollection<treenodebase> _children;
public ObservableCollection<treenodebase> Children
{
get
{
return this._children;
}
set
{
if (this._children == value)
{
return;
}
if (this._children != null)
{
this._children.CollectionChanged -= _children_CollectionChanged;
}
this._children = value;
if (this._children != null)
{
this._children.CollectionChanged += _children_CollectionChanged;
}
this.OnPropertyChanged("Children");
this.OnPropertyChanged("HasChildren");
}
}
void _children_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
this.OnPropertyChanged("HasChildren");
}
#endregion Children Property
public bool HasChildren
{
get
{
return (Children != null) && (Children.Count > 0);
}
}
}
</treenodebase></treenodebase>
Class ViewModels.FileTreeNode
is derived from TreeNodeBase
:
public class FileTreeNode : TreeNodeBase
{
public bool FileOrFolder { get; set; }
public string Name { get; set; }
public FileTreeNode AddChild(bool fileOrFolder, string name)
{
if (this.FileOrFolder)
throw new Exception("Cannot add a child to a file");
if (this.Children == null)
{
this.Children = new ObservableCollection<treenodebase>();
}
FileTreeNode newNode = new FileTreeNode
{
FileOrFolder = fileOrFolder,
Name = name,
Parent = this
};
Children.Add(newNode);
return newNode;
}
#region IsExpanded Property
private bool _isExpanded = false;
public bool IsExpanded
{
get
{
return this._isExpanded;
}
set
{
if (this._isExpanded == value)
{
return;
}
this._isExpanded = value;
this.OnPropertyChanged("IsExpanded");
}
}
#endregion IsExpanded Property
}
</treenodebase>
It has FileOrFolder
Boolean property that is true when the node is a file and false if it is a folder. Also it has Name
property corresponding to the file or folder name. Finally it defines IsExpanded
property which we use to specify whether the corresponding node is expanded (and you can see its children) or not.
There is also public FileTreeNode AddChild(bool fileOrFolder, string name)
method that allows adding a child to the current node, specifying the child's FileOrFolder
and Name
properties. It returns the child itself so that one can add the children to the newly created child.
The View Model for this sample is built by MySimpleFileSystem
class defined under TestViewModels
project. It derives from FileTreeNode
class. MySimpleFileSystem
creates the file system tree in its own constructor. It sets its own name to be "Root" and marks itself as a folder. Then it adds folders "Documents" and "Pictures" to it and a file "MySystemFile". Finally it populates each one of its subfolders:
public MySimpleFileSystem()
{
this.Name = "Root";
this.FileOrFolder = false;
FileTreeNode docFolder = this.AddChild(false, "Documents");
FileTreeNode pictureFolder = this.AddChild(false, "Pictures");
FileTreeNode systemFile = this.AddChild(true, "MySystemFile");
docFolder.AddChild(true, "Document1");
docFolder.AddChild(true, "Document2");
pictureFolder.AddChild(true, "Picture1");
pictureFolder.AddChild(true, "Picture2");
}
Visuals for this sample are defined within MainWindow.xaml file of the RecursiveDataTemplateSample
main project. We define the tree view model as the Window
's resource within that file:
<!---->
<testViewModels:MySimpleFileSystem x:Key="TheFileSystem" />
The recursive data template is defined in the same file and is called TheFileRepresentationDataTemplate:
<DataTemplate x:Key="TheFileTreeRepresentationDataTemplate"
DataType="viewModels:FileTreeNode">
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Left">
<!-- Expander toggle button, it only visible for the items that have children -->
<customControl:MyToggleButton Style="{StaticResource ToggleButtonExpanderStyle}"
IsChecked="{Binding Path=IsExpanded, Mode=TwoWay}"
Visibility="{Binding Path=HasChildren,
Converter={StaticResource TheBooleanToVisibilityConverter}}" />
<!--name of the file or folder-->
<TextBlock Margin="10,0,0,0"
Text="{Binding Name}" />
</StackPanel>
<!-- This is the ItemsControl that represents the children of the current node
It is only visible if the current node is expanded-->
<!-- Note that ItemTemplate of the ItemsControl is set to the
DataTemplate we are currently in: TheFileTreeRepresentation.
This is a recursive template.
-->
<ItemsControl Margin="30,0,0,0"
ItemsSource="{Binding Children}"
ItemTemplate="{DynamicResource TheFileTreeRepresentationDataTemplate}"
Visibility="{Binding Path=IsExpanded, Converter={StaticResource TheBooleanToVisibilityConverter}}" />
</StackPanel>
</DataTemplate>
Note that the ItemsControl
for the Children
of the current node is referring to the same data template - TheFileTreeRepresentationDataTemplate: ItemTemplate="{DynamicResource TheFileTreeRepresentationDataTemplate}"
. We are forced to use DynamicResource
here since the DataTemplate
references itself.
Finally we "marry" the DataTemplate
and the View Model by using the ContentControl
:
<ContentControl ContentTemplate="{StaticResource TheFileTreeRepresentationDataTemplate}"
Content="{StaticResource TheFileSystem}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="20,20,0,0"/>
Note, that the same pattern of recursive DataTemplate
s can be used for much more complex controls than the one shown in the sample. E.g. I used it to display a full organization chart for a large organization.
Conclusion
In this article we presented the patterns that can be used for creating visuals that mimic behaviors defined by non-visual objects - Views mimicking the behavior of View Models. Next article will talk about more complex (architectural) patterns.