More about heterogeneous MulticolumnTreeViews
I wrote before about heterogeneous hierarchies in the WPF Elements MulticolumnTreeView (aka TreeListView). In that example, I assumed that, although the hierarchy was made up of different types of objects, the set of columns made sense across all the types, or specifically that the columns could be bound in the same way across all the types — in that example, that each type had an EnglishName and a LatinName property. But what if that’s not the case?
For example, consider a simple contact management application, with just two types, Company and Person. For our columns, we might have Name, Email Address and Notes. This gives us a couple of problems. First, Email Address doesn’t make sense for companies. Second, while a company has a Name property, a person has a FirstName and a LastName property, so our Name column is going to need to have different bindings.
The solution for columns that don’t have a consistent binding is to set the column’s CellTemplateSelector rather than its CellTemplate or DisplayMemberBinding. The CellTemplateSelector is a DataTemplateSelector, which means we can dispatch off to different templates as required. In our case, we need two selectors. The Email Address selector selects an empty template for a Company object, and a simple binding for a Person object. The Name selector selects a simple Name binding for a Company object, and a more complex data template for Person objects. Let’s get those templates out of the way first:
<DataTemplate x:Key="EmptyTemplate"> <TextBlock /> </DataTemplate> <DataTemplate x:Key="DefaultNameTemplate"> <TextBlock Text="{Binding Name}" /> </DataTemplate> <DataTemplate x:Key="PersonNameTemplate"> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding FirstName}" /> <TextBlock Text=" " /> <TextBlock Text="{Binding LastName}" /> </StackPanel> </DataTemplate> <DataTemplate x:Key="DefaultEmailAddressTemplate"> <TextBlock Text="{Binding EmailAddress}" /> </DataTemplate>
What about our selectors? Looking at them, the logic of both selectors is exactly the same — select based on type of object. Only the choice of templates is different. So we can make the choice of templates part of the selector’s instance data. In our XAML we will then create two selector instances, but of the same type. Here’s a first pass at the selector design:
public class EntityTypeTemplateSelector : DataTemplateSelector { public DataTemplate DefaultTemplate { get; set; } public DataTemplate PersonTemplate { get; set; } public DataTemplate CompanyTemplate { get; set; } public override DataTemplate SelectTemplate(object item, DependencyObject container) { if (item is Person) { return PersonTemplate ?? DefaultTemplate; } if (item is Company) { return CompanyTemplate ?? DefaultTemplate; } return DefaultTemplate; } }
Notice the fallback to DefaultTemplate if no specific template is defined for a type. In our two-type model, this is almost superfluous, but if we started adding more types to the hierarchy, and found that most of them had a Name property, it would save us having to mention DefaultNameTemplate against every type.
(You may find it objectionable that this selector is specifically coupled to the Company/Person model. Can’t we parameterise it by type as well, so that we only ever have to write the selector once, and set up the type-template mapping in the instance data? Yes we can, but it makes the XAML a bit more verbose. We’ll look at this in a moment.)
With this selector defined, we can now create instances in our XAML:
<local:EntityTypeTemplateSelector x:Key="NameSelector" PersonTemplate="{StaticResource PersonNameTemplate}" DefaultTemplate="{StaticResource DefaultNameTemplate}" /> <local:EntityTypeTemplateSelector x:Key="EmailAddressSelector" CompanyTemplate="{StaticResource EmptyTemplate}" DefaultTemplate="{StaticResource DefaultEmailAddressTemplate}" />
All that remains is to tell our MulticolumnTreeView to use these selectors instead of static templates or bindings:
<ms:MulticolumnTreeView ItemTemplate="{StaticResource People}"> <ms:MulticolumnTreeView.Columns> <GridViewColumn Header="Name" CellTemplateSelector="{StaticResource NameSelector}" /> <GridViewColumn Header="Email Address" CellTemplateSelector="{StaticResource EmailAddressSelector}" /> <GridViewColumn Header="Notes" DisplayMemberBinding="{Binding Notes}" /> </ms:MulticolumnTreeView.Columns> </ms:MulticolumnTreeView>
Our heterogeneous hierarchy is now displaying heterogeneous properties.
I want to come briefly back to the idea of a more reusable selector. This is pretty easy to write: the only trick is to apply ContentPropertyAttribute so that the type-template mappings can appear as direct children of the selector element in XAML. (A real implementation would do some error checking as well, specifically to handle null references.)
[ContentProperty("Templates")] public class EntityTypeTemplateSelector : DataTemplateSelector { private List<TypeTemplate> _templates = new List<TypeTemplate>(); public List<TypeTemplate> Templates { get { return _templates; } } public DataTemplate DefaultTemplate { get; set; } public override DataTemplate SelectTemplate(object item, DependencyObject container) { foreach (TypeTemplate tt in Templates) { if (tt.Type.IsAssignableFrom(item.GetType())) { return tt.Template; } } return DefaultTemplate; } } public class TypeTemplate { public Type Type { get; set; } public DataTemplate Template { get; set; } }
Using it, however, is a little bit fiddlier:
<local:EntityTypeTemplateSelector x:Key="NameSelector" DefaultTemplate="{StaticResource DefaultNameTemplate}"> <local:TypeTemplate Type="cm:Person" Template="{StaticResource PersonNameTemplate}" /> </local:EntityTypeTemplateSelector> <local:EntityTypeTemplateSelector x:Key="EmailAddressSelector" DefaultTemplate="{StaticResource DefaultEmailAddressTemplate}"> <local:TypeTemplate Type="cm:Company" Template="{StaticResource EmptyTemplate}" /> </local:EntityTypeTemplateSelector>
There’s a bit more noise here, and automated tools like Visual Studio Intellisense won’t be able to support the way they could on the version with a hardwired template set. (This might be a consideration depending on your team and workflow. Developers may not mind entering type names. Graphic designers might prefer you gave them a fixed set of “blanks” to fill in.) It’s undoubtedly more reusable, though, and won’t need to be updated whenever the model changes. So which is the right option depends on your application and your project structure.
Leave a Reply
Categories
BrainDump (1)
Community Code (4)
Events (16)
F# (14)
General (53)
Lab Samples (2)
LightSpeed (268)
MegaPack (8)
News (71)
NHibernate Designer (26)
Nightly news (52)
Phone Elements (24)
Products (87)
Projects (5)
Screencast (6)
SharePoint (3)
Silverlight (14)
Silverlight Elements (66)
SimpleDB Management Tools (20)
Visual Studio (9)
VS File Explorer (7)
Web Workbench (39)
WPF (44)
WPF Diagrams (57)
WPF Elements (110)
WPF Property Grid (32)


Tagged as

Posted by Ivan Towlson on 21 July 2008 


