Reputation: 794
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
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
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
Reputation: 2947
It seems that using a DataTemplateSelector
is the way to go in such situations.
Upvotes: 4
Reputation: 992
You can convert your interface to an equivalent abstract class. It works in this way.
Upvotes: 1
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