绑定到模型或视图模型

8

我知道已经有关于这个主题的问题了,但是那里的问题比较特定,没有提供决定性的答案。

尤其是这些问题:Question1Question2和当然还有Question3,所以请不要轻易关闭这个问题。那里的回答只是说“这样做,那样做”,而不是为什么!

有些人否认使用ViewModel的必要性,他们认为Model直接绑定是标准的方法。这是我否认并试图用技术论据证明的。

基于我的MVCMVPPresentation Model背景,对我来说使用ViewModel是很自然的。 我可能错过了一个重要的点吗?

因此,对我来说,默认情况下绑定到ViewModel,无论Model是什么(无论它是否实现了INotifyPropertyChanged)。

几个原因我认为绑定到ViewModel,包括(如这里提到的另一篇文章中提到的

1. 从视图中移除逻辑

  • 使逻辑可单元测试
  • 减少代码冗余(在需要重复时)

2. 安全性

  • 模型包含用户不应更改的属性
  • 如果绑定到模型,可能会发生自动但不想要的更新

3. 松耦合

  • 如果直接将模型绑定,则较低层与视图之间存在耦合
  • 更改模型会导致所有视图的更改
  • 视图不依赖于任何给定模型
  • 模型可以很容易地使用EF、某些DSL、批处理文件等生成

4. 开发速度

  • 你可以从一个原型视图模型层次结构开始绑定到它
  • 如果模型仍在开发中,您可以从一个原型模型开始
  • 无论视图如何,模型和视图模型都可以进行测试驱动开发
  • 视图可以完全由设计师或具有强大设计背景的开发人员构建

5. “棘手的同步”问题得到解决

  • 对于任何给定的“棘手的同步”问题,有很多解决方案,例如:
  • AutoMapper
  • 来自模型的事件系统(模型触发事件,ViewModel订阅)

6. 项目中结构相等

  • 有些地方必须使用ViewModel,例如SelectedItem
  • 混合绑定到Model和ViewModel容易出错
  • 新手开发人员更难弄清项目的结构
  • 当别无选择时才开始引入ViewModel会很混乱

7. 可扩展性

  • 让我们定义:如果您不使用ViewModel,则不是MVVM
  • MVVM可以轻松适应许多数据源,许多视图
  • 如果您发现任何性能问题:延迟加载和缓存进入ViewModel

8. 关注点分离

  • 掌握复杂性是软件中的主要问题
  • ViewModel的唯一责任是推送更改
  • 向视图发送通知与将其推送到不同的进程或机器一样容易
  • ViewModel而不是View在模型/数据源上注册更改通知
  • 数据源可以忽略向导致更改的ViewModel发送事件

相反,来自另一个线程的人提出了一些观点,包括:

  1. 如果直接更新模型,视图模型将不知道触发属性更改事件。这会导致UI不同步。这严重限制了在父子视图模型之间发送消息的选项。

  2. 如果模型具有自己的属性更改通知,则问题#1和2不是问题。相反,您必须担心包装器VM超出范围但模型没有时的内存泄漏。

  3. 如果您的模型很复杂,拥有许多子对象,则必须遍历整个树并创建一个阴影第一对象图。这可能非常繁琐且容易出错。

  4. 包装集合尤其难以处理。任何时候都有东西(UI或后端)从集合中插入或删除项目,阴影集合都需要更新以匹配。这种代码真的很难写对。

因此,问题是:默认的绑定方式是什么,为什么?

我是否错过了使使用ViewModel成为必要的要点?

是否有任何真正的原因希望绑定到模型?

最重要的是为什么,而不是如何。


“否认需要 ViewModel” 和从不直接绑定模型之间存在巨大的区别。 - Jonathan Allen
@JonathanAllen,我很期待看到您的观点,为什么您认为直接绑定模型是“默认情况”和“大多数代码应该遵循的模式”。 - Mare Infinitus
我刚刚发布了我的示例代码。如果你想改变我的想法,你必须演示一个不是一团糟的包装器。到目前为止,我还没有找到任何人能够做到这一点。 - Jonathan Allen
我想补充一下,三个小时过去了,我仍然是唯一一个发布了任何代码示例的人。 - Jonathan Allen
@JonathanAllen 可能这个问题不能通过发布一些代码示例来回答。实际上,这个问题不是关于代码,而是关于组织你的代码。 - Mare Infinitus
1
除非你能用代码支持你的观点,否则你所说的一切都没有意义。在你用实际例子证明你的想法之前,它只是一种猜测。 - Jonathan Allen
6个回答

6

视图模型通常包含成员,旨在与视图一起使用(例如,像IsSomethingSelectedIsSomethingExpandedIsSomethingVisible这样的属性,任何ICommand的实现)。

您是否认为将所有这些内容带入模型中有任何理由?当然不是。这就是为什么视图模型存在的原因。


已经编辑了问题,包括这一点。 - Mare Infinitus
2
没错。但这并不意味着模型永远不会与视图绑定。这只是意味着模型不是唯一绑定到视图的东西。 - Jonathan Allen
@JonathanAllen:当然。绑定到模型通常很方便。这个问题可以分成两部分:1)为什么要使用视图模型?2)应该绑定到模型吗?我已经回答了第一部分。 :) - Dennis

