2

I have a TreeView bound to a list of Tileset. Tileset contains TileGroup, TileGroup contains both Tile and TileRun instances. Both Tile and TileRun implement ITile, but eventually there will be many more types implementing ITile

I have the following XAML:

<TreeView
    Grid.Row="0"
    Grid.Column="0"
    BorderThickness="0"
    ItemsSource="{Binding Path=Tilesets}">
    <TreeView.Resources>
        <HierarchicalDataTemplate DataType="{x:Type local:Tileset}" ItemsSource="{Binding Path=TileGroups}">
            <TextBlock Text="{Binding Path=Name}" />
        </HierarchicalDataTemplate>
        <HierarchicalDataTemplate DataType="{x:Type local:TileGroup}" ItemsSource="{Binding Path=Tiles}">
            <TextBlock Text="{Binding Path=Name}" />
        </HierarchicalDataTemplate>
        <DataTemplate DataType="{x:Type tiles:ITile}">
            <TextBlock Text="{Binding Path=Name}" />
        </DataTemplate>
    </TreeView.Resources>
</TreeView>

Tileset and TileGroup choose the correct DataTemplate but ITile does not, no template is selected, the tree just displays the data type.

However, if I add a DataTemplate for both Tile and TileRun explicitly, everything works great.I don't want to do that though, as there will eventually be many more classes implementing ITile.

I am aware that I could handle this using a DataTemplateSelector, but I'd like a pure XAML solution if possible.

Am I doing something wrong here, or does WPF just not support this type of automatic template selection based on interfaces?

