ViewModel应该如何引用其Models属性?

14
作为ViewModel的职责是“准备”Model的属性以在View中显示,那么从ViewModel中引用基础Model的属性的最佳方法是什么?
目前我能想到两个解决方案:

选项1 - 在ViewModel中复制Model的属性(包装器方法)

架构

class Model
{
    public string p1 { get; set; }
    public int p2 { get; set; }
}

class ViewModel : INotifyPropertyChanged
{
    // Model-instance for this ViewModel
    private Model M;

    public string p1 
    {
        get { return M.p1; }
        set 
        { 
            M.p1 = value; 
            // assuming View controls are bound to the ViewModel's properties 
            RaisePropertyChanged("p1");  
        }
    }

    // let's say, I only want to check a Checkbox in the View,
    // if the value of p2 exceeds 10.
    // Raising the property changed notification would get handled
    // in the modifying code instead of the missing setter of this property.
    public bool p2
    {
        get 
        { 
            if (M.p2 > 10)
            { return true; }
            else
            { return false; }
        }
    }

    // Initialize the Model of the ViewModel instance in its c'tor
    public ViewModel()
    { M = new Model(); }
}

绑定

<Textbox Text="{Binding p1}"/>
<Checkbox IsEnabled="False" IsChecked="{Binding p2, Mode=OneWay}"/>    

优点

  • 完全控制模型属性在视图上的显示方式,如 p2 所示: 按需将int 转换为bool
  • 与选项2相比,ViewModel 属性的更改可以单独引发,可能会稍微提高性能。

缺点

  • 违反 DRY
  • 需要编写/维护更多代码。
  • 对模型/ViewModel 进行修改很容易变成 shotgun surgery

选项二 - 将整个模型视为ViewModel的属性

架构

class Model
{
    public string p1 { get; set; }
    public int p2 { get; set; }
}

class ViewModel : INotifyPropertyChanged
{
    // Model instance for this ViewModel (private field with public property)
    private Model _M;
    public Model M 
    { 
       get { return _M; }
       set 
       {
           _M = value;
           // Raising the changing notification for the WHOLE Model-instance.
           // This should cause ALL bound View-controls to update their values,
           // even if only a single property actually got changed
           RaisePropertyChanged("M");
       } 
    }

    // Initialize the Model of the ViewModel instance in its ctor
    public ViewModel()
    { M = new Model(); }
}

绑定

<Textbox Text="{Binding M.p1}"/>
<Checkbox IsEnabled="False" IsChecked="{Binding M.p2, Mode=OneWay, Converter={StaticResource InverseBooleanConverter}"/>

优点

  • 可以节省大量代码。
  • 降低复杂性。
  • 增强可维护性。

缺点

  • 在这种方法中,ViewModel 只是 Models 属性的连续流水加热器,除了对 View 进行一些可能的交互逻辑之外。
  • 无法控制 Model 属性在 View 中的显示方式 - 最终导致 ViewModel 的完全不必要以及在 View 中实现转换逻辑。

1
为什么类型Model不被视为ViewModel?为什么不让它自己继承INotifyPropertyChanged? - Veverke
1
为了简单起见,所示代码当然不能运行,仅用于理解一般问题:如何通过ViewModel公开Model内的数据?是使用属性克隆还是声明Model实例作为可绑定属性?或者采用完全不同的方法? - M463
1
还可以看一下http://blog.alner.net/archive/2010/02/09/mvvm-to-wrap-or-not-to-wrap.aspx - Rico Suter
1
谢谢您提供有用的链接,这是一篇写得很好且直截了当的文章! :-)我在思考时意识到,我并不是第一个遇到这个问题的人。 - M463
@RicoSuter 作者说:“第二个选项更简单,似乎更实用。它说:‘我将在需要时在VM上添加属性,但我不会试图控制对模型的所有访问。’” 我认为他错了,你的ViewModel仅包含在View中显示所需的内容。 - nkoniishvt
一如既往:我认为这取决于情况。是否需要这种解耦取决于是否需要更快的开发/务实的方法... - Rico Suter
2个回答

