65.9K
CodeProject 正在变化。 阅读更多。
Home

MVVM 和 BaseViewModel 基础

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.25/5 (4投票s)

2015年9月1日

CPOL

7分钟阅读

viewsIcon

25305

为全局架构建立 MVVM 基础。

引言

本文介绍如何设置 MVVM 层和交互,以简化框架和全局架构的开发。

背景

本文并非 MVVM 入门教程,而是面向实际应用环境。

层间交互

层间交互的关键原则是单一职责。虽然一般的架构指南并未严格规定层间交互,但我发现在 MVVM 中,如果你不坚持单一层、仅向下模式,你将不可避免地混淆职责。

虽然大多数使用 MVVM 的人可能知道这些层是什么,但我经常看到它们的职责混淆。所以我想做的第一件事是定义每一层的总体目的和交互,以及它们不应该是什么......

这些层是:

Model - 服务和数据层交互。

View Model - 组合 UI 交互所需数据的业务逻辑。

View - 控件和呈现逻辑。

这些层不是:

Model - 你的视图模型层的属性支持。

View Model - 控制所有控件逻辑的地方。

View - 除了 XAML 之外没有任何代码。

你不希望你的模型层作为属性支持,因为视图模型是一个可编辑和可组合的层。这样做会让你的模型层处于潜在的脏状态,但最好的情况是你必须同时管理你的模型层和视图模型层。

对于从 WinForms 或其他技术转向 WPF 的开发人员来说,本能的反应是将以前视图对象中的所有内容都塞进视图模型中。虽然视图不应该了解业务逻辑,但业务逻辑也不应该了解诸如哪些控件是只读的或什么是可见的等等。你得到的不是视图怪兽,而是视图模型怪兽,这违背了目的。

通过良好分离的职责,你需要在这些层之间添加一些“粘合剂”,特别是对于企业级软件。对于这第一篇文章,我不会深入探讨“粘合剂”,但也不想让你不知所措。

如果你的视图需要与业务逻辑交互,它应该通过命令进行。命令被很好地确立为视图模型和视图之间的“粘合剂”。它有时确实会模糊一些视图和视图模型的职责,但它很好地限制了交互范围,并且仍然促进了单一职责。

服务通信的方式一直在变化,而且更加多样化。最新的焦点是异步通信和发布/订阅方法,但没有普遍接受的做法。我更喜欢发布/订阅方法,因为除了异步之外,发布/订阅还很好地支持松耦合和抽象。

异步通信对于视图模型尤其重要,这样长时间运行的业务操作可以在不阻塞调度程序线程的情况下发生。在发布/订阅中,例如,保存对象的请求可以由任何接收消息的人发送和处理。每当操作完成时,视图模型或任何其他监听器都可以相应地响应。它还有助于促进单一职责,因为监听器可以从各种消息源汇总到公共处理。

命令和发布/订阅的复杂性确实需要更多关注,但关键是存在技术和方法可以在不混淆职责的情况下在这些层之间进行通信。

使用代码

/// <summary>
/// Basic view model functions.
/// </summary>
public abstract class BaseViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public virtual bool IsNew { get; protected set; }

    public virtual bool IsDirty { get; set; }

    protected virtual void RaisePropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

/// <summary>
/// Basic view model which wraps a model object.
/// </summary>
public abstract class BaseViewModel<T> : BaseViewModel
{
    public BaseViewModel(T model)
        : base()
    {
        Model = model;
    }
 
    /// <summary>
    /// Gets or sets if the view model houses a new object.
    /// </summary>
    /// <remarks>A view model is designated as new by the model object not previously existing.</remarks>
    public override bool IsNew
    {
        get
        {
            return Model == null;
        }

        protected set
        {
            throw new NotImplementedException();
        }
    }

    public T Model { get; protected set; }

    /// <summary>
    /// Creates a new model object that can be used for persistence so the underlying model object doesn't get corrupted.
    /// </summary>
    /// <returns>The new model object to persist or pass.</returns>
    /// <remarks>NOTE:  This should start by creating a new model object and not use the underlying model!</remarks>
    public virtual T ToModel()
    {
        throw new NotImplementedException();
    }

    /// <summary>
    /// Reloads the view model with the specified model.
    /// </summary>
    /// <param name="model">The updated model object.</param>
    /// <remarks>NOTE:  Separation must be kept between the view model and model.  Changes by the UI should not impact the
    /// model object.  Any updates or saves that are success should come back through services to the view model.</remarks>
    public virtual void Reload(T model)
    {
        // The override implementation should set view model properties to null in order to reload
        // the values in the UI, call this base method, then raise property changed events.
        Model = model;
        IsDirty = false;
    }
}

 

 

