Building a template selector in Silverlight

There are several “missing” features in Silverlight that people keep bringing up, or keep trying to find solutions for. One of them is the template selector, or DataTemplateSelector class to be more specific.

The DataTemplateSelector has a single responsibility, which is the way it should be. It is responsible for returning a DataTemplate based an object. Generally, it is used in lists, where the data template can be selected based on the bound item, and thus give different templates to different kind of objects in the list.

In Silverlight, this is not possible out of the box, and instead requires us to create multiple UI controls, and hide and show them using data binding. This approach works, but it easily becomes heavy and complicated, which is why I want a template selector that works…

And I say “that works” as there are many solutions and options available out on the web, but none of them are that great, at least from the ones I have seen. Most of them use some form of IValueConverter, which looks fugly and quirky when written in the Xaml. And generally works poorly at design-time.

I wanted something cleaner… So I tried to solve it using attached properties instead of converters, making the whole thing a little cleaner from a Xaml point of view.

The goal is to put the DataTemplates in ResourceDictionary objects with some form of key. I then want to match that key to a value in the ViewModel, without having to implement some custom interface or something. And as the ResourceDictionary’s key is of type object, I can really use any kind of key I want, which makes the whole thing very flexible.

I also want the actual selection to work in 2 different ways. By default, I want it to walk up the control tree, looking at all resource dictionaries along the way, until a DataTemplate resource with the same key is found. But, I also want it to be possible to point out a specific ResourceDictionary and thus not spend CPU cycles on traversing the control tree.

So from this, I figured that I needed 2 attached properties, one called KeyPath and one called ResourceDictionary. With these two, I should be able to get the job done…

But before I go into code, I just want to mention that it is a bit complicated and hard to explain, so downloading the code and playing around with it might be a good idea… The code is available at the bottom of the post as usual… But a combination of looking at the code and reading the post is probably a good idea…

The attached properties are declared by a class called TemplateSelection, and are typed as string and ResourceDictionary…no surprises there…

For the KeyPath property, I declare a callback method to get notified when the value changes. In the callback, I do a somewhat ugly hack and verify that the control has an ItemTemplate property using reflection. I guess it would have been nicer to check for a property of type DataTemplate, but as far as I know, all the controls in Silverlight that use DataTemplate, do so using a property called ItemTemplate. If you need something else, just change it…

var prop = sender.GetType().GetProperty("ItemTemplate");
if (prop == null)
throw new Exception();

Next I get a little creative… First off, I create a new Guid, which will be used to identify this specific control. I then generate a new DataTemplate containing a custom control called TemplateSelectorDisplayControl, passing it the Guid. I then store the control in a Dictionary<Guid, WeakReference>, using the newly created Guid as the key, before setting the ItemTemplate property of the control to the the newly created template.

var id = Guid.NewGuid();
var dt = XamlReader.Load(string.Format(DataTemplateFormatString, id));
_controlDictionary.Add(id, new WeakReference(sender));

prop.SetValue(sender, dt, null);

The DataTemplate is created from a string as you can see using the XamlReader class. The string used to create the template looks like this

<DataTemplate 
xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"
xmlns:ts=\"clr-namespace:TemplateSelector;assembly=TemplateSelector\">
<ts:TemplateSelectorDisplayControl TemplateSelectorKey=\"{0}\" />
</DataTemplate>

As you can see, it defines a DataTemplate with a single control, which gets the Guid I created passed in through a property called TemplateSelectorKey… The TemplateSelectorKey property is of type string and not Guid as this was the easiest way to do it…

Ok, so that is part one…time to see what the TemplateSelectorDisplayControl does…

The TemplateSelectorDisplayControl is a custom control, that will make the “magic” happen. It has 2 dependency properties, one public called Content, which is used to bind its “content” to the ContentPresenter in the controls template, and one private called TemplateKey. The TemplateKey property is used to get the key from the ViewModel through binding. It obviously also has a string property called TemplateSelectorKey which is used to get the Guid from the TemplateSelection class as seen before…

That sounds awfully complicated, and it kind of is, but then again it isn’t…

In the ApplyTemplate method of the control, I get the KeyPath to use from the TemplateSelection class using the Guid. As the control I am working on has no knowledge of what parent control is being used, the Guid helps to identify the control and get the correct value. If the attached property KeyPath has not been set, I set the TemplateKey property to the type of the ViewModel being used instead. That way, if you want to, you can set the KeyPath property to an empty string to use the type of the VM as the ResourceDictionary key (not recommended…). Otherwise, I data bind the TemplateKey property using the KeyPath.

public override void OnApplyTemplate()
{
base.OnApplyTemplate();

var path = TemplateSelection.GetKeyPathById(Guid.Parse(TemplateSelectorKey));
if (string.IsNullOrEmpty(path))
{
var key = DataContext.GetType();
SetValue(TemplateKeyProperty, key);
return;
}
SetBinding(TemplateKeyProperty, new Binding(path));
}

