Friday, November 5, 2010

Silverlight Data Template Selector using Managed Extensibility Framework

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:

Data Template Selector in Silverlight

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:

Social Media 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:

namespace MEFDataTemplate.SampleData
{
#if DEBUG
    public class DesignTimeTweet : Tweet 
    {
        public DesignTimeTweet()
        {
            AvatarUri = new Uri("/MEFDataTemplate;component/Images/twitterprofile.png", UriKind.Relative);
            Message = "This is a tweet inside the designer.";
            Posted = DateTime.Now;
            Username = "@jeremylikness";
        }
    }
#endif
}

Now we can build out template for a tweet. I created a folder called Templates and added a user control called TweetTemplate.xaml. The XAML organizes a sample tweet like this:

    <Grid x:Name="LayoutRoot" HorizontalAlignment="Stretch" Background="White" d:DataContext="{d:DesignInstance design:DesignTimeTweet,IsDesignTimeCreatable=True}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>            
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <StackPanel Orientation="Horizontal" Margin="5">
            <Image Margin="2" Source="/MEFDataTemplate;component/Images/twitter.png"/>
            <TextBlock VerticalAlignment="Center" Margin="2" Text="{Binding Username}"/>
        </StackPanel>
        <Image Margin="2" VerticalAlignment="Top" Source="{Binding AvatarUri}" Stretch="None" Grid.Row="1"/>
        <TextBlock Margin="5" Text="{Binding Posted,StringFormat='MM-dd-yyyy h:mm:ss'}" FontWeight="Bold" HorizontalAlignment="Right" Grid.Column="1"/>
        <TextBlock Margin="5" TextWrapping="Wrap" Text="{Binding Message}" Grid.Row="1" Grid.Column="1"/>
    </Grid>

Inside the designer, it looks like this:

Data Template inside the Designer

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:

namespace MEFDataTemplate.Templates
{
    public interface IExportAsTemplateForTypeMetadata
    {
        Type TargetType { get; }        
    }
}

And the export attribute itself is implemented like this:

namespace MEFDataTemplate.Templates
{
    [MetadataAttribute]
    [AttributeUsage(AttributeTargets.Class)]
    public class ExportAsTemplateForTypeAttribute : ExportAttribute, IExportAsTemplateForTypeMetadata 
    {
        public ExportAsTemplateForTypeAttribute(Type targetType) : base(typeof(UserControl))
        {
            TargetType = targetType;            
        }

        public Type TargetType { get; set; }       
    }
}

Now, we can tag our template and let MEF know it handles Tweet objects:

namespace MEFDataTemplate.Templates
{
    [ExportAsTemplateForType(typeof(Tweet))]
    [Export]
    public partial class TweetTemplate
    {
        public TweetTemplate()
        {
            InitializeComponent();
        }
    }
}

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:

namespace MEFDataTemplate.Templates
{
    public class TemplateFactory<T>
    {
        [Import]
        public ExportFactory<T> Factory { get; set; }

        public T Instance
        {
            get
            {
                return Factory.CreateExport().Value;
            }
        }
    }
}

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:

namespace MEFDataTemplate.Converters
{
    public class DataTemplateSelector : IValueConverter 
    {
        private const string INSTANCE = "Instance";
        const string LAYOUT_ROOT = "LayoutRoot";                                      
            
        public DataTemplateSelector()
        {
            if (!DesignerProperties.IsInDesignTool)
            {
                CompositionInitializer.SatisfyImports(this);
            }
        }

        [ImportMany]
        public Lazy<UserControl, IExportAsTemplateForTypeMetadata>[] Templates { get; set; }

        private readonly Dictionary<Type,object> _factories = new Dictionary<Type, object>();

        private readonly Dictionary<Type,Func<UserControl>> _templateFactory = new Dictionary<Type,Func<UserControl>>();

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (DesignerProperties.IsInDesignTool)
            {
                return new TextBlock {Text = "Design time row."};
            }

            FrameworkElement view = new Grid();

            if (value != null)
            {                
                if (_templateFactory.ContainsKey(value.GetType()))
                {
                    view = _templateFactory[value.GetType()]();                   
                }
                else 
                {
                        var viewType = (from t in Templates
                                        where t.Metadata.TargetType.Equals(value.GetType())
                                        select t.Value.GetType()).FirstOrDefault();
                        if (viewType != null)
                        {
                            var factory = Activator.CreateInstance(typeof(TemplateFactory<>).MakeGenericType(new[] { viewType }));
                            CompositionInitializer.SatisfyImports(factory);
                            _factories.Add(value.GetType(), factory);
                            Func<UserControl> resolver = ()=>
                                factory.GetType().GetProperty(INSTANCE).GetGetMethod().Invoke(factory, null) as UserControl;
                            _templateFactory.Add(
                                value.GetType(), resolver);
                            view = _templateFactory[value.GetType()]();
                        }
                }

                if (view != null)
                {
                    RoutedEventHandler handler = null;
                    handler = (o, e) =>
                                  {
                                      view.Loaded -= handler;
                                      ((Grid) view.FindName(LAYOUT_ROOT)).DataContext = value;
                                  };
                    view.Loaded += handler;
                }
            }

            return view;
        }       

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

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:

var factory = Activator.CreateInstance(typeof(TemplateFactory<>).MakeGenericType(new[] { viewType }));

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:

 Func<UserControl> resolver = ()=>
                                factory.GetType().GetProperty(INSTANCE).GetGetMethod().Invoke(factory, null) as UserControl;

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.

    <UserControl.Resources>
        <Converters:DataTemplateSelector x:Key="Selector"/>
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot" Background="White">        
        <Grid.DataContext>
            <ViewModel:MainViewModel/>
        </Grid.DataContext>
        <ListBox ItemsSource="{Binding SocialFeed}" HorizontalAlignment="Center" Width="800" VerticalAlignment="Top">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <ContentControl Width="750" HorizontalAlignment="Center" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"
                                    Content="{Binding Converter={StaticResource Selector}}"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>

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.

Jeremy Likness