[Updated – Fixed code samples]
What do you do when you have a list box or data grid that has a collection of items to display - but some items need to be presented differently than others? For example, I may have a collection of company staff members to display in a single ListBox, but the managerial staff need to have extra information displayed (they always do!), and each level is styled slightly differently like this for example:
In this post I'll describe a technique I have used successfully before - I'll stick with the list of staff example above to illustrate the solution, but the class types are going to be a little contrived for the sake of simplicity.
The Data
Lets define a StaffMember class like this:
1: public enum StaffRoleType { Manager, MiddleManager, LowerManager, Pleb, Contractor }
2:
3: public enum CoffeeType { Espresso, Cappuccino, HotChocolate, Other }
4:
5: public class StaffMemeber
6: {
7: public string Title { get; set; }
8: public string FirstName { get; set; }
9: public string LastName { get; set; }
10: public StaffRoleType StaffRole { get; set; }
11: public CoffeeType CoffeeType { get; set; }
12: public Color FavouriteColor { get; set; }
13: public string CarType { get; set; }
14: }
The StaffRoleType will be used to differentiate the managers from the plebs so we can display the really important information for managers like what kind of coffee they drink and what their favourite color is. If this was real code we might have a different class for each role type, and they would all implement the INotifyPropertyChanged interface. But that class will do for demonstration purposes.
Different Data = Different Data Templates
A nice way to solve this problem would be if we could somehow use different data templates to display each type of staff- but do this inside the existing controls like a ListBox. Ideally we could use Blend to design these different data types too. Of course, the ListBox doesn't let us define more than one data template for the ListBoxItem; but rather than try and rewrite the ListBox, we can achieve our goals with a couple of simple additions to the ContentControl:
1: using System.Collections.ObjectModel;
2: using System.Linq;
3: using System.Windows;
4: using System.Windows.Controls;
5: public class SelectableContentControl : ContentControl
6: {
7: public static readonly DependencyProperty TemplateNameProperty = DependencyProperty.Register(
8: "TemplateName",
9: typeof(string),
10: typeof(SelectableContentControl),
11: new PropertyMetadata(string.Empty, TemplateNameChanged));
12:
13: private readonly ObservableCollection<DataTemplate> templateCollection = new ObservableCollection<DataTemplate>();
14:
15: /// <summary>
16: /// Gets the collection of templates
17: /// </summary>
18: public ObservableCollection<DataTemplate> Templates
19: {
20: get
21: {
22: return this.templateCollection;
23: }
24: }
25:
26: /// <summary>
27: /// Gets or sets the name of the template to use
28: /// </summary>
29: public string TemplateName
30: {
31: get
32: {
33: return (string)GetValue(TemplateNameProperty);
34: }
35:
36: set
37: {
38: SetValue(TemplateNameProperty, value);
39: }
40: }
41:
42: /// <summary>
43: /// Select the appropriate DataTemplate when the Content changes.
44: /// </summary>
45: /// <param name="oldContent">The old Content value.</param>
46: /// <param name="newContent">The new Content value.</param>
47: protected override void OnContentChanged(object oldContent, object newContent)
48: {
49: base.OnContentChanged(oldContent, newContent);
50: this.SelectTemplate();
51: }
52:
53: private static void TemplateNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
54: {
55: ((SelectableContentControl)d).SelectTemplate();
56: }
57:
58: private void SelectTemplate()
59: {
60: if (!string.IsNullOrEmpty(this.TemplateName))
61: {
62: DataTemplate namedTemplate =
63: this.Templates.FirstOrDefault(
64: t => t.GetValue(FrameworkElement.NameProperty).Equals(this.TemplateName));
65: if (null != namedTemplate)
66: {
67: this.ContentTemplate = namedTemplate;
68: return;
69: }
70: }
71:
72: // default to the first template
73: if (this.Templates.Count > 0)
74: {
75: this.ContentTemplate = this.Templates[0];
76: }
77: }
78: }
The new control has two key properties:
- Templates - an ObservableCollection of DataTemplate
- TemplateName - a string representing the name of the template to choose.
This is what the XAML for a ListBox may look like:
1: <ListBox ItemsSource="{Binding StaffList}" >
2: <ListBox.ItemTemplate>
3: <ss:SelectableContentControl TemplateName="{Binding StaffRole}" Content="{Binding}">
4: <ss:SelectableContentControl.Templates>
5:
6: <!-- default for unmatched StaffRoleType values -->
7: <DataTemplate>
8: <local:PlebianUC />
9: </DataTemplate>
10:
11: <!-- Contractor template -->
12: <DataTemplate x:Name="Contractor">
13: <local:ContractorUC />
14: </DataTemplate>
15:
16: <!-- Contractor template -->
17: <DataTemplate x:Name="MiddleManager">
18: <local:MiddleManagerUC />
19: </DataTemplate>
20:
21: <!-- Contractor template -->
22: <DataTemplate x:Name="Manager">
23: <local:ManagerUC />
24: </DataTemplate>
25: </ss:SelectableContentControl.Templates>
26: </ss:SelectableContentControl>
27: </ListBox.ItemTemplate>
28: </ListBox>
So what's going on here?
The ListBox has it's ItemsSource bound to a property on the DataContext called StaffList, which contains a collection of StaffMember instances. The ListBox.ItemTemplate contains a single SelectableContentControl with its TemplateName property set to "StaffRole" and it's Content bound to the DataContext of the ListBoxItem. At run time when a DataTemplate is being created for each item in the listbox and the item is databound to it, the SelectableContentControl will read the StaffRole property and find a template with the same name as the StaffRole value. The first DataTemplate in the Templates collection will be used for any data items that have a StaffRole value that doesn't match a Template name. We don't even need a value converter in this example, since the StaffRole property will convert to a string without one.
In order to make this technique friendly to Blend you need to create a UserControl for each of the DataTemplates so that the content can be styled in Blend. If you are more comfortable writing XAML than using Blend then you can skip the UserControls and put the content directly in the DataTemplates. Either way, you will still need to specify the DataTemplate collection by hand in XAML since Blend doesn't provide and easy way to edit a collection of DataTemplate instances (at least that I know of)
Feel free to copy the code above and use it in your own projects. Let me know if you find it useful.