Ok, now that we got the key stuff handled, it is time to get the data template. This is done in the TemplateKey’s change callback method… The nice thing is that this will be called in both the cases above…it doesn’t matter if I use SetValue() or SetBinding(), the system will handle the everything and make sure the callback is called…

In the callback the actual value is available, and I don’t have to care if it has been set explicitly in the previous code or fetched from the ViewModel using a data binding.

Using the Guid and the key, I can once again ask the TemplateSelection class for the template (I will get back to how it does this later). If I get a DataTemplate back, I call its LoadContent() method to create the controls defined by it and then set the Content property to the returned value. The TemplateBinding defined in the control template will handle the rest… And if no DataTemplate is returned, I just set the Content property to the DataContext, causing the control to renter a TextBlock with the name of the DataContext type…

private void OnTemplateKeyChanged(DependencyPropertyChangedEventArgs args)
{
var tc = TemplateSelection.GetTemplate(Guid.Parse(TemplateSelectorKey), args.NewValue);
if (tc != null)
{
Content = tc.LoadContent();
return;
}
Content = DataContext;
}

Ok, so what is happening in the GetTemplate() method? Well…a whole bunch…

First I check if the dictionary with WeakReferences to controls contains the supplied key. If not, I just return null (shouldn’t happen unless someone is messing with the code…), otherwise I get hold of the WeakReference and verify that it is still alive. If it isn’t, I once again return null…

Now, if I get this far, it is time to find the template. If the ResourceDictionary attached property has been set on the control, I look for a DataTemplate in the defined dictionary, otherwise I traverse the control tree looking for a data template with the correct key…

internal static DataTemplate GetTemplate(Guid id, object key)
{
if (!_controlDictionary.ContainsKey(id))
return null;

var wr = _controlDictionary[id];
if (!wr.IsAlive)
return null;

var dictionary = GetResourceDictionary((DependencyObject) wr.Target);
if (dictionary != null)
{
return dictionary.Where(kvp => kvp.Key.Equals(key)).Select(kvp => kvp.Value).OfType<DataTemplate>().FirstOrDefault();
}

return GetTemplateFromResources(wr.Target, key);
}

And the traversal code looks like this

private static DataTemplate GetTemplateFromResources(object obj, object key)
{
var fe = obj as FrameworkElement;
if (fe != null)
{
var template = fe.Resources.Where(kvp => kvp.Key.Equals(key)).Select(kvp => kvp.Value).OfType<DataTemplate>().FirstOrDefault();
if (template != null)
return template;

if (fe.Parent != null)
return GetTemplateFromResources(fe.Parent, key);
}
return null;
}

Ok, that is actually all there is to it…hehe…yeah, I know, it isn’t the simplest code available…

Quick recap! The list control sets the KeyPath attached property. Setting this property causes the ItemTemplate property of the control to be set to a dynamically created DataTemplate that contains a single control called TemplateSelectorDisplayControl. The TemplateSelectorDisplayControl gets a Guid passed to it, which is used to identify what control it is being used for.

The TemplateSelectorDisplayControl then data binds its TemplateKey DependencyProperty property using the value of the KeyPath attached property. This makes it possible to read the value from the ViewModel without knowing anything about it or having it implement some special interface. Once the key has been retrieved using the binding, it, together with the Guid, is used to get hold of a template with the correct key.

And once it has the data template, it is used to create the content of the control…

That is a couple of really weird steps to get it working, but as soon as that is built, we can write the following Xaml

<UserControl.Resources>
<DataTemplate x:Key="Test">
<Grid Background="Yellow">
<TextBlock Text="{Binding Message}" />
</Grid>
</DataTemplate>
</UserControl.Resources>

<Grid x:Name="LayoutRoot" Background="White">
<ItemsControl local:TemplateSelection.KeyPath="Test">
<ItemsControl.Resources>
<DataTemplate x:Key="Test2">
<Grid Background="Green">
<TextBlock Text="{Binding Message}" />
</Grid>
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.Items>
<local:TestClass Test="Test" Message="Hello World" />
<local:TestClass Test="Test2" Message="Hello World2" />
</ItemsControl.Items>
</ItemsControl>
</Grid>

Which will render a Yellow and a Green row with the text “Hello World” and “Hello World 2” inside.

Or, if you want to skip the control traversal, you can write

<ItemsControl local:TemplateSelection.KeyPath="Test" 
local:TemplateSelection.ResourceDictionary="{Binding Resources, ElementName=ctrl}">

Which will make sure that only DataTemplates in the “ctrl” control’s Resources will be used, which in this case makes the green template fail and instead render the name of the DataContext type…

Unfortunately, the latter code does not work very well at design-time, but works fine at runtime… At design-time it seems to still use the traversal code, thus finding the template correctly. So to be honest, it works a little too well at design-time in this case…

That’s it! As the code is complicated, and my descriptions probably a bit lacking, I suggest having a play around with the code… If you find any issues please let me know so I can have a look at it. But so far, it seems to work for me…

Code: TemplateSelector.zip (47.92 kb)

Pingbacks and trackbacks (1)+

Comments are closed