5
那么问题是:默认的绑定方式是什么,为什么?
一般情况下,我认为使用ViewModel并将其绑定是默认方式。这就是为什么“ViewModel”存在并成为MVVM模式的一部分的原因。
除了纯粹的数据之外,还有其他需要ViewModel的原因。您通常还要实现特定于应用程序的逻辑(即不属于模型但在应用程序中需要的逻辑)。例如,任何ICommand实现都应该在ViewModel上,因为它与Model完全无关。
有没有真正的理由想要将绑定到模型?
在某些情况下,这可能更简单,特别是如果您的模型已经实现了INotifyPropertyChanged。减少代码复杂性是一个有价值的目标,具有自己的优点。

另一个人认为ICommand离开了MVMM模式,而我并不认同。对我来说,它只是另一种扩展和封装。而且:绑定到模型和ViewModel不会比在整个项目中保持统一的设计更复杂吗? - Mare Infinitus
@MareInfinitus 是的 - ICommand 一直是 MVVM 的核心元素。 实际上,我会考虑那些试图完全删除 ICommand 的设计作为 MV-其他东西.. ;) - Reed Copsey
3
关于绑定到Model和ViewModel两者的问题 - 可以这样做,但通常会更简单。一个好的例子是ItemsControl - 将ViewModel公开一个ObservableCollection<ModelType> 并直接将其绑定到它,而不是试图构建包含封装模型的集合等,通常会更好。 - Reed Copsey
是的,有一些需要注意的地方,特别是在使用POCO模型和单向绑定时。但即使在这种情况下,我通常也会进行封装。有时我会保留对模型的引用。您是否看到了我在问题中没有提到的要点? - Mare Infinitus
6
并不完全是这样——最终,项目中的建筑师会根据自己的判断来决定。没有"唯一正确的方式"来做这件事。 - Reed Copsey
但我看到很多理由选择一种方式而不是另一种。 - Mare Infinitus

3

反驳:

  1. 从视图中移除逻辑

将验证、计算字段等逻辑推入模型中同样有用。这样做可以让你保持一个更轻量、更清晰的视图模型。

•使逻辑可单元测试

模型本身非常容易进行单元测试。与处理外部服务的视图模型不同,你不必担心模拟库等问题。

•减少代码冗余(需要时避免重复)

多个视图模型可以共享同一模型,从而减少验证、计算字段等方面的冗余。

  1. 安全性 •模型包含用户不应更改的属性

那就不要在 UI 上公开它们。

•如果绑定到模型,则可能会发生自动但不需要的更新

这一点毫无意义。如果你的 VM 只是一个围绕模型的包装器,它只会将这些更新向下推送。

  1. 松散耦合 •如果直接绑定到模型,则较低层和视图之间会存在耦合

当你在它们之间添加包装器 VM 时,这种耦合并不会神奇地消失。

•更改模型会导致所有视图的更改

更改模型会导致所有包装器视图模型的更改。更改视图模型也会导致所有视图的更改。因此,模型仍然可能导致所有视图的更改。

•视图不依赖于任何特定的模型

无论有没有视图模型围绕模型,这都是正确的。它只看到属性,而不是实际的类。

•模型可以使用 EF、某些 DSL、批处理文件等轻松生成

没错。通过一点工作,这些轻松生成的模型可以包括有用的接口,如 INotifyDataErrorInfo、IChangeTracking 和 IEditableObject。

  1. 开发速度

绑定到模型可以提供更快的开发速度,因为你不必映射所有属性。

•你可以从原型视图模型层次结构开始,并绑定到那里

或者我可以从原型模型开始。添加包装器没有任何收益。

•如果模型仍在开发中,则可以从原型模型开始 •无论视图如何,都可以使用测试驱动的方式开发模型和视图模型

