为什么不能将IView(Of SpecificViewModel)转换为IView(Of ViewModelBase)?

时间:2013-03-15 03:49:27

标签: vb.net generics mvvm interface casting

这是MVVM + WPF,但实际上与这些工具没什么关系。问题更为通用,属于OO设计。

昨天MarcinJuraszek通过为original problem建议一个出色的解决方案帮助了我。该解决方案解决了手头的问题,但现在我陷入了下一个阶段。这是怎么回事:

的ViewModels

我的ViewModel类继承自公共抽象父ViewModel类:

Public MustInherit Class ViewModelBase
    Implements INotifyPropertyChanged, IDisposable

    ...

End Class

具体的ViewModel是这样的:

Class SalesOrderEntryViewModel
    Inherits ViewModelBase
    Implements IEditorViewModel, IChildViewModel

    ...

End Class

IEditorViewModelIChildViewModel是我的一些具体ViewModel实现的接口。

视图

我的所有View类都实现了以下界面:

Interface IView(Of T As ViewModelBase)
    WriteOnly Property MyVM As T
    ReadOnly Property HeaderText As String
End Interface

基于上述SalesOrderEntryViewModel的具体视图定义为:

Class SalesOrderEntryPage
    Implements IView(Of SalesOrderEntryViewModel)

End Class

到目前为止一切顺利。我现在面临的实际问题是我想在应用程序级别创建所有打开的视图的强类型集合。这个系列的类型应该是什么?我试过以下的东西:

Dim Views As List(Of IView(Of ViewModelBase))

当我尝试将SalesOrderEntryPage类的对象添加到此列表时,它会抛出一个运行时异常,告诉我它无法从SalesOrderEntryPage转换为IView(Of ViewModelBase),尽管定义SalesOrderEntryPage确实是IView(Of ViewModelBase)

目前,VB.NET正在帮我解决后期绑定问题,但我想知道为什么会这么说,这方面的优雅解决方案是什么?

2 个答案:

答案 0 :(得分:5)

您遇到的问题与泛型的使用有关,并且与泛型类型参数的 variance 概念有关。通用接口的类型参数可以是不变协变逆变

默认情况下,通用接口在其类型T中是不变的。这意味着,您可以使用T作为方法参数的类型,也可以使用返回类型的方法。在缺点方面,这意味着以下两种情况都不可能:

  

将IView(ViewModelBase)转换为IView(Of SalesOrderEntryViewModel)

     

将IView(Of SalesOrderEntryViewModel)转换为IView(Of ViewModelBase)

当你考虑:

时,这是有道理的
  1. 假设IView(Of T)有一个方法需要类型 T的参数:这意味着,IView(Of SalesOrderEntryViewModel)有一个方法,需要一个类型的参数SalesOrderEntryViewModel。但是如果你可以将IView(Of SalesOrderEntryViewModel)转换为IView(Of ViewModelBase),你会希望它有一个参数类型为ViewModelBase的方法,但它没有,因为该方法是针对参数设计的仅键入SalesOrderEntryViewModel而不是其通用版本或从ViewModelBase派生的其他ViewModel。结果:您无法将IView(Of SalesOrderEntryViewModel)转换为IView(Of ViewModelBase)。

    1. 假设IView(Of T)有一个方法,返回类型为T的值,并假设实现IView(Of ViewModelBase)的类上的方法将返回SalesOrderEntryViewModel,是的,因为SalesOrderEntryViewModelViewModelBase的一个实例。到现在为止还挺好。但是,如果您现在尝试将此类强制转换为IView(Of SomeOtherViewModel,则它不再起作用,因为您的方法要返回的类型SalesOrderEntryViewModel不是SomeOtherViewModel的实例。结果:您无法将IView(Of ViewModelBase)转换为IView(Of SalesOrderEntryViewModel)。

    2. 可是:

      围绕其中一个约束有一种方法,但你必须选择:

      您可以在T中创建界面逆变:这意味着您不能将T用作方法的返回类型,但可以将Interface(Of Base)强制转换为{ {1}}。

      Interface(Of Derived)

      或者您在Interface IContravariant(Of In A) Sub SetSomething(ByVal sampleArg As A) Sub DoSomething(Of T As A)() ' The following statement generates a compiler error. ' Function GetSomething() As A End Interface 中创建了协变界面:然后您不能拥有采用T类型参数的方法,但您可以将T投射到{ {1}}。

      Interface(Of Derived)

      这就是理论。


      针对您的特殊情况:

        

      当我尝试将SalesOrderEntryPage类的对象添加到此列表时,   它会抛出一个运行时异常,告诉我它无法转换   SalesOrderEntryPage到IView(Of ViewModelBase)

      因此,您希望从接口(Of Derived)转换为Interface(Of Base)。这意味着,您需要在其ViewModelType中设置接口Interface(Of Base)协变。因此,您无法通过Interface ICovariant(Of Out R) ' The following statement generates a compiler error ' because you can use only contravariant or invariant types ' in generic contstraints. ' Sub DoSomething(Of T As R)() End Interface 界面设置ViewModel。

      所以,它并没有真正解决问题,但我希望它能告诉你,为什么它不起作用,你想要做什么。

      我建议为所有视图定义逆变或非泛型基接口。我做了后者,虽然它不包含太多(非通用IView(模型)接口),但它使生活更容易。然后派生一个允许设置View的ViewModel的界面。这样,您就可以使用视图的IView版本填充您的馆藏。

答案 1 :(得分:0)

对于任何陷入这种情况并且愿意做反射的人来说,这是一个帮助方法,它使用反射信息来实现这一点:

    Private Function ImplementsGenericInterface(type As Type, interfaceName As String, genericArgTypeName As String) As Boolean
    Dim myFilter As New Reflection.TypeFilter(Function(typeObj As Type, criteriaObj As [Object])
                                                  Return typeObj.ToString().StartsWith(interfaceName)
                                              End Function)

    Dim MyIViewInterfaces() As System.[Type] = type.FindInterfaces(myFilter, "")

    If MyIViewInterfaces.Length > 0 Then
        Return MyIViewInterfaces.Any(Function(x) x.GetGenericArguments().Any(Function(y) y.BaseType.Name = genericArgTypeName))
    End If

    Return False
End Function

对于我在问题中发布的示例,请使用它:

ImplementsGenericInterface(objSalesOrderEntryPage.GetType(), "ProjectNamespace. Name", GetType(ViewModelBase).Name)