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);

1 comment:

  1. It's a very related story for the casino's performance, too. Navigating the site is a simple course of, and it is even 점보카지노 more fluid when using the cellular platform. The best thing is, you do not even should do anything special to win it - merely play at Bitstarz casino, and you'll be|and you may be} routinely entered. Bitstarz has a welcome package deal for brand spanking new|for model new} players in which you'll find a way to|which you'll} get up to an extra 5 BTC properly as|in addition to} 180 free spins.