zoukankan      html  css  js  c++  java
  • 在 WPF 程序中使用 MVVM 模式

    MVVM 模式是一个很久之前的技术了,最近因为一个项目的原因,需要使用 WPF 技术,所以,重新翻出来从前的一段程序,重温一下当年的技术。

    MVVM 模式

    MVVM 实际上涉及三个部分,Model, View 和 ViewModel ,三者的关系如下图所示。

    在三部分的关系中,视图显示的内容和操作完全依赖于 ViewModel。

    Model 是应用程序的核心,代表着最大、最重要的业务资产,因为它记录了所有复杂的业务实体、它们之间的关系以及它们的功能。

    Model 之上是 ViewModel。ViewModel 的两个主要目标分别是:使 Model 能够轻松被 WPF/XAML View 使用;将 Model 从 View 分离并对 Model 进行封装。这些目标当然非常好,但是由于一些现实的原因,有时并不能达到这些目标。

          您构建的 ViewModel 知道用户在高层上将如何与应用程序交互。但是,ViewModel 对 View 一无所知,这是 MVVM 设计模式的重要部分。这使得交互设计师和图形设计师能够在 ViewModel 的基础上创建优美、有效的 UI,同时与开发人员密切配合,设计适当的 ViewModel 来支持其工作。此外,View 与 ViewModel 的分离还使得 ViewModel 更有利于单元测试和重用。

    由于视图模型的变化要影响到视图的状态,我们需要使用两个重要的技术:可观察对象和命令模式。

    可观察对象

    可观察对象要求当对象的状态发生变化的时候,需要能够主动通知所有的观察者,在 WPF 中涉及到两个重要的接口 INotifyPropertyChanged 和 INotifyCollectionChanged,它们分别用来表示单个对象的状态发生了变化,和一个集合发生了变化。

    INotifyPropertyChanged 接口的定义如下所示:

    namespace System.ComponentModel
    {
        // 摘要: 
        //     向客户端发出某一属性值已更改的通知。
        public interface INotifyPropertyChanged
        {
            // 摘要: 
            //     在更改属性值时发生。
            event PropertyChangedEventHandler PropertyChanged;
        }
    }

    这是一个接口,通常我们会定义一个实现这个接口的基类来便于使用。在下面的实现中,通过事件来通知所有的观察者。

    namespace MVVM.Framework
    {
        // 实现观察者主题的通知
        public class BaseObservableObject : INotifyPropertyChanged
        {
            // 事件
            public event PropertyChangedEventHandler PropertyChanged;
    
            // 标准的触发事件的方法
            protected void OnPropertyChanged(string propertyName)
            {
                // 如果没有注册,会是 null
                if (PropertyChanged != null)
                {
                    var e = new PropertyChangedEventArgs(propertyName);
                    PropertyChanged(this, e);
                }
            }
        }
    }

    而 INotifyCollectionChanged 的定义如下,系统已经提供了一个泛型的实现 ObservableCollection,定义在命名空间 System.Collections.ObjectModel 中,我们可以直接使用。

    namespace System.Collections.Specialized
    {
        // 摘要: 
        //     向侦听器通知动态更改,如在添加或移除项时或在刷新整个列表时。
        [TypeForwardedFrom("WindowsBase, Version=3.0.0.0, Culture=Neutral, PublicKeyToken=31bf3856ad364e35")]
        public interface INotifyCollectionChanged
        {
            // 摘要: 
            //     当集合更改时发生。
            event NotifyCollectionChangedEventHandler CollectionChanged;
        }
    }

    命令模式

    对于命令模式来说,最重要的就是我们将每个命令封装为一个对象,这里涉及的接口是 ICommand,定义如下:

    namespace System.Windows.Input
    {
        // 摘要: 
        //     定义一个命令
        [TypeConverter("System.Windows.Input.CommandConverter, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null")]
        [TypeForwardedFrom("PresentationCore, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
        [ValueSerializer("System.Windows.Input.CommandValueSerializer, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null")]
        public interface ICommand
        {
            // 摘要: 
            //     当出现影响是否应执行该命令的更改时发生。
            event EventHandler CanExecuteChanged;
    
            // 摘要: 
            //     定义用于确定此命令是否可以在其当前状态下执行的方法。
            //
            // 参数: 
            //   parameter:
            //     此命令使用的数据。 如果此命令不需要传递数据,则该对象可以设置为 null。
            //
            // 返回结果: 
            //     如果可以执行此命令,则为 true;否则为 false。
            bool CanExecute(object parameter);
            //
            // 摘要: 
            //     定义在调用此命令时调用的方法。
            //
            // 参数: 
            //   parameter:
            //     此命令使用的数据。 如果此命令不需要传递数据,则该对象可以设置为 null。
            void Execute(object parameter);
        }
    }

    命令中,不仅包含了执行的方法,还包含了用来判断是否可以执行的方法,以及当是否可以执行发生变化的事件,这使得我们可以在数据状态发生变化的时候,动态通知视图的显示也同时发生变化,比如,在没有数据的情况下,修改按钮是不可用的,当已经存在数据的情况下,修改按钮就进入可用状态等等。

    通常我们会实现这个接口。我们自己来提供两个方法分别实现需要执行的操作和判断是否可以执行的条件。这里涉及到两个委托。

    Predicate 委托表示一个返回 bool 值的方法,这是一个泛型委托,我们可以传递一个参数进来,作为判断的条件。

    namespace System
    {
        // 摘要: 
        //     表示定义一组条件并确定指定对象是否符合这些条件的方法。
        //
        // 参数: 
        //   obj:
        //     要按照由此委托表示的方法中定义的条件进行比较的对象。
        //
        // 类型参数: 
        //   T:
        //     要比较的对象的类型。
        //
        // 返回结果: 
        //     如果 obj 符合由此委托表示的方法中定义的条件,则为 true;否则为 false。
        public delegate bool Predicate<in T>(T obj);
    }

    而 Action 委托则表示一个没有返回值的方法。我们在 View 中进行操作的时候,通常需要改变的是 ViewModel 的状态,并不需要返回结果给 View。

    namespace System
    {
        // 摘要: 
        //     封装一个方法,该方法只有一个参数并且不返回值。
        //
        // 参数: 
        //   obj:
        //     此委托封装的方法的参数。
        //
        // 类型参数: 
        //   T:
        //     此委托封装的方法的参数类型。
        public delegate void Action<in T>(T obj);
    }

    这样,我们默认的命令实现就成为如下的形式,DelegaeCommand 构造函数接收两个方法,一个就是被封装的实际操作,一个用来判断是否可用的方法。

    另外额外提供了一个 UpdateCanExecuteState 方法, 在每次执行处理方法之后,自动调用一下,更新是否可用的状态。

    namespace MVVM.Framework
    {
        /// <summary>
        /// 实现命令支持
        /// </summary>
        public class DelegateCommand : ICommand
        {
            // 是否可执行的条件
            private readonly Predicate<Object> canExecuteMethod;
            
            // 实际执行的操作, 表示有一个对象作为参数的方法
            private readonly Action<Object> executeActionMethod;
    
            // 构造函数
            // 创建命令对象的时候,提供实际执行方法的委托
            // 判断是否启用的委托
            public DelegateCommand(
                Predicate<Object> canExecute, 
                Action<object> executeAction
                )
            {
                canExecuteMethod = canExecute;
                executeActionMethod = executeAction;
            }
    
            #region ICommand Members
    
            public event EventHandler CanExecuteChanged;
    
            // 检查是否可以执行
            public bool CanExecute(object parameter)
            {
                var handlers = canExecuteMethod;
    
                if (handlers != null)
                {
                    return handlers(parameter);
                }
    
                return true;
            }
    
            // 执行操作
            public void Execute(object parameter)
            {
                // 检查是否提供了实际的方法委托
                var handlers = executeActionMethod;
    
                if (handlers != null)
                {
                    handlers(parameter);
                }
    
                // 执行之后,更新是否可以执行的状态
                UpdateCanExecuteState();
            }
    
            #endregion
    
            public void UpdateCanExecuteState()
            {
                var handlers = CanExecuteChanged;
    
                if (handlers != null)
                {
                    handlers(this, new EventArgs());
                }
            }
        }
    }

    第一部分先到这里,后面我们继续进行。

  • 相关阅读:
    懂一点Python系列——快速入门
    DatagridView 控件列顺序与设置的不一样
    WinForm Column cannot be added because its CellType property is null.
    Rabbitmq消息服务器通讯异常: name must not be blank
    Redis 数据库
    redis 安装与使用
    windows 服务的安装与卸载之bat脚本命令
    以超级管理员方式运行bat文件
    CMD 下切换目录
    C#导出Excel后关闭进程EXCEL.EXE
  • 原文地址:https://www.cnblogs.com/haogj/p/3788510.html
Copyright © 2011-2022 走看看