Welcome to MSDN Blogs Sign in | Join | Help

Easy DataTemplateSelectors in Silverlight

DataTemplates are the visual ToString()s of Silverlight and WPF.  They play an important role in the content model and are associated with the content properties of the controls you use everyday (i.e., Content and ContentTemplate, Header and HeaderTemplate, Items or ItemsSource and ItemTemplate, etc.).  Most of the time you know exactly what sort of data you need to display in your template.

There are occasions though – usually involving ItemsControl.ItemTemplate – where we want to display different types of data in different ways.  WPF answers this problem with DataTemplateSelector (and the ContentTemplateSelector, HeaderTemplateSelector, and ItemTemplateSelector properties).  Silverlight doesn’t yet support it, but we’ll look at how we can achieve the same effect after a little more motivation.

Modeling Hierarchies

Let’s start with a simple collection of business objects.  The following classes model Users and Groups.  Groups can contain Users and other Groups as Members.  Everything has an Alias and Users also have Titles.

using System;
using System.Collections.ObjectModel;
using System.Windows.Markup;

namespace Demo
{
    /// <summary>
    ///
Represents a member of a group.
   
/// </summary>
   
public abstract partial class GroupMember
   
{
        /// <summary>
        ///
Gets or sets the alias of the member.
       
/// </summary>
       
public string Alias { get; set; }

        /// <summary>
        ///
Initializes a new instance of the GroupMember class.
       
/// </summary>
       
internal GroupMember()
        {
        }
    }

    /// <summary>
    ///
Represents a group.
   
/// </summary>
   
[ContentProperty("Members")]
    public sealed partial class Group : GroupMember
   
{
        /// <summary>
        ///
Gets the members of the group.
       
/// </summary>
       
public Collection<GroupMember> Members { get; private set; }

        /// <summary>
        ///
Initializes a new instance of the Group class.
       
/// </summary>
       
public Group()
        {
            Members = new Collection<GroupMember>();
        }
    }

    /// <summary>
    ///
Represents a user.
   
/// </summary>
   
public sealed partial class User : GroupMember
   
{
        /// <summary>
        ///
Gets or sets the title of the user.
       
/// </summary>
       
public string Title { get; set; }

        /// <summary>
        ///
Initializes a new instance of the User class.
       
/// </summary>
       
public User()
        {
        }
    }
}

If you looked carefully you may have noticed the ContentPropertyAttribute from the System.Windows.Markup namespace that’s decorating the Group class.  This tells the XAML parser to add any content elements nested inside a Group to its Members collection.  Here’s a basic corporate directory structure declared in XAML.

<toolkit:ObjectCollection x:Key="Directory">
    <
demo:User Alias="alan" Title="President" />
    <
demo:User Alias="bob" Title="Vice President" />
    <
demo:Group Alias="Engineering">
        <
demo:Group Alias="ProgramManagement">
            <
demo:User Alias="cindy" Title="Program Manager II" />
            <
demo:User Alias="dave" Title="Program Manager" />
        </
demo:Group>
        <
demo:Group Alias="Development">
            <
demo:User Alias="edward" Title="Developer" />
            <
demo:User Alias="frank" Title="Senior Developer" />
            <
demo:User Alias="gary" Title="Developer II" />
        </
demo:Group>
        <
demo:Group Alias="Quality">
            <
demo:User Alias="henry" Title="Test II" />
            <
demo:User Alias="ines" Title="Test" />
            <
demo:User Alias="jack" Title="Test" />
        </
demo:Group>
        <
demo:User Alias="kurt" Title="Build Engineer" />
    </
demo:Group>
    <
demo:Group Alias="Sales">
        <
demo:User Alias="laura" Title="Sales" />
        <
demo:User Alias="matt" Title="Sales" />
        <
demo:User Alias="nancy" Title="Sales" />
    </
demo:Group>
    <
demo:Group Alias="Marketing">
        <
demo:User Alias="owen" Title="Product Manager" />
    </
demo:Group>
</
toolkit:ObjectCollection>