Bradley Uffner
  • 16,641
  • 3
  • 39
  • 76
  • Possible duplicate of [WPF databinding to interface and not actual object - casting possible?](https://stackoverflow.com/questions/327984/wpf-databinding-to-interface-and-not-actual-object-casting-possible) – Peter Duniho Jul 28 '17 at 23:36
  • Does this answer your question? [How to bind DataTemplate datatype to interface?](https://stackoverflow.com/questions/15023441/how-to-bind-datatemplate-datatype-to-interface) – Mike Nakis Jun 11 '23 at 17:09

2 Answers2

4

Am I doing something wrong here, or does WPF just not support this type of automatic template selection based on interfaces?

You are not doing something wrong. This kind of data binding support for interfaces is simply not supported. Please refer to Beatriz Costa's (MSFT) answer in the following thread on the MSDN forums for more information about why.

Data templates and interfaces: https://social.msdn.microsoft.com/Forums/vstudio/en-US/1e774a24-0deb-4acd-a719-32abd847041d/data-templates-and-interfaces?forum=wpf

"The data binding team discussed adding support for interfaces a while ago but ended up not implementing it because we could not come up with a good design for it. The problem was that interfaces don't have a hierarchy like object types do. Consider the scenario where your data source implements both IMyInterface1 and IMyInterface2 and you have DataTemplates for both of those interfaces in the resources: which DataTemplate do you think we should pick up?

When doing implicit data templating for object types, we first try to find a DataTemplate for the exact type, then for its parent, grandparent and so on. There is very well defined order of types for us to apply. When we talked about adding support for interfaces, we considered using reflection to find out all interfaces and adding them to the end of the list of types. The problem we encountered was defining the order of the interfaces when the type implements multiple interfaces."

So you will either have to define a DataTemplate for both Tile and TileRun explicitly or use a DataTemplateSelector.

mm8
  • 163,881
  • 10
  • 57
  • 88
1

As mm8's answer says, interfaces are not supported. The explanation given by the "data binding team" is kind of funny: they did not know what to do when many interfaces match, so they did not implement the feature.

Here is my InheritanceDataTemplateSelector which solves the problem in the following pragmatic way:

If your viewmodels implement multiple interfaces, and if you happen to have a DataTemplate defined for more than one such interfaces, this selector will just return the first matching DataTemplate.

It just does a breadth-first search, which will most likely be just fine for the vast majority of use cases, and if it is not fine for you, it should be fairly trivial to customize the search strategy to suit your needs.

    namespace MyWpf;

    using Sys = System;
    using Wpf = System.Windows;
    using WpfControls = System.Windows.Controls;

    //PEARL: DataTemplate in WPF does not work with interfaces!
    //       The declaration <DataTemplate DataType="{x:Type SomeInterface}"> silently fails.
    //       We solve this problem by introducing a DataTemplateSelector 
    //       that takes interfaces into consideration.
    //Original inspiration from https://stackoverflow.com/q/41714918/773113
    public class InterfaceDataTemplateSelector : WpfControls.DataTemplateSelector
    {
        delegate object? ResourceFinder( object key );

        public override Wpf.DataTemplate? SelectTemplate( object item, Wpf.DependencyObject container )
        {
            ResourceFinder resourceFinder = getResourceFinder( container );
            return tryGetDataTemplateRecursively( item.GetType(), resourceFinder );
        }

        static ResourceFinder getResourceFinder( Wpf.DependencyObject container ) //
            => (container is Wpf.FrameworkElement containerAsFrameworkElement) //
                    ? containerAsFrameworkElement.TryFindResource //
                    : Wpf.Application.Current.TryFindResource;

        static Wpf.DataTemplate? tryGetDataTemplateRecursively( Sys.Type type, ResourceFinder resourceFinder )
        {
            return tryGetDataTemplateFromType( type, resourceFinder ) //
                    ?? tryGetDataTemplateFromInterfacesRecursively( type, resourceFinder ) //
                    ?? tryGetDataTemplateFromSuperTypeRecursively( type, resourceFinder );
        }

        static Wpf.DataTemplate? tryGetDataTemplateFromType( Sys.Type type, ResourceFinder tryFindResource )
        {
            Wpf.DataTemplateKey resourceKey = new Wpf.DataTemplateKey( type );
            object? resource = tryFindResource( resourceKey );
            if( resource is Wpf.DataTemplate dataTemplate )
            {
                if( !dataTemplate.IsSealed )
                    dataTemplate.DataType = type;
                return dataTemplate;
            }
            return null;
        }

        static Wpf.DataTemplate? tryGetDataTemplateFromInterfacesRecursively( Sys.Type type, ResourceFinder resourceFinder )
        {
            foreach( var interfaceType in type.GetInterfaces() )
            {
                Wpf.DataTemplate? dataTemplate = tryGetDataTemplateRecursively( interfaceType, resourceFinder );
                if( dataTemplate != null )
                    return dataTemplate;
            }
            return null;
        }

        static Wpf.DataTemplate? tryGetDataTemplateFromSuperTypeRecursively( Sys.Type type, ResourceFinder resourceFinder )
        {
            return type.BaseType == null ? null : tryGetDataTemplateRecursively( type.BaseType, resourceFinder );
        }
    }

How to use:

In your Resources section, define each DataTemplate as usual, where now each DataType is an interface instead of a concrete type:

<DataTemplate DataType="{x:Type viewModels:MyViewModelInterface}">
    <local:MyView />
</DataTemplate>

Then, add one more resource for the InheritanceDataTemplateSelector:

<myWpf:InterfaceDataTemplateSelector x:Key="InterfaceDataTemplateSelector" />

Then, at the right place which needs to make use of a DataTemplate, specify that this selector should be used. For example, in an ItemsControl:

<ItemsControl ItemsSource="{Binding SomeViewModelCollection}"
    ItemTemplateSelector="{StaticResource InterfaceDataTemplateSelector}">

Note: the ViewModel interfaces do not have to extend INotifyPropertyChanged. The concrete implementation of a ViewModel may implement it, if needed.

Mike Nakis
  • 56,297
  • 11
  • 110
  • 142
  • @Samuel Liew: I edited the answer to "tailor" it to the question asked. A vote to close a duplicate often does nothing, so a question being a duplicate of another will often not prevent me from answering it. This is a problem with the way stackoverflow works. I may have voted to close a duplicate of this one, but I do not remember, and would you know? – Mike Nakis Aug 03 '23 at 18:28