如何将DataTemplate的数据类型绑定到接口?

34
我正在编写一个复合松耦合的MVVM WPF应用程序,父VM中的子VM是接口而不是类实例,例如:
public IChildViewModel { get; set; }

现在我该如何使用DataTemplate渲染此属性呢?例如:
<DataTemplate DataType="{x:Type contracts:IChildViewModel}">

我知道由于接口的本质(多重继承等),WPF不允许直接绑定。但是,由于接口应该在松散耦合的应用程序中广泛使用,是否有绑定DataTemplate到接口的解决方法?谢谢。


1
使用一个 ContentControl,根据传递给 IValueConverterDataContext 和接口设置它的 ContentTemplate,这样怎么样?然后您可以测试值是否是与参数传递的类型相同,如果为 True,则使用适当的 DataTemplate。 - Rachel
5个回答

5
你可以通过明确告诉wpf你绑定到一个接口字段来绑定接口:
(请注意,ViewModelBase只是实现INotifyPropertyChanged接口的基类)
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; }
}

然后在 ViewModel 中:

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

最后,让它成为可能的Xaml代码:

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

正如您所看到的,绑定明确地指向“IInterface”定义。

4
这真的可行吗?我相信XAML类型系统不考虑接口,就像在这里讨论的那样:http://badecho.com/2012/07/adding-interface-support-to-datatemplates/ 虽然这可能会编译通过,但我怀疑它是否能按预期工作。 - Élie
6
确实,我刚试过:它编译通过了,但 DataTemplate 没有被应用。 - ckuepker
刚刚也试了一下。它可以工作,但只有在你明确写出Path=时才能工作。即{Binding (viewModels:IInterface.TextField)}是行不通的。 - N. Kudryavtsev

4
似乎在这种情况下使用DataTemplateSelector是可行的方式。

1
你可以将你的接口转换为等效的抽象类。它的工作方式如下。

1
我在UWP的数据模板中使用了接口类型的绑定,没有在绑定路径上显式指定接口类型。当接口没有被明确实现时,它可以正常工作。但是当接口被明确实现后,它会默默失败。我认为如果接口被明确实现,则需要在绑定路径中显式引用接口类型,以便绑定可以正确查找属性路径。

1
这是我的InheritanceDataTemplateSelector,它只与接口一起使用。
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://dev59.com/gZ7ha4cB1Zd3GeqPhU9L
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 );
    }
}

如何使用:
在您的“资源”部分中,像往常一样定义每个“DataTemplate”,但现在每个“DataType”都是一个接口而不是具体类型:
<DataTemplate DataType="{x:Type viewModels:MyViewModelInterface}">
    <local:MyView />
</DataTemplate>

然后,为InheritanceDataTemplateSelector添加一个额外的资源:
<myWpf:InterfaceDataTemplateSelector x:Key="InterfaceDataTemplateSelector" />

然后,在需要使用的正确位置,指定应该使用这个选择器。例如,在一个中:
<ItemsControl ItemsSource="{Binding SomeViewModelCollection}"
    ItemTemplateSelector="{StaticResource InterfaceDataTemplateSelector}">

注意:ViewModel接口不必扩展INotifyPropertyChanged。ViewModel的具体实现可以实现它,如果需要的话。 另外请注意:与其他答案所建议的相反,在绑定到接口ViewModel的成员时,没有必要使用任何特殊的符号。(至少在最近版本的WPF中是如此。)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接