ObjectCollection is a nice little helper included in the Silverlight Toolkit that serves as a XAML friendly collection (remember that ArrayList and the rest of the non-generic collections weren’t included in Silverlight).  To display the hierarchical directory structure, we’ll use a TreeView and a HierarchicalDataTemplate.  The template shows the Alias (common to both Groups and Users) and then recursively applies the template to a Group’s Members.

<common:HierarchicalDataTemplate
 
x:Key="GroupMemberTemplate"
 
ItemsSource="{Binding Members}">
    <
TextBlock Text="{Binding Alias}" />
</
common:HierarchicalDataTemplate>

Wait – if this template is used for both Groups and Users, how are we able to bind to the Members property only on Group?  Silverlight happens to be goodly enough to ignore our attempt to bind to properties that don’t exist (like when we ask for a User’s Members).

The final step is to hook our directory data and template up to the TreeView.  It’s all pretty straightforward – here’s the XAML with everything in place.

<UserControl
 
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 
xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
 
xmlns:common="clr-namespace:System.Windows;assembly=System.Windows.Controls"
 
xmlns:toolkit="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit"
 
xmlns:demo="clr-namespace:Demo"
 
x:Class="Demo.MainPage">
    <
Grid Margin="5">
        <
Grid.RowDefinitions>
            <
RowDefinition Height="Auto" />
            <
RowDefinition Height="*" />
        </
Grid.RowDefinitions>
        <
Grid.Resources>
           
<!-- Directory Data -->
           
<toolkit:ObjectCollection x:Key="Directory">
                <
demo:User Alias="alan" Title="President" />
                <
demo:User Alias="bob" Title="Vice President" />
                <
demo:Group Alias="Engineering">
                    <
demo:Group Alias="ProgramManagement">
                        <
demo:User Alias="cindy" Title="Program Manager II" />
                        <
demo:User Alias="dave" Title="Program Manager" />
                    </
demo:Group>
                    <
demo:Group Alias="Development">
                        <
demo:User Alias="edward" Title="Developer" />
                        <
demo:User Alias="frank" Title="Senior Developer" />
                        <
demo:User Alias="gary" Title="Developer II" />
                    </
demo:Group>
                    <
demo:Group Alias="Quality">
                        <
demo:User Alias="henry" Title="Test II" />
                        <
demo:User Alias="ines" Title="Test" />
                        <
demo:User Alias="jack" Title="Test" />
                    </
demo:Group>
                    <
demo:User Alias="kurt" Title="Build Engineer" />
                </
demo:Group>
                <
demo:Group Alias="Sales">
                    <
demo:User Alias="laura" Title="Sales" />
                    <
demo:User Alias="matt" Title="Sales" />
                    <
demo:User Alias="nancy" Title="Sales" />
                </
demo:Group>
                <
demo:Group Alias="Marketing">
                    <
demo:User Alias="owen" Title="Product Manager" />
                </
demo:Group>
            </
toolkit:ObjectCollection>
            
           
<!-- Basic Template -->
           
<common:HierarchicalDataTemplate
             
x:Key="GroupMemberTemplate"
             
ItemsSource="{Binding Members}">
                <
TextBlock Text="{Binding Alias}" />
            </
common:HierarchicalDataTemplate>
        </
Grid.Resources>
        
       
<!-- Directory Tree -->
       
<TextBlock Grid.Row="0" Text="Directory: " />
        <
controls:TreeView
         
Grid.Row="1"
         
ItemsSource="{StaticResource Directory}"
         
ItemTemplate="{StaticResource GroupMemberTemplate}" />
    </
Grid>
</
UserControl>

There’s no code necessary, so we can run it to get something that looks like the below.

Basic example