显然,在某些情况下,视图模型不会包装单个模型,例如复合视图模型、集合和对象图;然而,创建的大多数视图模型对象都将包装一个模型,所以我想专注于这个特定的类(如果对本文有足够的兴趣,我也会写关于其他类的文章)。

模型和视图模型的清晰分离提供了一些非常方便的功能,包括检测它是否是一个新对象、检测脏字段以及重新加载/撤消更改。为了支持这些功能,具体实现都应该具有延迟加载属性和适当的属性支持。

例如,你的模型对象中有一个 Name 字段。视图模型中应该有一个相应的 Name 属性。在初始化时,使用 Model.Name 加载 Name 属性。如果你想查看它是否是脏的,你可以将 Model.Name 与 Name 进行比较。如果你想撤消更改或能够重新加载值,你所要做的就是将 Name 设置为 null,然后它将重新加载模型值。重新加载功能对于可以从多个客户端进行实时更新的应用程序来说非常有用。

下面是具体实现的示例

public class WidgetViewModel : BaseViewModel<Widget>
{
    public WidgetViewModel(Widget widget)
        : base(widget)
    {
 
    }
 
    private string _name;
    public string Name
    {
        get
        {
            if (_name == null && Model != null)
            {
                _name = Model.Name;
            }
 
            return _name;
        }
 
        set
        {
            if (_name != value)
            {
                _name = value;
                RaisePropertyChanged("Name");
            }
        }
    }

    public void Undo()
    {
        Reload(Model);
    }

    public override void Reload(Widget model)
    {
        _name = null;

        base.Reload(model);

        RaisePropertyChanged("Name");
    }
 
    public override Widget ToModel()
    {
        var widget = new Widget();
        widget.Name = Name;
 
        return widget;
    }
}

ToModel() 方法旨在将编辑过的视图模型对象转换为模型层对象,然后服务和数据层可以使用该对象。不破坏底层模型对象非常重要,因为采取的动作可能会被驳回。例如,如果你尝试保存一个对象,但它被驳回为无效或无法保存。如果你修改了对象图中的模型对象,那么你现在有一个需要处理的脏对象。在这种架构中,你只需放弃视图模型或重新加载,然后就搞定了。

你希望服务专门处理模型对象的原因是,视图模型固有地与视图耦合,而服务逻辑不应该与表示有任何关联。即使你的代码很好地分离了视图和视图模型的职责,视图模型也是专门为容纳视图中所需的信息而创建的。当在服务层中使用这种数据视图时,很容易破坏服务的可伸缩性和灵活性。

视图模型对象也固有地臃肿,因此传输成本可能更高。例如,如果我的模型对象中有一个指向依赖实体 Foreign Key,我只需传递 Key 字段。在视图模型对象中,你可能拥有该实体的另一个视图模型,其中可能包括名称、描述、备注等。换句话说,一大堆你不需要发送的数据。

另一个重要注意事项是这些基本视图模型中不包含什么。请注意,虽然 IsReadOnly 是一个常见的属性,但它不是视图模型中的属性。它是一个视图属性!控件是只读的,而不是实体。

实体可能具有权限、工作流和其他访问控制,这些控制直接关系到控件是否只读,但这些不应与业务逻辑混淆。如果你将这些都放在一起,很快你的服务将使用 IsReadOnly 字段来确定要调用哪些方法,这很糟糕!

关注点

遵守这些准则有助于你继续编写简洁、可重用的代码。有几个关键概念可以在不严格交互的情况下实现;然而,严格执行这些准则将有助于防止陷入陷阱。你可能认为在这里或那里放松交互不会有什么坏处,但相信我,它会给你带来麻烦。

不担心对象膨胀,因为你的流量不大或者你的硬件很强大?这足够公平了,但是如果它超出预期(顺便说一下,这种情况经常发生)呢?更不用说传递依赖实体可能导致客户端和缓存之间的同步问题。

花时间将视图控件从视图模型中抽象出来。是的,这需要更多的工作......但最终它将帮助你构建更可重用的控件。

假设你正在加载历史数据,因此你需要将日期范围传递给服务调用。将控件绑定到该视图模型的开始日期和结束日期属性非常容易;然而,日期范围控件是一个具有大量重用价值的绝佳控件。更不用说用户请求与实际实体没有任何关系,因此如果你将它们都放在同一个视图模型中,那就是糟糕的视图模型设计。

无论那些随心所欲、非模式导向的开发人员说什么,对我来说,花时间正确地规划和设计 MVVM 层总是值得的。从一切都这样做开始,培养你的风格,然后在你积累了一些经验后,根据需要开始收回。

更多内容,也许吧......

这是我第一次写 Code Project 文章,所以请告诉我你的想法!我很乐意扩展任何内容或闭嘴!

更新

  • 2015-09-01 添加了具体示例。
© . All rights reserved.