Pages

Friday, November 5, 2010

Sometimes it makes sense to have multiple types of views contained within a list or region. In WPF, a data template selector can help determine which template is used based on the data template, allowing a container to mix different types. It's not so straightforward with Silverlight because the DataTemplateSelector class does not exist.

There have been some excellent articles on the web with simple workarounds for this, but they often involve some sort of dictionary or hard-coded selection process. What I wanted to do was provide an example that does several things:

Allows you to add new templates easily simply by tagging them with attributes,

Is design-time friendly, and

Handles dependencies in the templated views, if needed

The target situation is a hypothetical social media feed that aggregates data from different types but shows them in a single list. This example mocks the data but should be sufficient to show how it could be done. Consider this screenshot which shows three different styles of presenting data in the list:

So, let's get started. The way I decided to handle the selector would be to use views as my "data templates" (this is similar to how Calburn.Micro does it) and tag the views with the type they handle.

I created three simple types:

To wire a sample in the designer, I created a design-time model that is not built in release mode. Here is the example for a tweet:

Now we need to tag it. If you are familiar with MEF, you know it is not straightforward to create an ExportFactory with metadata, as there is no easy way to reach the metadata and create the corresponding factory (this is what is needed to create new views for each item, rather than having a single copy). Even the Lazy feature won't work for us because the lazy value is lazy loaded, but then retains the same value. So what can we do?

We'll get to the tricky part in a second, but for now we'll export our view twice. First, we'll make a special export that allows us to tag the view with the type of entity it can handle. The metadata looks like this:

Notice that I export the control twice. The first time is to tag the target type it can handle, and the second time is to give me an export I can work with to create the views inside the templates.

We repeat the process for the photos and blogs. Next, I need a container to help me generate new views. Remember, I can't grab both metadata and export factories, so I'll "cheat" by making a host for the export factory:

Essentially, when I type this class and create an instance, I can reference the Instance property and it will go to the export factory to give me a new instance. We could simplify this and just restrict T to new(), and that would work fine, but doing this also allows the type to have dependencies. If you wanted to import a logger, event aggregator, or other dependency, this mechanism supports it - and you'll be surprised at how good the performance actually is.

Now we've got some work to do. I'm going to use a value converter that takes the data type and returns the data template. We'll also bind the data to the data template. Here's what it looks like, and I'll explain what's going on below:

So the first thing we do is compose imports if we aren't in the designer. This will pull in all tagged views and the metadata that describes the types that they support.