Ah, nothing so beautiful as a TreeView…  but it would be nice if the Users looked a little different from the Groups (on Windows this would usually mean an icon with a single person or an icon with two people cozied up together).  If we only needed to customize a single attribute like the icon, we’d just write a new IValueConverter and be done (Shawn has more details under Option #3).  But what if we need to have different Backgrounds and Borders, or need to bind different sets of properties, or need to have entirely separate DataTemplates?  Writing half a dozen new classes that implement IValueConverter (or one class using String Oriented Programming via ConverterParameter) isn’t very appealing.

Variations on a Theme

To understand how we might use different DataTemplates for different items in an ItemsControl, let’s take a look at the API in WPF for DataTemplateSelector (via Visual Studio’s handy Go To Definition).

using System;
using System.Windows;

namespace System.Windows.Controls
{
    // Summary:
    // Provides a way to choose a System.Windows.DataTemplate based on the data
    // object and the data-bound element.
   
public class DataTemplateSelector
   
{
        // Summary:
        // Initializes a new instance of the System.Windows.Controls.DataTemplateSelector
        // class.
       
public DataTemplateSelector();

        // Summary:
        // When overridden in a derived class, returns a System.Windows.DataTemplate
        // based on custom logic.
        //
        // Parameters:
        // item:
        // The data object for which to select the template.
        //
        // container:
        // The data-bound object.
        //
        // Returns:
        // Returns a System.Windows.DataTemplate or null. The default value is null.
       
public virtual DataTemplate SelectTemplate(object item, DependencyObject container);
    }
}

There’s just a single method that takes an item (a Group or User in our case) and its container (the TreeViewItem associated with a Group or User) which it uses to return the appropriate DataTemplate.  We would just write a new GroupMemberTemplateSelector class that inherited from it, add the selection logic, and then use it with the fictitious TreeView.ItemTemplateSelector property.

We’ll use ContentControl to do the same thing on Silverlight.  The OnContentChanged method is a great substitute for SelectTemplate.  Add the GroupTemplate and UserTemplate dependency properties to round out the implementation.

using System;
using System.Windows;
using System.Windows.Controls;

namespace Demo
{
    /// <summary>
    ///
Provides a way to choose a DataTemplate for Groups and Users.
   
/// </summary>
   
public sealed partial class GroupMemberTemplateSelector : ContentControl
   
{
        #region public DataTemplate GroupTemplate
        /// <summary>
        ///
Gets or sets the DataTemplate used to visualize a Group.
       
/// </summary>
       
public DataTemplate GroupTemplate
        {
            get { return GetValue(GroupTemplateProperty) as DataTemplate; }
            set { SetValue(GroupTemplateProperty, value); }
        }

        /// <summary>
        ///
Identifies the GroupTemplate dependency property.
       
/// </summary>
       
public static readonly DependencyProperty GroupTemplateProperty =
            DependencyProperty.Register(
                "GroupTemplate",
                typeof(DataTemplate),
                typeof(GroupMemberTemplateSelector),
                new PropertyMetadata(null));
        #endregion public DataTemplate GroupTemplate

        #region public DataTemplate UserTemplate
        /// <summary>
        ///
Gets or sets the DataTemplate used to visualize a User.
       
/// </summary>
       
public DataTemplate UserTemplate
        {
            get { return GetValue(UserTemplateProperty) as DataTemplate; }
            set { SetValue(UserTemplateProperty, value); }
        }

        /// <summary>
        ///
Identifies the UserTemplate dependency property.
       
/// </summary>
       
public static readonly DependencyProperty UserTemplateProperty =
            DependencyProperty.Register(
                "UserTemplate",
                typeof(DataTemplate),
                typeof(GroupMemberTemplateSelector),
                new PropertyMetadata(null));
        #endregion public DataTemplate UserTemplate

        /// <summary>
        ///
Initializes a new instance of the GroupMemberTemplateSelector class.
       
/// </summary>
       
public GroupMemberTemplateSelector()
        {
        }

        /// <summary>
        ///
Select the appropriate DataTemplate when the Content changes.
       
/// </summary>
        /// <param name="oldContent">
The old Content value.</param>
        /// <param name="newContent">
The new Content value.</param>
       
protected override void OnContentChanged(object oldContent, object newContent)
        {
            base.OnContentChanged(oldContent, newContent);

            ContentTemplate =
                (newContent is Group) ? GroupTemplate :
                (newContent is User) ? UserTemplate :
                null;
        }
    }
}

When someone sets GroupMemberTemplateSelector.Content to a User or Group, it will automatically pick the correct DataTemplate to use for its ContentTemplate and display the item accordingly.  Note that while WPF just picks a template and hands it off to be dealt with in the content model, our approach actually expands the template and puts it in the visual tree.

Using the GroupMemberTemplateSelector is really easy.  Just stick it in your existing HierarchicalDataTemplate, bind the Content to the item we need to display, and provide new DataTemplates for GroupTemplate and UserTemplate.

<!-- Better Template -->
<common:HierarchicalDataTemplate
 
x:Key="GroupMemberTemplate"
 
ItemsSource="{Binding Members}">
    <
demo:GroupMemberTemplateSelector Content="{Binding}">
        <
demo:GroupMemberTemplateSelector.GroupTemplate>
            <
DataTemplate>
                <
Border
                 
Background="#FFFFFF"
                 
BorderBrush="#F92672"
                 
BorderThickness="1"
                 
CornerRadius="2"
                 
Padding="5 2">
                    <
TextBlock
                     
Text="{Binding Alias}"
                     
FontFamily="Tahoma"
                     
FontSize="12"
                     
FontWeight="Bold"
                     
Foreground="#354535"
                     
Margin="3" />
                </
Border>
            </
DataTemplate>
        </
demo:GroupMemberTemplateSelector.GroupTemplate>
        <
demo:GroupMemberTemplateSelector.UserTemplate>
            <
DataTemplate>
                <
Border
                 
BorderBrush="#F92672"
                 
BorderThickness="1 0 0 0"
                 
CornerRadius="5"
                 
Padding="4">
                    <
StackPanel>
                        <
TextBlock
                         
Text="{Binding Title}"
                         
FontSize="10"
                         
FontStyle="Italic"
                         
Foreground="Gray" />
                        <
TextBlock
                         
Text="{Binding Alias}"
                         
FontFamily="Verdana"
                         
FontSize="12"
                         
FontWeight="Bold" />
                    </
StackPanel>
                </
Border>
            </
DataTemplate>
        </
demo:GroupMemberTemplateSelector.UserTemplate>
    </
demo:GroupMemberTemplateSelector>
</
common:HierarchicalDataTemplate>

Pasting this XAML into the UserControl from above and running gives us a TreeView with a little more pop.

Example with DataTemplateSelector

It won’t win any design awards, but at least we’ve made a distinction between Users and Groups.

Loose ends…

Why didn’t we actually implement the DataTemplateSelector API as on WPF to get some of that subset compatibility that Silverlight’s so famous for?  It would require cracking open ItemsControl and friends so we could pass in both the items and containers, etc.  Proper integration of DataTemplateSelector and StyleSelector (same idea but for Styles) will need to wait for the platform to catch up.

It’s also worth pointing out that WPF has an even easier way to do this if you only need to vary your DataTemplates based on the type of item they’re displaying.  Just add a DataType property on your DataTemplate and the content model will automagically find it and apply it implicitly to items of that type.  You could use this to do crazy things like specify how you want all the Int32s in your application to be visually displayed by default.

The OnContentChanged method (or SelectTemplate on WPF) runs a little bit of code to figure out which DataTemplate to use for its item.  You could in theory use this as an extensibility point to do a little processing on the item before its template is applied (examples that come to mind involve setting up attached dependency properties that you want to bind in the template, etc.).  Obviously this introduces very subtle dependencies and you probably shouldn’t ever do it.  I just wanted to throw it out there because it’s interesting.

 

Next time we’ll explore ways to make this more general purpose.

Published Sunday, May 31, 2009 2:37 AM by tedglaza
Filed under: ,

Comments

# Easy DataTemplateSelectors in Silverlight | ASP NET Hosting

# Easy DataTemplateSelectors in Silverlight - Ted Glaza

Thank you for submitting this cool story - Trackback from DotNetShoutout

Sunday, May 31, 2009 12:38 PM by DotNetShoutout
Anonymous comments are disabled
 
Page view tracker