Friday, January 27, 2012

XAML DataTemplates binding to interface

Some times it is much convenient to use interface as a View Model for a View. In WinForms there was no problem with it (using BindingSource for instance), but in WPF DataBinding engine is limited.
Let's say you have any TreeView (the same works for ListBox, any ItemsControl or ContentControl, ContentPresenter - whatever has *TemplateSelector property). And you want nodes of this TreeView to display different content using different DataTemplates. And you want these DataTemplates to by mapped be bounded to Interface, instead of class. Another words View Model of each node datatemplate is defined by interface.

So here is the solution to achieve it.

I've created an MarkupExtension, that allows to assign very easily TemplateSelector, which will select appropriate datatemplate based on compatibility of interface.
The XAML will look like this:

            ItemsSource="{Binding Source={StaticResource nodes}}"
            ItemTemplateSelector="{local:InterfaceTemplateSelector nodeDataTemplate}"

                    DataType="{x:Type local:INode}"
                    ItemsSource="{Binding Path=(local:INode.Children)}"

If you want to have several templates for different nodes than you will need the following XAML modifications:
For ListBox you will have to change property ItemTemplateSelector="{local:InterfaceTemplateSelector 'rootNodeDataTemplate,nodeDataTemplate'}"
and create second data template:
                   DataType="{x:Type local:IRootNode}"
                   ItemsSource="{Binding Path=(local:IRootNode.Children)}">....
InterfaceTemplateSelector has two properties:
1) ResourceKeys (also first constructor parameter)
2) ResourceKeysSeparator (also second optional constructor parameter) - default value ","
InterfaceTemplateSelector initialization parameter is CSV-string representing keys of resources which are data templates targeted to Interface.

This selector will take all available resource DataTemplates and check DataType each of it (which should have interface assigned) against type of content Item that is about to render.
Keep in mind that sequence of keys in InterfaceTemplateSelector defines priority of associating DataTemplate for content Item. Let's say you have Item that implement both interfaces (INode and IRootNode). But in our case it will get rootNodeDataTemplate assigned, because its key mentioned first.

Here is the implementation the markup extension, to make it work accordingly:

    public class InterfaceTemplateSelectorExtension : System.Windows.Markup.MarkupExtension
        public InterfaceTemplateSelectorExtension()
            ResourceKeysSeparator = ",";
        public string ResourceKeysSeparator { getset; }
        public InterfaceTemplateSelectorExtension(string resourceKeysCSV)
            ResourceKeys = resourceKeysCSV;
        public InterfaceTemplateSelectorExtension(string resourceKeys, string separator)
            ResourceKeys = resourceKeys;
            ResourceKeysSeparator = separator;
        /// Comma separated resource keys specifying keys of DataTemplates that binds to interface
        public string ResourceKeys { getset; }
        public override object ProvideValue(IServiceProvider serviceProvider)
            return new InterfaceTemplateSelector(ResourceKeys.Split(new string[]{ResourceKeysSeparator}, StringSplitOptions.RemoveEmptyEntries));
        public class InterfaceTemplateSelector:DataTemplateSelector
            string[] resourceKeys;
            public InterfaceTemplateSelector(string[] resourceKeys)
                this.resourceKeys = resourceKeys;
            public override DataTemplate SelectTemplate(object item, DependencyObject container)
                var c = (FrameworkElement)container;
                var dataTemplates = (from rk in resourceKeys
                                let resource = c.TryFindResource(rk)
                                where resource is DataTemplate
                                where (resource as DataTemplate).DataType is Type
                                select resource).Cast<DataTemplate>()
                var itemType = item.GetType();
                var result = dataTemplates.FirstOrDefault(dt => 
                    (dt.DataType as Type).IsInstanceOfType(item)
                return result??base.SelectTemplate(item, container);