同样地,添加一个模型的包装器并不能带来任何好处。

•视图可以完全由设计师或具有强大设计背景的开发人员构建

再一次,将模型添加到包装器中并没有任何收获。
“棘手的同步”问题得到了解决,有很多解决方法,例如AutoMapper。但如果你使用自动映射将数据复制到视图模型中,则并未使用MVVM模式,而只是使用了Views和Models。
从模型发出事件,ViewModel订阅事件的事件系统容易造成内存泄漏,除非你非常小心,并放弃在多个视图之间共享模型的能力。
在整个项目中保持相等的结构不太相关。没有人反对非包装器视图模型。
混合绑定模型和视图模型容易出错,但不被支持。
对于新开发人员来说,弄清项目结构更加困难也不被支持。如果没有其他选择,后期引入ViewModel会显得混乱,但这也不太相关。
大规模可扩展性方面,如果你不使用ViewModel,则它就不是MVVM。但这也不被支持。
MVVM可以轻松适应多种数据源和多个视图,但这也不太相关。我们并不争论是否使用MVVM,而是争论如何最好地使用它。
如果发现性能问题,惰性加载和缓存应该放在ViewModel中,这是有共识的。但没有人建议将服务调用塞进模型中。
分离关注点是包装器视图模型最大的缺陷所在。视图模型已经处理了UI数据(例如模式、选定项),并托管调用外部服务的ICommands。如果将所有模型数据、验证逻辑、计算属性等都塞进视图模型中,就会使其变得更加臃肿。

1
请注意,我没有评论家徽章。最终,我会回答所有这些问题。 - Mare Infinitus
当VM从Model订阅事件时,您会警告内存泄漏(在相关文章中也是如此)。请详细说明。在我的设计中(我认为是典型的基于桌面的应用程序),View、Model和Model View具有相同的生命周期。我不明白这种安排如何容易受到泄漏的影响。您能启发我吗? - kmote
在 MVVM 设计模式下,一个单一的 Model 可以同时共享多个视图。例如,该模型可以出现在列表视图中,并且出现在显示选定项目详细信息的单独窗口中。如果您100%确信只有一个 VM 会看到给定的模型,并且100%确信将来永远不会改变,那么您可以让 VM 听取模型上的事件。 - Jonathan Allen

2
你的问题没有一个“正确”的答案。当然,WPF可以很愉快地让你绑定到任何你自己定义的“Model”对象;框架只是不关心。你并不总是需要遵循MVVM模式,仅仅因为你在做一个WPF应用程序。对于你编写的任何软件来说,上下文始终是关键。如果你时间紧迫,需要快速完成原型,则可以先绑定到Model,以后再进行重构。
所以我想你真正想问的是“何时应该使用MVVM模式?”
答案当然是,“当你的问题符合该模式时”。
那么MVVM模式给你什么呢?你已经列出了多个使用MVVM的理由,但最重要的理由是松耦合——所有其他的理由都源于这一点。
MVVM模式的整个意义在于确保你的模型是一个状态机,它对如何将数据呈现给用户或从用户那里获取数据一无所知。在某种程度上,你的模型是纯数据,结构化为对模型有意义而不是对人类有意义的格式。你的ViewModel负责在纯数据平台(模型)和用户输入平台(视图)之间进行翻译。ViewModel与视图和模型之间是紧密耦合的,但重要的是模型不知道视图。

1
你是对的,松耦合似乎是核心要点。 - Mare Infinitus
关于重构,我认为你可能误解了。我的意思是,如果你没有时间编写一个视图模型,可以直接将应用程序绑定到模型上构建,随时可以在你需要的时候进行重构,甚至可以选择不进行重构。我并不是要求你只为重构而重构。 - Randolpho
关于框架和开发速度,我会说做适合自己的事情,但我会警告不要仅仅因为速度而去做任何事情。有时现在多付出一点额外的努力,六个月后会得到很大的回报。 - Randolpho
关于何时使用MVVM,我个人更喜欢基于领域的设计方法,其中Model代表了您的产品试图完成的通用、"纯粹"或抽象定义。当然,并非所有问题都适合这种模型,但是我处理的大多数问题都适合。在我的经验范围内,UI呈现了更大系统的一小部分视图。当您遇到这种情况时,MVVM是一个非常有用的模式,因为它通过将这种复杂性推入ViewModel来帮助减少更大系统的复杂性。 - Randolpho
如果您没有关闭问题,可能会出现更多方面。这不是讨论,而是一个相当复杂的问题。回到主题:时间压力的论点是您所说的绑定模型的原因。对我来说,模型是应用程序的业务方面,但以抽象形式呈现。将任何将其带入视图的内容放入视图模型中。使视图想要与模型交互的任何内容都会进入ViewModel(例如命令、SelectedItem、查询)。但这只是我的口味(在问题中给出了原因)。想看到其他方面。 - Mare Infinitus
显示剩余2条评论

