WPF Based Dynamic DataTemplateSelector






4.94/5 (7 votes)
Demonstrates how to make use of a completely WPF based DataTemplateSelector relying on no code-behind.
Introduction
This article will cover a method for defining different DataTemplateSelector
options within your WPF code and allowing a very small amount
of backend code to take care of all the selections for you. This allows you to make use of this code from within an MVVM structure without any issues,
and while making the front end extremely easy to manage without complicating the back end code.
Background
Recently I was working on a project in which I wanted to implement a list where the contents could be one of a number of different types.
Obviously, I wouldn't know what the contents of this list were beforehand (and as such I couldn't just statically create the content) so I started looking at
ways to bend the ItemsTemplate
property to do my nefarious bidding. One of the first things I noticed was ItemsTemplateSelector
which sounded promising,
but upon reading up a bit on it I found that it encouraged the use of code behind to select the data templates.
The way you are expected to do this, for those of you who are curious and a bit too lazy to go looking elsewhere for an example, is documented below.
DataTemplate Usage
WPF Code
<local:TemplateSelector x:Key="MyTemplateSelector" />
<Style x:Key="MyListStyle" TargetType="ListView">
<Setter Property="ItemTemplateSelector" Value="{StaticResource MyTemplateSelector}"/>
</Style>
Code Behind
public class TemplateSelector : DataTemplateSelector
{
DataTemplate stringTemplate; //This would need to be initialized
//You override this function to select your data template based in the given item
public override System.Windows.DataTemplate SelectTemplate(object item,
System.Windows.DependencyObject container)
{
if(item is string)
return stringTemplate;
else
return base.SelectTemplate(item, container);
}
}
Obviously, this is an extremely simplified version of what you would end up doing, but it should give you an idea of what is going on.
Since I was using Caliburn.Micro
(something I would highly recommend you start using if you aren't already)
I was a bit apprehensive of using anything where my back end code would need to grab data templates from my front end (UI).
That being said, I wasn't going to be too stubborn, but I am also an inherently lazy person, and I don't like having to do extra work to get things working, something it appeared
I would need to do to get a hold of the templates I was trying to supply. The issue is that the DataTemplateSelector
doesn't actually have access
to the ResourceDictionary
used by your user interface, and while it is probably possible to find it through the container parameter, I really didn't feel like doing the work.
So, out comes Google and I start searching for alternative solutions, of which there were surprisingly few. The general response was how to use DataTemplateSelector
s
and there were a few decent articles on how you could move the selection code out of your apparently numerous selector classes. I wouldn't know,
since I was just starting to write my first one, but I took their word for it anyway and kept looking.
After a few minutes I got bored of this and decided to try my hand at writing my own. One of my main aims was to be able to add new template selectors from within my WPF code rather than having to resort to writing code behind. One of the great features of WPF is the ability to register attached properties on objects you do not have the source to. This allows you to add functionality that wasn't there to start with and extend existing classes in creative new ways.
The result was a class which allowed me to add data templates, along with selection criteria, right from within my WPF code using an attached property.
DynamicDataTemplate Implementation
/// <summary>
/// Provides a means to specify DataTemplates to be selected from within WPF code
/// </summary>
public class DynamicTemplateSelector : DataTemplateSelector
{
/// <summary>
/// Generic attached property specifying <see cref="Template"/>s
/// used by the <see cref="DynamicTemplateSelector"/>
/// </summary>
/// <remarks>
/// This attached property will allow you to set the templates you wish to be available whenever
/// a control's TemplateSelector is set to an instance of <see cref="DynamicTemplateSelector"/>
/// </remarks>
public static readonly DependencyProperty TemplatesProperty =
DependencyProperty.RegisterAttached("Templates", typeof(TemplateCollection), typeof(DataTemplateSelector),
new FrameworkPropertyMetadata(new TemplateCollection(), FrameworkPropertyMetadataOptions.Inherits));
/// <summary>
/// Gets the value of the <paramref name="element"/>'s attached <see cref="TemplatesProperty"/>
/// </summary>
/// <param name="element">The <see cref="UIElement"/> who's attached template's property you wish to retrieve</param>
/// <returns>The templates used by the givem <paramref name="element"/>
/// when using the <see cref="DynamicTemplateSelector"/></returns>
public static TemplateCollection GetTemplates(UIElement element)
{
return (TemplateCollection)element.GetValue(TemplatesProperty);
}
/// <summary>
/// Sets the value of the <paramref name="element"/>'s attached <see cref="TemplatesProperty"/>
/// </summary>
/// <param name="element">The element to set the property on</param>
/// <param name="collection">The collection of <see cref="Template"/>s to apply to this element</param>
public static void SetTemplates(UIElement element, TemplateCollection collection)
{
element.SetValue(TemplatesProperty, collection);
}
/// <summary>
/// Overriden base method to allow the selection of the correct DataTemplate
/// </summary>
/// <param name="item">The item for which the template should be retrieved</param>
/// <param name="container">The object containing the current item</param>
/// <returns>The <see cref="DataTemplate"/> to use when rendering the <paramref name="item"/></returns>
public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
{
//This should ensure that the item we are getting is in fact capable of holding our property
//before we attempt to retrieve it.
if (!(container is UIElement))
return base.SelectTemplate(item, container);
//First, we gather all the templates associated with the current control through our dependency property
TemplateCollection templates = GetTemplates(container as UIElement);
if(templates == null || templates.Count == 0)
base.SelectTemplate(item, container);
//Then we go through them checking if any of them match our criteria
foreach (var template in templates)
//In this case, we are checking whether the type of the item
//is the same as the type supported by our DataTemplate
if (template.Value.IsInstanceOfType(item))
//And if it is, then we return that DataTemplate
return template.DataTemplate;
//If all else fails, then we go back to using the default DataTemplate
return base.SelectTemplate(item, container);
}
}
/// <summary>
/// Holds a collection of <see cref="Template"/> items
/// for application as a control's DataTemplate.
/// </summary>
public class TemplateCollection : List<Template>
{
}
/// <summary>
/// Provides a link between a value and a <see cref="DataTemplate"/>
/// for the <see cref="DynamicTemplateSelector"/>
/// </summary>
/// <remarks>
/// In this case, our value is a <see cref="System.Type"/> which we are attempting to match
/// to a <see cref="DataTemplate"/>
/// </remarks>
public class Template : DependencyObject
{
/// <summary>
/// Provides the value used to match this <see cref="DataTemplate"/> to an item
/// </summary>
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(Type), typeof(Template));
/// <summary>
/// Provides the <see cref="DataTemplate"/> used to render items matching the <see cref="Value"/>
/// </summary>
public static readonly DependencyProperty DataTemplateProperty =
DependencyProperty.Register("DataTemplate", typeof(DataTemplate), typeof(Template));
/// <summary>
/// Gets or Sets the value used to match this <see cref="DataTemplate"/> to an item
/// </summary>
public Type Value
{ get { return (Type)GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } }
/// <summary>
/// Gets or Sets the <see cref="DataTemplate"/> used to render items matching the <see cref="Value"/>
/// </summary>
public DataTemplate DataTemplate
{ get { return (DataTemplate)GetValue(DataTemplateProperty); } set { SetValue(DataTemplateProperty, value); } }
}
Using the Code
<local:DynamicTemplateSelector x:Key="MyTemplateSelector" />
<DataTemplate x:Key="StringTemplate">
<TextBlock>
<Run Text="String: " />
<Run Text="{Binding}" />
</TextBlock>
</DataTemplate>
<DataTemplate x:Key="Int32Template">
<TextBlock>
<Run Text="Int32: " />
<Run Text="{Binding}" />
</TextBlock>
</DataTemplate>
<Style x:Key="MyListStyle" TargetType="ListView">
<Setter Property="ItemTemplateSelector" Value="{StaticResource MyTemplateSelector}"/>
<Setter Property="local:DynamicTemplateSelector.Templates">
<Setter.Value>
<local:Templates>
<local:Template Value={x:Type String} DataTemplate={StaticResource StringTemplate}/>
<local:Template Value={x:Type Int32} DataTemplate={StaticResource Int32Template}/>
</local:Templates>
</Setter.Value>
</Setter>
</Style>
It may not look it just yet, but when you start adding lots of different types, or templates, this quickly becomes a lot easier to manage; allowing you to add templates and types without needing to change any back end code.
Points of Interest
As I said, my main reason for writing this code was to maintain an MVVM structure within the project I was working on. I only recently started writing code
to match the MVVM paradigm and I can truly say that it has been one of the best things I've done since I moved away from Java a few years ago. If you haven't
yet, take a look at the truly exceptional Caliburn.Micro
and put the time into learning how to use it.
Other than that, I'd also recommend learning how to make use of attached Dependency Properties, they are useful for all manner of things from adding
watermarks to your text boxes (something I have only seen one person do properly so far in WPF) to allowing Caliburn.Micro
to hide and unhide a window using a
binding without having to write your own WindowManager
.
History
- 06/07/2012 - Initial release.
- 09/07/2012 - Fixed some issues with inheritance of DependencyProperty and added a collection to make adding values easier.