Charlie
Charlie

Reputation: 794

How to bind DataTemplate datatype to interface?

I am writing a composite loosely coupled MVVM WPF application and child VMs in a parent VM are interfaces rather than class instances, e.g.

public IChildViewModel { get; set; }

Now how do I render this property using a DataTemplate? like:

<DataTemplate DataType="{x:Type contracts:IChildViewModel}">

I understand due to the nature of interfaces (multiple inheritance etc.) WPF does not allow this direct binding. But as interfaces should be used widely in loosely coupled applications, is there any workaround to bind DataTemplate to interfaces? Thanks.

Upvotes: 34

Views: 15430

Answers (5)

Mike Nakis
Mike Nakis

Reputation: 61969

Here is my InterfaceDataTemplateSelector which just works with interfaces:

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:

<UserControl x:Class=...
         xmlns:myWpf="clr-namespace:MyWpf;assembly=MyWpf"
...

    <FrameworkElement.Resources>
      <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.

Also note: contrary to what other answers suggest, there is no need to use any special notation when binding to members of an interface viewmodel. (At least not in any recent version of WPF.)

Upvotes: 2

sjb-sjb
sjb-sjb

Reputation: 1187

I have used Binding with interface types in a data template, in uwp. I did not specify the interface type explicitly on the Binding path. It worked when the interface was not implemented explicitly. When the interface was implemented explicitly it failed silently. I believe that if the interface is implemented explicitly then the explicit reference to the interface type in the Binding path is needed, so that the Binding can correctly look up the property path.

Upvotes: 1

Natxo
Natxo

Reputation: 2947

It seems that using a DataTemplateSelectoris the way to go in such situations.

Upvotes: 4

Lamelas84
Lamelas84

Reputation: 992

You can convert your interface to an equivalent abstract class. It works in this way.

Upvotes: 1

t0yk4t
t0yk4t

Reputation: 892

You can bind to interfaces by telling wpf explicitly that you are binding to an interface field:

(Please note that ViewModelBase is simply a base-class that implements the INotifyPropertyChanged interface)

public class Implementation : ViewModelBase, IInterface
{
    private string textField;

    public string TextField
    {
        get
        {
            return textField;
        }
        set
        {
            if (value == textField) return;
            textField = value;
            OnPropertyChanged();
        }
    }
}

public interface IInterface
{
    string TextField { get; set; }
}

Then on the ViewModel:

private IInterface interfaceContent;
public IInterface InterfaceContent
{
    get { return interfaceContent; }
}

And finally the Xaml that makes it possible:

<ContentControl Grid.Row="1" Grid.Column="0" Content="{Binding InterfaceContent}">
    <ContentControl.ContentTemplate>
        <DataTemplate DataType="{x:Type viewModels:IInterface}">
            <TextBox Text="{Binding Path=(viewModels:IInterface.TextField)}"/>
        </DataTemplate>
    </ContentControl.ContentTemplate>
</ContentControl>

As you can see, the binding refers explicitly to the 'IInterface' definiton.

Upvotes: 4

Related Questions