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

2

9

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.*

Prismatic answered 18/1, 2017 at 8:52 Comment(4)
Short answer: Because it's designed that way. Probably because you could get multiple matches for a single class. Official answer on MSDNFootcloth
If you look at your MSDN link, you'll see they specifically say you can use a DataTemplateSelector, which I am doing above. Your argument, and what they say they decided against is with them trying to match the interface themselves, which is not what I'm trying to achieve here.Prismatic
Your original question was "Why can't a DataTemplate bind to an interface?". I thought you were interested in why it is the way it is.Footcloth
A modified version of your SubclassedTypeTemplateSelector worked for me with viewmodels-as-interfaces in a non-hierarchical scenario. The fact that you debugged yours and you verified that it returns the right template means that the modifications I made are irrelevant. So, your problem is most likely related to the fact that you are (were, in 2017,) working in a hierarchical scenario.Kor
P
1

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");
Pinot answered 18/1, 2017 at 10:28 Comment(1)
Actually, I tried exactly that and that doesn't work either. Specifically I removed the DataType property, added a key, then returned the template found by that key (which is essentially exactly what you suggested) and it still didn't work. However, more peculiarly, when I set that template directly to the ItemTemplate of the TreeView ({StaticResource ICategory}), it does work. And again, the ItemsSource binding works in either case which is what stumps me even more.Prismatic
D
0
  1. DataTemplates can not aim to Interfaces - so you need a selector or a workaround -> done.
  2. you can just use {x:Type} instead of implementing BaseTypeMapping
  3. use BaseType.IsAssignableFrom(item.GetType()) to check if you have a match
  4. you have to remove the type from you template definition, it can not be assigned if you have a wrong type - so you add a key.
  5. after #4 your template has a key and does not work implicit by just assigning the type, so you have to remove it -> dataTemplate.Key = null after you assign a type.
  6. I don't have an IDE here, but find ressources gives you an instance, so you change by reference. Is this what you intend?
Dorfman answered 18/1, 2017 at 15:0 Comment(1)
I actually use BaseTypeMapping because it also has a DataTemplate and DataTemplateKey properties (which I omitted from here for brevity.) BUT... using that instead, meaning the template doesn't have a DataType assigned, it works when set directly to the ItemTemplate but not when returned via the ItemTemplateSelector, which is why I'm completely stumped here, especially because as I said, the ItemsSource binding does work.Prismatic

© 2022 - 2024 — McMap. All rights reserved.