10
您的ViewModel有责任将Model暴露给View,不应将Model的属性作为ViewModel的附加属性公开,而是应直接将View绑定到Model。此外,在Model中具有逻辑并不是错误的,事实上,将与Model相关的代码包含在Model中比将其放在ViewModel中更有意义。
以下是一个示例:
public class Movie
{
    private string _Name;

    public string Name
    {
        get { return _Name; }
        set
        {
            _Name = value;

            //Notify property changed stuff (if required)

            //This is obviously a stupid example, but the idea
            //is to contain model related logic inside the model.
            //It makes more sense inside the model.
            MyFavourite = value == "My Movie";
        }
    }

    private bool _MyFavourite;

    public bool MyFavourite
    {
        get { return _MyFavourite; }
        set
        {
            _MyFavourite = value;

            //Notify property changed stuff.
        }
    }
}

回答您的问题,更直接一点,您应将模型作为 属性 在视图模型中公开。

public class ViewModel
{
    private Movie _Model;

    public Movie Model
    {
        get { return _Model; }
        set 
        { 
            _Model = value;

            //Property changed stuff (if required)
        }
    }

    ...
}

因此,您的视图将绑定到模型属性,就像您已经这样做的那样。
编辑
在转换类型的示例中,您可以在模型中实现只读属性,如下所示:
public bool MyBool
{
    get 
    { 
        return MyInt > 10; }
    }
}

这里的关键是,每当 MyInt 改变时,您需要调用 INotifyPropertyChanged 来通知属性已更改。因此,您的其他属性将如下所示:

public int MyInt
{
   get { ... }
   set 
   {
       _MyInt = value;

       //Notify property changed for the read-only property too.
       OnPropertyChanged();
       OnPropertyChanged("MyBool");
   }
}

3
所以,基本上,您鼓励使用选项#2。但是,如果ViewModel的唯一目的是向View显示Model的实例,那么我为什么需要ViewModel呢?此外,在这种尝试中,模型属性的缺失准备问题(例如类型转换,请参见Option#1的优点部分中的M.p2示例)仍未解决。 - M463
4
是的,选项#2是正确的选择。通常情况下,视图需要绑定到视图模型,其中视图模型公开了模型。但这并不意味着在某些(非常罕见的)情况下,您不需要视图模型,此时您可以直接绑定到模型。请记住,MVVM是一种模式,而不是法律。关于类型转换,没有任何限制阻止您在模型中创建一个只读属性(仅具有get的属性),这将使您可以将逻辑保留在模型内部。或者,您可以像您已经做的那样使用转换器。 - Mike Eason
6
就我所知,这个答案似乎违反了MVVM的整个前提,让我非常困惑。ViewModel本应该隐藏模型,对吗?我知道模式并不等于法律,但问题明确询问如何遵循这种模式,所以正确答案应该解释如何遵循这种模式,而不是违背它。 - Le Mot Juiced
2
我必须在这里同意@LeMotJuiced的看法。比如说,我有一个服务从外部服务获取我的域对象并将数据呈现为IEnumerable<MyModel>。我不希望它知道它用于何处,实际上在大多数情况下,它不能知道,因为该模型位于一个没有对WPF(或任何其他视图)进行引用的独立库中。因此,在理想的情况下,模型应仅包含数据。ViewModel然后决定呈现给View的模型的哪个部分,并包含视图特定的逻辑,例如复选框的属性。 - Andre
请查看以下链接:https://softwareengineering.stackexchange.com/questions/123317/should-we-bind-view-to-a-model-property-or-viewmodel-should-have-its-own 和 https://docs.catelproject.com/vnext/introduction/mvvm/different-interpretations-of-mvvm/ - user1121956
显示剩余4条评论

2
在我看来,Model 不应该带有 RaisePropertyChanged 相关内容。一些视图模型(例如 Blazor)可能不需要它,而另一些视图模型(例如 WPF)可能使用其他机制,如 DependencyProperty。因此,对我来说,Model 是一个 POCO 类。因此,将更改报告到数据的责任落在了 ViewModel 上。因此,ViewModel 负责包装 Model 的属性(OA 的选项1)。
您可能希望查看 AutoMapper 来集中映射。

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