8

我知道已经有关于该主题的问题,但是那里的问题有些特定于其他问题,并且不提供结论性的答案。

尤其是这里的问题:Question1Question2,当然还有Question3 所以请不要太快关闭这个问题。他们在那里回答只是说“做这个,做那个”而不是为什么!

有些人否认需要 aViewModel并说“标准”方式是直接绑定到 Model。这是我否认并试图用技术论据来证明的。

从我在MVC, MVP,的背景来看,我Presentation Model很自然地使用ViewModel. 也许我错过了一个重要的点?

所以对我来说,默认是绑定到 a ViewModel,不管它Model是什么(也不管它是否实现INotifyPropertyChanged)。

我看到绑定到s 的原因有几个ViewModel,包括(如CodeProject另一篇文章所述

1. 从视图中移除逻辑

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

2. 安全

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

3.松耦合

  • 如果直接绑定到模型,下层和视图之间会有耦合
  • 更改模型会导致所有视图发生变化
  • 视图不依赖于任何给定的模型
  • 可以用EF、一些DSL、批处理文件等轻松生成模型

4、发展速度

  • 您可以从Prototype ViewModel层次结构开始并绑定到该层次结构
  • 如果模型仍在开发中,您可以从Prototype Model
  • Model并且ViewModel可以开发测试驱动,无论视图如何
  • View完全可以由设计师或具有强大设计背景的开发人员构建

5.“棘手的同步”解决了

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

6. 整个项目的平等结构

  • 有些地方必须使用 ViewModel,例如 SelectedItem
  • 混合绑定到模型和视图模型很容易出错
  • 新开发人员更难弄清楚项目的结构
  • 以后无路可走的时候开始带 ViewModel 很乱

7. 可扩展性

  • 让我们定义:如果你不使用 ViewModel,它就不是 MVVM
  • MVVM 可以很容易地被大量数据源、大量视图采用
  • 如果您发现任何性能问题: ViewModel 中的延迟加载和缓存

8. 关注点分离

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

相反,来自另一个线程的家伙转储了一些要点,包括

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

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

  3. 如果您的模型很复杂,有很多子对象,那么您必须遍历整个树并创建第二个对象图来遮盖第一个对象图。这可能非常乏味且容易出错。

  4. 包装的集合特别难以使用。任何时候(UI 或后端)从集合中插入或删除项目时,影子集合都需要更新以匹配。这种代码真的很难正确。

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

我是否错过了必须拥有 ViewModel 的要点?

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

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

4

6 回答 6

6

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

你觉得有什么理由把所有这些东西都带进模型吗?当然不。这就是视图模型存在的原因。

于 2013-06-05T18:29:38.670 回答
5

So, the question is: what is the default way to bind and why?

In general, I would argue that having a ViewModel, and binding to it, is the default. There's a reason that "ViewModel" exists and is part of the MVVM pattern.

There are other reasons a ViewModel is necessary other than purely the data. You also typically implement application-specific logic (ie: not part of the model, but required in the application). Any ICommand implementation, for example, really should be on a ViewModel, since it's completely unrelated to the Model.

Are there any real reasons one would like to bind to a model?

It can be simpler in some cases, especially if your model already implements INotifyPropertyChanged. Reducing code complexity is a valuable goal with its own merit.

于 2013-06-05T18:25:08.670 回答
3

相反的论点:

  1. 从视图中删除逻辑

从视图模型中删除逻辑同样有用。通过将验证、计算字段等逻辑推送到模型中,您将获得一个更轻、更清晰的视图模型。

•使逻辑单元可测试

模型本身非常容易进行单元测试。您不必担心模拟库之类的事情,就像您使用处理外部服务的视图模型一样。

•减少代码冗余(在需要的地方重复)

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

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

然后不要在 UI 上公开它们。

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

这没有任何意义。如果您的 VM 只是模型的包装器,那么无论如何它只会将这些更新下推。

  1. 松耦合 •如果直接绑定到模型,下层和View之间会有耦合

当您在它们之间插入一个包装虚拟机时,这种耦合不会神奇地消失。

•更改模型会导致所有视图发生变化

更改模型会导致所有包装视图模型发生变化。更改视图模型也会导致所有视图的更改。因此,模型仍然可以引起所有视图的变化。

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

无论是否有包装模型的视图模型都是如此。它只看到属性,而不是实际的类。

•模型可以很容易地用EF、一些DSL、批处理文件等生成

是的。通过一些工作,这些容易生成的模型可以包含有用的接口,如 INotifyDataErrorInfo、IChangeTracking 和 IEditableObject。

  1. 发展速度

绑定到模型提供了更快的开发,因为您不必映射所有属性。

•您可以从原型 ViewModel 层次结构开始并绑定到该层次结构

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

•如果模型仍在开发中,您可以从原型模型开始 •模型和视图模型可以测试驱动开发,无论视图如何

同样,通过在模型周围添加包装器没有任何好处。

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

再一次,通过在模型周围添加包装器没有任何好处。

  1. “棘手的同步”得到解决 •对于任何给定的“棘手的同步”问题,有很多解决方案,例如 •AutoMapper

如果您使用自动映射器将数据复制到视图模型中,那么您就没有使用 MVVM 模式。您只是在使用视图和模型。

•来自模型的事件系统(模型触发事件,ViewModel 订阅)

你好内存泄漏。也就是说,除非您非常小心并放弃跨多个视图共享模型的能力。

  1. 整个项目的结构相同 •有些地方必须使用 ViewModel,例如 SelectedItem

无关紧要。没有人反对非包装视图模型。

• 混合绑定到模型和视图模型容易出错

不支持。

• 新开发人员更难弄清楚项目的结构

不支持。

• 无路可走的时候开始带 ViewModel 很乱

无关紧要。同样,没有人反对也使用非包装视图模型。

  1. 可扩展性 •让我们定义:如果你不使用 ViewModel,它就不是 MVVM

无关紧要。第三次,没有人反对也使用非包装视图模型。

•MVVM 可以很容易地被大量数据源、大量视图采用

无关紧要。我们不是在争论是否使用 MVVM,而是在争论如何最好地使用它。

•如果您发现任何性能问题:延迟加载和缓存进入 ViewModel

同意,但无关紧要。没有人建议您将服务调用推送到模型中。

  1. 关注点分离

这是包装视图模型最难崩溃的地方。

视图模型已经必须处理 UI 数据(例如模式、选定项)并托管调用外部服务的 ICommand。

将所有模型数据、验证逻辑、计算属性等推入视图模型会更加臃肿。

于 2013-06-05T21:57:26.420 回答
2

您的问题没有“正确”的答案。当然,WPF 会很高兴地允许您绑定到您已声明为“模型”对象的任何内容;框架根本不在乎。您不必总是仅仅因为您在 WPF 中执行应用程序就必须遵循 MVVM 模式。上下文始终是您编写的任何软件的关键。如果您时间紧迫并且需要快速完成原型,请务必绑定到模型并在需要时进行重构。

所以我认为你真正要问的是“我什么时候应该使用 MVVM 模式?”

答案当然是“当你的问题符合模式时”。

那么 MVVM 模式给了你什么?您已经列出了使用 MVVM 的几个原因,但该模式最重要的一个是松散耦合——所有其他的都与此相关。

MVVM 模式的全部意义在于确保您的模型是一个状态机,它对如何向用户呈现或从用户获取数据一无所知。在某种程度上,您的模型是以对模型有意义的格式构造的纯数据,而不是以对人类有意义的格式构造的。您的 ViewModel 负责在纯数据平台(模型)和用户输入平台(视图)之间进行转换。ViewModel 与 View 和 Model 紧密耦合,但重要的是 Model 对 View 一无所知。

于 2013-06-06T02:23:35.410 回答
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; }

}
于 2013-06-05T20:24:43.300 回答
1