1

这是一个简单的对象图。只有一些非常简单的模型,具有正常的属性更改和验证事件。

那些认为模型需要包装在视图模型中的人,请展示你们的代码。

public class ModelBase : INotifyPropertyChanged, INotifyDataErrorInfo
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));

        OnErrorChanged(propertyName);
    }

    protected void OnErrorChanged(string propertyName)
    {
        if (ErrorsChanged != null)
            ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
    }

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public virtual IEnumerable GetErrors(string propertyName)
    {
        return Enumerable.Empty<string>();
    }

    public virtual bool HasErrors
    {
        get { return false; }
    }
}

public class Customer : ModelBase
{
    public Customer()
    {
        Orders.CollectionChanged += Orders_CollectionChanged;
    }

    void Orders_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        if (e.OldItems.Count > 0)
            foreach (INotifyPropertyChanged item in e.OldItems)
                item.PropertyChanged -= Customer_PropertyChanged;

        if (e.NewItems.Count > 0)
            foreach (INotifyPropertyChanged item in e.NewItems)
                item.PropertyChanged += Customer_PropertyChanged;

        OnPropertyChanged("TotalSales");
    }

    void Customer_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "Total")
            OnPropertyChanged("TotalSales");
    }

    public decimal TotalSales
    {
        get { return Orders.Sum(o => o.Total); }
    }

    private string _FirstName;
    public string FirstName
    {
        get { return _FirstName; }
        set
        {

            if (_FirstName == value)
                return;
            _FirstName = value;
            OnPropertyChanged();
        }
    }

    private string _LastName;
    public string LastName
    {
        get { return _LastName; }
        set
        {

            if (_LastName == value)
                return;
            _LastName = value;
            OnPropertyChanged();
        }
    }



    private readonly ObservableCollection<Order> _Orders = new ObservableCollection<Order>();
    public ObservableCollection<Order> Orders
    {
        get { return _Orders; }
    }

}

public class Order : ModelBase
{
    public Order()
    {
        OrderLines.CollectionChanged += OrderLines_CollectionChanged;
    }

    void OrderLines_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        if (e.OldItems.Count > 0)
            foreach (INotifyPropertyChanged item in e.OldItems)
                item.PropertyChanged -= OrderLine_PropertyChanged;

        if (e.NewItems.Count > 0)
            foreach (INotifyPropertyChanged item in e.NewItems)
                item.PropertyChanged += OrderLine_PropertyChanged;

        OnPropertyChanged("Total");
        OnErrorChanged("");
    }

    public override bool HasErrors
    {
        get { return GetErrors("").OfType<string>().Any() || OrderLines.Any(ol => ol.HasErrors); }
    }

    void OrderLine_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "Extension")
            OnPropertyChanged("Total");
    }

    public decimal Total
    {
        get { return OrderLines.Sum(o => o.Extension); }
    }

    private int _OrderNumber;
    private DateTime _OrderDate;

    public DateTime OrderDate
    {
        get { return _OrderDate; }
        set
        {
            if (_OrderDate == value)
                return;
            _OrderDate = value;
            OnPropertyChanged();
        }
    }
    public int OrderNumber
    {
        get { return _OrderNumber; }
        set
        {
            if (_OrderNumber == value)
                return;
            _OrderNumber = value;
            OnPropertyChanged();
        }
    }

    private readonly ObservableCollection<OrderLine> _OrderLines = new ObservableCollection<OrderLine>();
    public ObservableCollection<OrderLine> OrderLines
    {
        get { return _OrderLines; }
    }

}

public class OrderLine : ModelBase
{
    private string _ProductName;
    private decimal _Quantity;
    private decimal _Price;
    public decimal Price
    {
        get { return _Price; }
        set
        {
            if (_Price == value)
                return;
            _Price = value;
            OnPropertyChanged();
        }
    }
    public string ProductName
    {
        get { return _ProductName; }
        set
        {
            if (_ProductName == value)
                return;
            _ProductName = value;
            OnPropertyChanged();
            OnPropertyChanged("Extension");
        }
    }
    public decimal Quantity
    {
        get { return _Quantity; }
        set
        {
            if (_Quantity == value)
                return;
            _Quantity = value;
            OnPropertyChanged();
            OnPropertyChanged("Extension");
        }
    }
    public decimal Extension
    {
        get { return Quantity * Price; }
    }

