Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't a DataTemplate bind to an interface when that DataTemplate was explicitly returned from a DataTemplateSelector?

I've created a DataTemplateSelector which is initialized with a collection of known interfaces. If an item passed into the selector implements one of those interfaces, the associated data template is returned.

First, here's the ICategory interface in question...

public interface ICategory
{
    ICategory ParentCategory { get; set; }
    string    Name           { get; set; }

    ICategoryCollection Subcategories { get; }
}

Here's the DataTemplateSelector which matches based on a base class or interface rather than just a specific concrete class...

[ContentProperty("BaseTypeMappings")]
public class SubclassedTypeTemplateSelector : DataTemplateSelector
{
    private delegate object TryFindResourceDelegate(object key);

    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        var frameworkElement = container as FrameworkElement;

        foreach(var baseTypeMapping in BaseTypeMappings)
        {
            // Check if the item is an instance of, a subclass of,
            // or implements the interface specified in BaseType
            if(baseTypeMapping.BaseType.IsInstanceOfType(item))
            {
                // Create a key based on the BaseType, (not item.DataType as usual)
                var resourceKey = new DataTemplateKey(baseTypeMapping.BaseType);

                // Get TryFindResource method from either the FrameworkElement,
                // or from the application
                var tryFindResource = (frameworkElement != null)
                    ? (TryFindResourceDelegate)frameworkElement.TryFindResource
                    : Application.Current.TryFindResource;

                // Use the TryFindResource delegate from above to try finding
                // the resource based on the resource key
                var dataTemplate = (DataTemplate)tryFindResource(resourceKey);
                dataTemplate.DataType = item.GetType();
                if(dataTemplate != null)
                    return dataTemplate;
            }
        }

        var defaultTemplate = DefaultDataTemplate ?? base.SelectTemplate(item, container);
        return defaultTemplate;
    }

    public DataTemplate DefaultDataTemplate { get; set; }

    public Collection<BaseTypeMapping> BaseTypeMappings { get; } = new Collection<BaseTypeMapping>();
}

public class BaseTypeMapping
{
    public Type BaseType { get; set; }
}

Here's how it's set up in the resources along with the respective HierarchicalDataTemplate with DataType = ICategory...

    <HierarchicalDataTemplate DataType="{x:Type model:ICategory}"
        ItemsSource="{Binding Subcategories}">

        <TextBlock Text="{Binding Name}" />

    </HierarchicalDataTemplate>

    <is:SubclassedTypeTemplateSelector x:Key="SubclassedTypeTemplateSelector">
        <!--<is:BaseTypeMapping BaseType="{x:Type model:ICategory}" />-->
    </is:SubclassedTypeTemplateSelector>

And finally, here's a TreeView which uses it...

<TreeView x:Name="MainTreeView"
    ItemsSource="{Binding Categories}"
    ItemTemplateSelector="{StaticResource SubclassedTypeTemplateSelector}" />

I've debugged it and can confirm the correct data template is being returned to the TreeView as expected both stepping through the code and because the TreeView is properly loading the subcategories as per the ItemSource binding on the HierarchicalDataTemplate. All of this works as expected.

What doesn't work is the contents of the template itself. As you can see, the template is simply supposed to show the name of the category but it's just presenting the object raw as if it were placed directly in a ContentPresenter without any template. All you see in the UI is the result of ToString. The template's contents are completely ignored.

The only thing I can think of is its not working because I'm using an interface for the DataType, but again, the binding for the children's ItemsSource does work, so I'm kind of stumped here.

Of note: As a test, I created a second DataTemplate based on the concrete type (i.e. Category and not just ICategory) and when I did, it worked as expected. The problem is the concrete type is in an assembly that's not supposed to be referenced by the UI. That's the entire reason we're using interfaces in the first place.

*NOTE: I have also tried changing the way I look up the template by using a Key instead of setting the DataType property. In that case, just as before, the selector still finds the same resource, but it still doesn't work!

Ironically however, if I use that same key to set the ItemTemplate of the TreeView directly via a StaticResource binding, then it does work, meaning it only doesn't work when I return the template from the selector and does not appear related to whether DataType is set or not.*

like image 858
Mark A. Donohoe Avatar asked Jan 18 '17 08:01

Mark A. Donohoe


1 Answers

What doesn't work is the contents of the template itself

This is because the templates that you define in your XAML markup are not being applied since the DataType property is set to an interface type. As @Manfred Radlwimmer suggests this is by design: https://social.msdn.microsoft.com/Forums/vstudio/en-US/1e774a24-0deb-4acd-a719-32abd847041d/data-templates-and-interfaces?forum=wpf. Returning such a template from a DataTemplateSelector doesn't make it work as you have already discovered.

But if you use a DataTemplateSelector to select the appropriate data template you could remove the DataType attribute from the data templates and give each template a unique x:Key instead:

<HierarchicalDataTemplate x:Key="ICategory" ItemsSource="{Binding Subcategories}">
    <TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>

You should then be able to resolve the resource using this key, e.g.:

var baseTypeName = "ICategory";
var dataTemplate = (DataTemplate)tryFindResource("baseTypeName");
like image 120
mm8 Avatar answered Nov 08 '22 19:11

mm8