Notice I have two dictionaries. One dictionary maps the target type to the TemplateFactory<T> for the view that supports the target type. Because we're dealing with generics, I only need one copy per type, so with 1,000 records I still only make three factory objects (one per type). Second, because we'll use reflection to grab the Instance property (since we can't close the generic directly) I don't want to reflect every single row. Next, I store a function that handles the result of the reflection.

When the value comes in, if we're in the designer, we just return a simple bit of text. Otherwise, we set up an empty Grid to return if our data template selection fails. You could throw an exception instead of to make this more robust. If I've already processed the type, I simply look up the function and call it to get a new view.

For the first time I find a type, we get to have some fun. First, I find the view that is intended for the target type by inspecting the metadata. This only has to be done once to get the type for the view. Next, I create an instance of the template factory. This works from the inside out for this statement:

The typeof statement gets up what's called an "open generic" or a generic type that hasn't been scoped to a target type yet. In order to close the generic, we call the MakeGenericType method and pass it the type we want to close it with - in this case, the type of the template we found by inspecting meta data. We create an instance, and now we have our MEF factory for generating the view.

Because we can't close the type, we need to use reflection to find the property that grabs the instance for us:

This uses reflection to get the property, gets the "getter" method, then invokes it for the factory object we just created and casts it as a UserControl. If you want to support other exports, you can cast this down to a base FrameworkElement if you like.

If you're really concerned with performance, you can cache the result of the GetGetMethod as an Action (storing the full function here will still call the reflection, whereas storing the result of the get will store a pointer to the actual getter without reflection - make sense?) then you could call that cached method, but I didn't see enough issues with performance to go to that level.

Finally, we wire up the DataContext. Here, we have to be careful. If we wire the Loaded event to a method, we'll lose the context of where we're at and not be able to hook the item to the data context. However, if we assign a lambda expression, we're really creating a reference from this converter to the view - which means it will never be released from memory.

So instead, we create a reference to the handler, then pass a lambda expression. The lambda expression is able to reference the item to wire to the data context, and also reference itself to unhook from the Loaded event. The result is that the control will hook to the item and then lose the event handler. This assumes the templates have a LayoutRoot, if you prefer you can connect to the user control directly.

The view model exposes a generic list of objects:

public ObservableCollection<object> SocialFeed { get; private set; }

And wires in 1,000 items randomly chosen between tweets, photos, and blog entries. You can tweak the size to test larger or smaller lists for performance. Using our selector is simple - we declare it and then use it to bind the object to a ContentControl.

When you run, you see we get a nice virtualizing list box with selected data templates and the performance on my machine is fast.

From here, you can do several things. If your items implement a common interface, you might provide a default template that handles anything and then export specific templates as you implement more detailed types. Obviously you can get rid of the instance factory and the composition initialization if your views will have no dependencies. Once this is wired in place, adding a new "data template" is as easy as design it, tag it, and run it.

To quote Forrest Gump, "That's all I got to say about that." Grab the source here.

8 comments:

One cool trick a friend showed me a little while back was to use a ConditionalAttribute (i.e. [Conditional("Debug")]) instead of a #if. It looks a lot nicer, but it also has the added benefit that it will also strip out all calls to the code in question at compile time.

This technique worked beautifully for me. I added a second module (xap) which also needed to use the Data Template Selector and it failed to work. To fix it I have changed the Attribute on DataTemplateSelector.Templates to be[ImportMany(AllowRecomposition=true)]This is a bit cargo cult for me as I don't fully have my head around MEF. It works ... but did I do something silly?

Paul, I know I'm answering this a few months late, but ... no, you did exactly what you should. By default an import can only be satisfied once, so when you loaded the second XAP, MEF had to reject the exports there. By specifying "AllowRecomposition=true" you give it permission to compose multiple times, i.e. when a new XAP is available. That is correct!

Working with this to see if it can be integrated in with Prism 4 application, fyi if you have to CompositionHost.Initialize() in your bootstrapper as we seem to have to to build our catalogs, then the SatisfyImports in the DataTemplateSelected has to be commented out or you'll get an exception.

Jeremy is it possible to have the DataTemplate selector load a template based upon a property within a collection vs. a type as you have within your example? I am receiving a collection from a service that contains a property called type. I would like to evaluate this field vs. the type and generate the same result. Thanks in advance

Thanks for this wonderful post, I have a similar requirement where the Mainpage has a treeview control (elements of type object) and a contentcontrol, the contents of the contentcontrol change based on the element selected in the treeview and the datacontext of the page loaded into the contentcontrol needs to be set to the selected element of the treeview.

I was able to achieve it using your datatemplateselector, although I have run into a small problem. The first time around when the required View is not in the templatefactory and it executes the statement (from t in Templates where t.Metadata.TargetType.Equals(value.GetType()) to find the appropriate View from Templates[] it seems to instantiate the View and again later after adding the View to the templatefactory. There by running the constructor of the View twice. I was wondering if you had run into the same problem. Any advice or help is greatly appreciated

Anonymous - you are probably better off to use the implict data template feature of Silverlight 5. This post is from Silverlight 4 timeframe and the implicit data template feature pretty much makes the technique presented here redundant.Of course, if you are still on Silverlight 4 ... then maybe this feature is a good reason to upgrade?There is lots of information around on how to get started, if you like to watch then maybe http://blogs.msdn.com/b/devschool/archive/2011/05/08/silverlight-5-implicit-data-templates.aspx would suit.