    public override IEnumerable GetErrors(string propertyName)
    {
        var result = new List<string>();

        if ((propertyName == "" || propertyName == "Price") && Price < 0)
            result.Add("Price is less than 0.");
        if ((propertyName == "" || propertyName == "Quantity") && Quantity < 0)
            result.Add("Quantity is less than 0.");

        return result;
    }

    public override bool HasErrors
    {
        get { return GetErrors("").OfType<string>().Any(); }
    }
}

这里是一个典型的 ViewModel:

public class CustomerViewModel : ModelBase
{
    public CustomerViewMode()
    {
        LoadCustomer = null; //load customer from service, database, repositry, etc.
        SaveCustomer = null; //save customer to service, database, repositry, etc.
    }

    private Customer _CurrentCustomer;

    public Customer CurrentCustomer
    {
        get { return _CurrentCustomer; }
        set
        {
            if (_CurrentCustomer == value)
                return;
            _CurrentCustomer = value;
            OnPropertyChanged();
        }
    }
    public ICommand LoadCustomer { get; private set; }
    public ICommand SaveCustomer { get; private set; }

}

一个简单的例子,一个简单的想法:你想让你的模型支持多语言吗? - Mare Infinitus
关于您在问题中的评论:这个模型难以包装,因为它有一些主要缺陷。1. 与语言有关,2. 派生属性,3. 混合的错误处理和数据存储,4. 未归一化(订单中的产品名称?),5. 不完整(您的库存检查在哪里?)。它足够大,可以看到一些缺陷,但又足够小,可以说“嘿,只是一个示例”。这并不是一个真正好的讨论开端。我会为该模型制作一些 POCO 类,并定义一组规则用于错误处理。任何派生属性和语言字符串都放在 ViewModel 中。不需要显示代码! - Mare Infinitus
1
@MareInfinitus - 正如我所想,你做不到。你的设计只适用于简单的数据传输对象。如果展示使用领域驱动设计实践创建的实际模型,你就会迷失方向。 - Jonathan Allen
完全不是。我的模型是POCO,通常会自动生成。对我来说,您的模型存在瑕疵,我甚至从未考虑过这种方法。我可以为此编写一个包装器,但不明白为什么我应该这样做,相反,我将重写模型并引入一个视图模型,时间大约相同。但有趣的是,您提出了处理复杂性的方式(似乎是“一种模型来控制所有”)。我在考虑对那个问题设置赏金。 - Mare Infinitus
不,如果有意义的话,我在使用DTO(数据传输对象)时经常使用。例如,如果我使用WCF(Windows Communication Foundation)。但是再次强调,这些都是自动生成的。 - Mare Infinitus
显示剩余3条评论

1

我同意Reed的观点 - ViewModel是应该绑定的对象。我总是把Model想象成一个或多个相对静态的值集,其变化频率不如ViewModel那样频繁或动态。通常,我会将任何可以在编译时假定值的东西放在Model中,将在运行时确定的内容放在ViewModel中。

View本身不应该有任何超出最基本逻辑的东西。剩下的都应该是对ViewModel的引用。安全性有时是问题,但我喜欢这样做只是为了代码可读性和简洁性。当所有美学方面都在View中完成,所有更数学、逻辑的事情都隐藏在ViewModel中,所有硬数据都在单独的Model中时,处理代码就容易得多。

MVVM也与MVC密切相关,其指导原则是Model和View不应直接看到彼此。对我来说,这又是一个清晰度问题。决定Model值如何改变的逻辑也应该在ViewModel/Controller中。View不应该自作聪明。

把View想象成一个接待员:它是一个友好的面孔,与用户进行交互。ViewModel是办公室里面会计师,就在前台旁边的门后面,而Model则是他/她的参考书和笔记集。如果接待员开始在会计师的书中写注释、擦掉会计师的笔记并更改记录,事情就会变得混乱。


不错的例子。但是模型可能会因为任何原因和来源而发生变化。但我也遵循编译时/运行时的思路。并且:如果它对另一个应用程序(如任何数据源)有用,它将成为模型的一部分。 - Mare Infinitus
-1 是因为认为模型是“更或多或少静态的”这一建议代表了对模型开发异常狭隘的理解。 - kmote

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