Introduction
As demonstrated in the article Simplifying the WPF TreeView by Using the ViewModel Pattern by Josh Smith, inserting a Presentation Model layer between your domain-model collection contents and template-generated WPF classes can make using WPF easier. Josh showed how to avoid writing procedural code against template-generated items such as instances of TreeViewItem
. Attempting to do so causes headaches, making WPF seem more like a burden than a boon. WPF works fine when you're setting properties of template-generated items in XAML, but that ease of use is lost if you try directly doing so in the code-behind file. So save yourself the agony by taking an indirect approach.
In order to access and modify properties of template-generated items from the code-behind file, you first need to bind them to whatever is in the control's ItemsSource
collection. Many of the properties you want to bind to (e.g., IsSelected
and IsExpanded
) will not be present on your domain objects, nor do you want to add them there since they are GUI-related rather than domain-related. The solution, as Josh presented in his article, is to create an additional Presentation Model layer consisting of objects that sit between your domain-model collection contents and the template-generated instances of WPF classes.
During the course of your adventures in WPF, it's likely you'll find other properties that are appropriate to the Presentation Model layer. These are properties that are clearly more GUI-related than domain-related, but would be quite convenient to have available as data-binding sources (without otherwise relying on value-converters). This article and the accompanying code library expand on this idea. The library makes it easy to create a new Presentation Model, of the sort just described, with minimal code. The ItemPresentationModel
class provides a presentation-tailored representation of a domain-model type. And the HierarchicalPresentationModel
class helps you by propagating modifications from a domain layer collection or tree structure, into a hierarchical Presentation Model.
Terminology
Two very similar design patterns have been described, each of which uses slightly different terminology. The original Presentation Model pattern by Martin Fowler uses the word "Presentation" to refer to the UI, whereas the latter WPF-specific Model-View-ViewModel by John Gossman uses "View". I prefer the original, and will use it throughout this article.
As for domain-specific Presentation Model classes, there is no universally accepted naming convention. The example in Fowler's work uses the combination of "Pmod" + domain class name, as in "PmodAlbum". Josh Smith's article started out using domain class name + "Presenter", but a reader pointed out that "it is somewhat of a misnomer". He therefore switched over to domain class name + "ViewModel", which follows John Gossman's terminology. For a while, I suffixed "View" onto Presentation Model classes, but this word's prominent use to refer to the UI layer has since led me to suffixing "PM" instead.
Presentation Model in WPF
This article isn't about the Presentation Model pattern per se. Fowler writes that "Presentation Model is not a GUI friendly facade to a specific domain object". However, the form of Presentation Model I use provides a data-binding friendly facade for each domain object to its corresponding template-generated item (e.g., TreeViewItem
). This variation of the pattern is what I call "Item Presentation Model", and has a corresponding base class by the same name in the library attached to this article.
This ItemPresentationModel
class wraps a domain object (the "Item
") with a Presentation Model. The domain object is publicly accessible via the class's "DomainItem
" property. This means you don't have to bother writing repetitive bridging properties of the form:
public string RegionName {
get { return _region.RegionName; }
}
where no value is being added, since you have direct access to the wrapped domain item.
Flow of Changes
Displaying current data on all screens is generally required of an application. Neglecting this, as I've seen in other WPF-related articles on the web, can lead to problems as the codebase grows. For example, if Domain Model object dm1
has two corresponding Presentation Model objects pm1
and pm2
, and if pm1
modifies dm1
, then pm2
will contain stale data. The same problem can occur, for both pm1
and pm2
, if some logic within the Domain Model modifies dm1
. The solution is to make the Domain Model observable, so changes automatically flow to the Presentation Model.
Demo 1
This example demonstrates how easy it is to create a subclass of ItemPresentationModel
. The code for the Demo1ItemPM
Presentation Model class is as follows:
public class Demo1ItemPM: ItemPresentationModel<SampleItem> {
public Demo1ItemPM ( SampleItem domainItem )
: base( domainItem ) {
UnsavedName = domainItem.Name;
}
private string unsavedName;
public string UnsavedName {
get { return unsavedName; }
set { unsavedName = value; }
}
}
Deriving from ItemPresentationModel
gives you the DomainItem
property mentioned above. You also get the IsSelected
property from Josh's article, which gets data-bound in the XAML file like this:
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>
UnsavedName
is exactly the sort of property you would add to the Presentation Model, since it's not something that belongs in the domain-model, yet its use with data-binding makes things easier. Here's the text box for editing an item's name:
<TextBox Text="{Binding Path=UnsavedName, UpdateSourceTrigger=PropertyChanged}"
Margin="-3,0,0,0"
FocusManager.FocusedElement="{Binding RelativeSource={RelativeSource Self}}"/>
Thanks to the binding, the editableContentControl_ButtonClick()
method can respond to saveButton
and cancelButton
clicks like this:
else if ( "saveButton".Equals( clickedButton.Name ) )
targetItem.DomainItem.Name = targetItem.UnsavedName;
else if ( "cancelButton".Equals( clickedButton.Name ) )
targetItem.UnsavedName = targetItem.DomainItem.Name;
If the saveButton
is clicked, then the domain object's name is updated (after which you would persist the change to the database). If the cancelButton
is clicked, then the UnsavedName
value is reset.
Run the demo, and look at the source code of Demo1.xaml and Demo1.xaml.cs, for further details.
Demo 2
Now we move on to representing hierarchical domain-model structures, with a Presentation Model that interfaces with template-generated TreeViewItem
s. For this example, we have a SampleTreeNode
domain class and its corresponding SampleTreeNodePM
Presentation Model. The superclass of SampleTreeNodePM
is my library's HierarchicalPresentationModel
base class. It wraps individual items of the domain tree-structure, and has a recursive SubItems
property that is used in the HierarchicalDataTemplate
:
<HierarchicalDataTemplate DataType="{x:Type uimodel:SampleTreeNodePM}"
ItemsSource="{Binding Path=SubItems}">
<TextBlock Text="{Binding Path=DomainItem.Name}"/>
</HierarchicalDataTemplate>
When you instantiate a root object of this wrapper type (see code snippet below), it populates its tree with items from the domain tree-structure, first wrapping each one. You then bind this root object's SubItems
property to TreeView.ItemSource
(the following line in the snippet). But if the domain-model thereafter changes, how can the Presentation Model be updated to reflect those changes? For example, if the user removes a SampleTreeNode
object's sub-item, how can that change be propagated to its corresponding SampleTreeNodePM
? Luckily, my HierarchicalPresentationModel
class takes care of that synchronization for you. After being created, each HierarchicalPresentationModel
object listens to its corresponding domain collection's change events, adding/removing sub-items to/from itself as to stay in sync.
public partial class Demo2 : UserControl {
private SampleTreeNodePM rootWrapper;
public Demo2() {
InitializeComponent();
rootWrapper = new SampleTreeNodePM();
sampleTreeView.ItemsSource = rootWrapper.SubItems;
}
So a HierarchicalPresentationModel
object acts as a sort of custom collection-view (analogous to WPF's CollectionView
) over an original domain-model sub-items collection. The binding from a domain collection to its corresponding collection-view is one-way; that is, changes to the domain collection affect the collection-view but the reverse is not true. The binding from a collection-view to the ItemsSource
property of its corresponding TreeView
(if it's a root) or TreeViewItem
(for all other nodes) is also one-way. In order to programmatically add/remove items, the client code modifies the domain collections; those changes are then propagated in the desired manner all the way to the TreeView
or TreeViewItem
.
Here's the code for the SampleTreeNodePM
Presentation Model class:
public sealed class SampleTreeNodePM
: HierarchicalPresentationModel<SampleTreeNode, SampleTreeNodePM> {
public SampleTreeNodePM() : this( SampleTreeNode.Root ) { }
private SampleTreeNodePM( SampleTreeNode domainItem )
: base( domainItem, domainItem.SubNodes, true, true, true ) { }
protected override SampleTreeNodePM CreateInstance( SampleTreeNode domainItem ) {
return new SampleTreeNodePM( domainItem );
}
protected override HierarchicalPlacementArgs<SampleTreeNodePM>
DesiredPosition( SampleTreeNode itemToAdd ) {
return new HierarchicalPlacementArgs<SampleTreeNodePM>(
OutliningCommands.NewChild, this, false );
}
}
This subclass consists of four parts:
- A
public
constructor for creating the tree root.
- A
private
constructor that'll be used to create all other nodes.
- An override of the
CreateInstance()
method that returns an instance created via the latter constructor.
- An override of the
DesiredPosition()
method which, in this case, uses the simplest of implementations, i.e. add the new item to the sub-items collection. The third argument to the return value's constructor is false
, indicating the resulting collection is not sorted.
As for the base constructor call in #2, you may be wondering what are the three boolean values being passed through:
- Specifies whether the Presentation Model subitems' indices are the same as in their corresponding source collection. We are mirroring the domain hierarchy exactly, so in this case the value is
true
.
- Specifies whether subitems also get deleted once their parent is deleted, which is also
true
in this case.
- Information useful for debugging
DesiredPosition()
implementations is written to the console when this value is true
.
HierarchicalPresentationModel
derives from ItemPresentationModel
, so deriving from it gives you the same properties mentioned for Demo 1, as well as Josh's IsExpanded
property which gets data-bound like this:
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
Use SampleTreeNodePM
as a template in your own projects. And check out the rest of the Demo2 code-behind file to see how to add, move, and remove nodes from the tree structure.
Demo 3
The HierarchicalPresentationModel
class has another use, which is to provide a hierarchical representation of a flat collection. That is, given the input of a flat collection from the domain model, this class can help you produce a hierarchical Presentation Model ready to be data-bound to a TreeView
. There are a couple of different ways to do this in WPF, but this is the only way I know to take a flat collection and display some items as children of others.
The original purpose of the HierarchicalPresentationModel
code was, in fact, to display a flat collection as a hierarchy. This was my first use of the Presentation Model pattern, before I even knew what the Presentation Model pattern was! Adding the data-bound IsSelected
and IsExpanded
properties was an advantage I realized only later, after coming across Josh Smith's Simplifying the WPF TreeView... article.
The demo here uses the same domain data as Demo 1. It may be interesting to compare Demo 1 and 3 since they display the same data, only using a different Presentation Model.
The source code for the Demo3ItemPM
class is as follows:
public sealed class Demo3ItemPM
: HierarchicalPresentationModel<SampleItem,Demo3ItemPM> {
public Demo3ItemPM() : base( new Representative("Root"),
App.SampleListContainer.SampleList, false, false, true ) { }
private Demo3ItemPM( SampleItem domainItem )
: base( domainItem, null, false, false, true ) { }
protected override Demo3ItemPM CreateInstance( SampleItem domainItem ) {
return new Demo3ItemPM( domainItem );
}
protected override HierarchicalPlacementArgs<Demo3ItemPM> DesiredPosition(
SampleItem itemToAdd ) { ... }
public Representative RepresentedBy {
get {
Represented represented = DomainItem as Represented;
if ( represented != null ) return represented.RepresentedBy;
else return null;
}
set {
Represented represented = DomainItem as Represented;
if ( represented != null ) represented.RepresentedBy = value;
}
}
}
This subclass consists of the same four parts as the one in Demo 2. You will notice a difference in the private
constructor, however. In this case, a null
value is passed in for the base constructor's subItemsSource
parameter. This indicates that no sub-items will be recursively created by the base constructor, which is what we want in this case since the DesiredPosition()
method will decide the subtree's arrangement. Following that, you'll notice the first two boolean values are also different from the previous demo: subitem indices do not correspond in this case, and deletes do not cascade.
The body of the DesiredPosition()
method is omitted because it's a bit long. If you have a similar scenario, though, then you should take a look at it. Apart from the specific types mentioned (Represented
and Representative
), the logic may be applicable to your situation. After that comes the RepresentedBy
bridging property, which I added since it made data-binding to the representedByComboBox
so much easier:
<ComboBox Name="representedByComboBox" SelectedItem="{Binding RepresentedBy}"
DisplayMemberPath="Name" Padding="5,2" Margin="0,0,10,10"/>
The problem was that two different subclasses are involved — one that has a RepresentedBy
property, and another that doesn't. This bridging property prevents a binding error by just returning a null
value when the RepresentedBy
property isn't present.