我同意 Reed 的观点——ViewModel 是人们应该绑定的。我总是想象模型是一组或多或少的静态值,可能不会像 ViewModel 那样频繁或动态地变化。通常,我尝试将任何具有可在编译时假定的值的值放入模型中,以及将在运行时确定的任何值放入 ViewModel 中。

View 本身不应该有任何比最简单的基本逻辑更多的东西。其余的应该是对 ViewModel 的引用。安全有时是个问题,但我喜欢这样做只是为了代码的可读性和简洁性。当所有美学的东西都在视图中完成时,处理代码要容易得多,更多的数学、逻辑的东西都隐藏在 ViewModel 中,所有的硬数据都在一个单独的模型中。

MVVM 也与MVC密切相关,其指导原则是模型和视图不应该直接看到对方。再一次,对我来说,这是一个清晰的事情。决定模型值应该如何改变的逻辑也应该在 ViewModel/Controller 中。视图不应该自己思考。

把 View 想象成一个接待员:它是一个友好的面孔,可以与用户交互(“对话”)。ViewModel 是前台门后办公室里的会计,Model 是他/她的一套参考书和笔记。如果接待员开始在会计师账簿的空白处书写,擦除会计师的笔记,并更改记录中的内容,事情就会开始变得混乱。

于 2013-06-05T18:49:18.730 回答