WPF数据绑定到接口而非实际对象 - 是否可以进行类型转换?

55

假设我有这样一个接口:

public interface ISomeInterface
{
...
}

我还有几个实现这个接口的类;

public class SomeClass : ISomeInterface
{
...
}

现在我有一个WPF ListBox,其中列出了ISomeInterface的项目,并使用自定义DataTemplate。

数据绑定引擎显然不允许我绑定到接口属性(至少我还没能找出来如何实现)-它只会看到对象是SomeClass对象,只有当SomeClass恰好具有可用作非接口属性的绑定属性时,数据才会显示出来。

我该如何告诉DataTemplate,对待每个对象都像它是一个ISomeInterface,而不是SomeClass等等呢?

谢谢!


谢谢通知 @slugster,我已更新被采纳的答案。 :) - Rune Jacobsen
6个回答

68
为了绑定到显式实现的接口成员,你只需要使用括号。例如:

隐式实现:

{Binding Path=MyValue}

显式:

{Binding Path=(mynamespacealias:IMyInterface.MyValue)}

很高兴我偶然发现了这个,它还帮助我解决了与绑定到PostSharp创建的对象有关的问题;http://stackoverflow.com/questions/13636962/binding-wpf-controls-to-dynamically-introduced-interfaces-on-model-objects - RJ Lohan
不幸的是,这似乎并不适用于OP的问题(我知道这个问题很旧了)。他没有询问绑定,而是DataType="{x:Type local:SomeClassBase}" - Steve
3
@Steve,这个答案是正确的 - OP在谈论数据模板内的绑定路径,具体类型上的显式接口属性默认情况下不可见于绑定引擎(隐式接口可见)。我认为被接受的答案并不是OP想要的,特别是因为这个答案是一年后发布的。 - slugster
1
这个可以工作,但不幸的是引擎还不够聪明,无法为IMyInterface.MyValueChanged事件添加处理程序(handler),如果有的话。这意味着,如果您想要更改通知,则实现类也必须实现INotifyPropertyChanged并使用接口属性的完全相同名称。如果多个接口被显式实现,并且相同的属性名称存在于其中一个接口上,则可能会出现问题。请谨慎使用。 - Crono
1
"Path="非常重要。在我添加它之前,它是无法工作的。 - bwall

21

这篇来自微软论坛的回答,作者是Beatriz Costa - MSFT,值得一读(比较老):

数据绑定团队曾讨论过添加接口支持,但最终未实现,因为我们无法为其设计出合适的方案。问题在于,接口没有像对象类型那样的层次结构。考虑这样一种情况:您的数据源同时实现了IMyInterface1和IMyInterface2,并且您在资源中有这两个接口的DataTemplate:我们应该选择哪个DataTemplate呢?
对于对象类型的隐式数据模板化,我们首先尝试查找确切类型的DataTemplate,然后是其父级、祖先等。对于我们要应用的类型,有非常明确定义的类型顺序。当我们考虑添加对接口的支持时,我们考虑使用反射来查找所有接口并将它们添加到类型列表的末尾。我们遇到的问题是,在类型实现多个接口时定义接口的顺序。
我们还需要记住的另一件事是,反射不是很廉价,这会降低此场景的性能。
那么解决方案是什么?您无法在XAML中全部完成,但只需编写少量代码即可轻松完成。ItemsControl的ItemTemplateSelector属性可以用于选择要为每个项使用的DataTemplate。在模板选择器的SelectTemplate方法中,您将收到一个参数,即要模板化的项。在这里,您可以检查它实现了哪个接口,并返回与之匹配的DataTemplate。

14

简短回答是DataTemplate不支持接口(考虑到多重继承、显式与隐式等问题)。我们通常的解决方法是让事物扩展一个基类,以允许DataTemplate进行特定化/泛化。这意味着一个相当不错但不一定最佳的解决方案是:

public abstract class SomeClassBase
{

}

public class SomeClass : SomeClassBase
{

}

<DataTemplate DataType="{x:Type local:SomeClassBase}">
    <!-- ... -->
</DataTemplate>

3
谢谢 - 即使这意味着我必须重新调整很多东西来实现我的目标,我还是会接受你的答案。有时候,当你被要求在现有的业务对象库上放置一个 WPF UI 时,生活就有点糟糕了 :) - Rune Jacobsen
1
@Rune Jacobsen:我们的店铺也面临着同样的成长烦恼。 - user7116

9

你还有另一种选择。在DataTemplate上设置一个Key,然后在ItemTemplate中引用该Key。像这样:

<DataTemplate DataType="{x:Type documents:ISpecificOutcome}"
              x:Key="SpecificOutcomesTemplate">
    <Label Content="{Binding Name}"
           ToolTip="{Binding Description}" />
</DataTemplate>

然后,您可以通过键引用模板,以便在需要使用它的地方使用,就像这样:

<ListBox ItemsSource="{Binding Path=SpecificOutcomes}"
         ItemTemplate="{StaticResource SpecificOutcomesTemplate}"
         >
</ListBox>

Rendering


2
不幸的是,这意味着您必须明确告知每个绑定要使用哪个模板。这很烦人,因为在我的情况下,我想绑定到相同的接口属性,但是底层对象会在运行时更改。我不想明确告诉绑定只实现一个数据模板,因为我希望它根据支持该属性的底层对象在运行时更改该数据模板。 - Jason Ridge

8
由 dummyboy 提出的答案是最佳答案(在我看来应该投票选为顶部)。它确实存在一个问题,即设计师不喜欢它(会出现错误“对象 null 不能用作 PropertyPath 的访问器参数),但是有一个好的解决方法。解决方法是定义数据模板中的项,然后将模板设置为标签或其他内容控件。例如,我想这样添加一张图片:
<Image Width="120" Height="120" HorizontalAlignment="Center" Source="{Binding Path=(starbug:IPhotoItem.PhotoSmall)}" Name="mainImage"></Image>

但是它一直给我相同的错误。解决方法是创建一个标签,并使用数据模板来显示我的内容。
<Label Content="{Binding}" HorizontalAlignment="Center" MouseDoubleClick="Label_MouseDoubleClick">
    <Label.ContentTemplate>
        <DataTemplate>
            <StackPanel>
                <Image Source="{Binding Path=(starbug:IPhotoItem.PhotoSmall)}" Width="120" Height="120" Stretch="Uniform" ></Image>
            </StackPanel>
        </DataTemplate>
    </Label.ContentTemplate>
</Label>

这种方法有其不足之处,但对我来说似乎运作得相当顺利。

5
注意:如果接口属性位于路径内,您也可以使用更复杂的多部分路径,例如:
 <TextBlock>
    <TextBlock.Text>
        <Binding Path="Packages[0].(myNamespace:IShippingPackage.ShippingMethod).CarrierServiceCode"/>
    </TextBlock.Text>
 </TextBlock>

或者直接使用Binding指令。

 <TextBlock Text="{Binding Path=Packages[0].(myNamespace:IShippingPackage.ShippingMethod).CarrierServiceCode}"/>

或者当使用接口的多个属性时,您可以在本地重新定义DataContext以使代码更易读。

 <StackPanel DataContext={Binding Path=Packages[0].(myNamespace:IShippingPackage.ShippingMethod)}">
    <TextBlock Text="{Binding CarrierName}"/>
    <TextBlock Text="{Binding CarrierServiceCode}"/>
  </StackPanel>

提示:注意不要在路径表达式的末尾意外地以)}结尾。这是我经常犯的一个愚蠢的复制/粘贴错误。 Path="(myNameSpace:IShippingPackage.ShippingMethod)}"

确保使用Path=

发现如果我没有明确使用Path=,则可能无法解析绑定。通常我会写出类似这样的内容:
Text="{Binding FirstName}"

替代

Text="{Binding Path=FirstName}"

但是在更复杂的界面绑定中,我发现需要使用 Path= 来避免这个异常:

System.ArgumentNullException: Key cannot be null.
Parameter name: key
   at System.Collections.Specialized.ListDictionary.get_Item(Object key)
   at System.Collections.Specialized.HybridDictionary.get_Item(Object key)
   at System.ComponentModel.PropertyChangedEventManager.RemoveListener(INotifyPropertyChanged source, String propertyName, IWeakEventListener listener, EventHandler`1 handler)
   at System.ComponentModel.PropertyChangedEventManager.RemoveHandler(INotifyPropertyChanged source, EventHandler`1 handler, String propertyName)
   at MS.Internal.Data.PropertyPathWorker.ReplaceItem(Int32 k, Object newO, Object parent)
   at MS.Internal.Data.PropertyPathWorker.UpdateSourceValueState(Int32 k, ICollectionView collectionView, Object newValue, Boolean isASubPropertyChange)

即不要这样做:

<TextBlock Text="{Binding Packages[0].(myNamespace:IShippingPackage.ShippingMethod).CarrierServiceCode}"/>

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