08 MVVM框架

10 MVVM框架

WPF是Windows Presentation Foundation的缩写,它是一种用于创建桌面应用程序的用户界面框架。WPF支持多种开发模式,其中一种叫做MVVM(Model-View-ViewModel)。

在WPF开发中,经典的编程模式是MVVM,是为WPF量身定做的模式,该模式充分利用了WPF的数据绑定机制,最大限度地降低了Xaml文件和CS文件的耦合度,也就是UI显示和逻辑代码的耦合度,如需要更换界面时,逻辑代码修改很少,甚至不用修改。

与WinForm开发相比,我们一般在后置代码中会使用控件的名字来操作控件的属性来更新UI,而在WPF中通常是通过数据绑定来更新UI;在响应用户操作上,WinForm是通过控件的事件来处理,而WPF可以使用命令绑定的方式来处理,耦合度将降低。

什么是MVVM?

MVVM是一种软件架构模式,它将应用程序分为三个层次:Model(模型),View(视图)和ViewModel(视图模型)。Model表示应用程序的数据和业务逻辑,View表示应用程序的用户界面,ViewModel表示View和Model之间的桥梁,它负责处理View的数据绑定和用户交互。

与此同时,在技术层面,WPF也带来了 诸如Binding(绑定)、Dependency Property(依赖属性)、Routed Events(路由事件)、Command(命令)、DataTemplate(数据模板)、ControlTemplate(控件模板)等新特性。

MVVM模式其实是MVP模式与WPF结合的应用方式时发展演变过来的一种新型架构模式。它立足于原有MVP框架并且把WPF的新特性糅合进去,以应对客户日益复杂的需求变化。

为什么要使用MVVM(MVVM的优势)?

MVVM的根本思想就是界面和业务功能进行分离,View的职责就是负责如何显示数据及发送命令,ViewModel的功能就是如何提供数据和执行命令。各司其职,互不影响。

在实际的业务场景中我们经常会遇到客户对界面提出建议要求修改,使用MVVM模式开发,当设计的界面不满足客户时,我们仅仅只需要对View作修改,不会影响到ViewModel中的功能代码,减少了犯错的机会。 随着功能地增加,系统越来越复杂,相应地程序中会增加View和ViewModel文件,将复杂的界面分离成局部的View,局部的View对应局部的ViewModel,功能点散落在各个ViewModel中,每个ViewModel只专注自己职能之内的事情。ViewModel包含了View要显示的数据,并且知道View的交互代码,所以ViewModel就像一个无形的View。

使用MVVM有以下几个好处:

  • 降低了View和Model之间的耦合度,使得它们可以独立地开发和测试。
  • 提高了代码的可重用性和可维护性,因为ViewModel可以在不同的View之间共享。
  • 简化了单元测试,因为ViewModel不依赖于具体的UI控件。
  • 支持双向数据绑定,使得View可以自动更新Model的变化,反之亦然。
  • 利用了WPF提供的强大特性,如命令、依赖属性、数据注解等。

结构模型图

image

  • Model: 就是系统中的对象,可包含属性和行为(就是一个class,是对现实中事物的抽象,开发过程中涉及到的事物都可以抽象为Model,例如客户,客户的姓名、编号、电话、住址等);
  • View: 就是用xaml实现的界面,负责与用户交互,接收用户输入,把数据展现给用户;
  • ViewModel: 是一个C#类,负责收集需要绑定的数据和命令,聚合Model对象,通过View类的DataContext属性绑定到View,同时也可以处理一些UI逻辑。显示的数据对应着ViewMode中的Property,执行的命令对应着ViewModel中的Command。

三者之间的关系: View对应一个ViewModel,ViewModel可以聚合N个Model,ViewModel可以对应多个View

MVVM前戏,需要掌握的知识

INotifyPropertyChanged

我们使用Binding​语法将控件的某个属性绑定到类属性上之后,控件属性的修改将会同步到类的属性上,但是即使我们设置了绑定方式为TwoWay​,我们修改类的属性时控件却不会发生变化,这是因为我们属性的变更没有进行通知

微软官方文档上对应数据绑定有这样一段话

数据绑定是在应用 UI 与其显示的数据之间建立连接的过程。 如果绑定具有正确的设置,并且数据提供适当的通知,则在数据更改其值时,绑定到该数据的元素会自动反映更改。

那么我们如何将属性的变更进行通知呢?就需要用到INotifyPropertyChanged​接口了。INotifyPropertyChanged接口,用于通知客户端我们的属性已经更改,需要进行UI的刷新了,具体用法如下

// 1、继承INotifyPropertyChanged接口
public partial class Window1 : Window, INotifyPropertyChanged
{
    // 2、实现INotifyPropertyChanged接口,只有一个事件
    public event PropertyChangedEventHandler PropertyChanged;
    private string myName = "张三";

    // 3、当属性变化时,触发对应的事件,并传递参数,通知变化的属性名字叫什么
    public string MyName
    {
        get { return myName; }
        set
        {
            myName = value;
            PropertyChanged(this, new PropertyChangedEventArgs("myName"));
        }
    }

    private int myAge = 20;
    public int MyAge
    {
        get { return myAge; }
        set
        {
            myAge = value;
            PropertyChanged(this, new PropertyChangedEventArgs("MyAge"));
        }
    }

    public bool MySex { get; set; } = true;

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        MyName = "李四";
        MyAge = 88;
        MySex = false;
    }
}

该例中类实现了INotifyPropertyChanged​接口,并在MyName和MyAge​变化时提供了通知,MySex​变化时未提供通知,当MyName和MyAge​变化时,xaml中的绑定也会进行更新,MySex​不会更新

可以封装一下代码如下

void OnPropertyChanged(string name)
{
    PropertyChanged(this, new PropertyChangedEventArgs(name));
}
// 当某个属性变化时进行调用
OnPropertyChanged(nameof(MyAge));  // nameof用于获取某个属性的属性名

但是如果该属性是一个集合,修改集合的项不会发起通知,因为不会走set​,我们需要使用ObservableCollection​类代替之前使用的List​,该类中实现了对应的通知

public ObservableCollection<string> Names { get; set; } = new ObservableCollection<string>
{
    "张三",
    "李四"
};
Names.Add("王五");

ICommand

命令是WPF中的一种机制,可以在xaml中绑定命令来执行一些操作,类似于绑定方法,但是又不太相同。命令可以写在其他的类中,由不同的窗体进行调用,而控件的事件绑定一般只出现在当前的窗体中,局限性较大。比如我们要实现一个关闭窗体的功能,这个功能需要在大多数窗口使用,那我们将不得不在每个窗体中都实现该方法,如果使用命令的话就不必如此了。

系统内置了一些命令供我们使用,如剪切、复制、粘贴等,更多命令参考这里

我们也可以自定义自己的命令,需要使用ICommand​这个接口,这个接口用于实现一个命令类,我们根据自己实现的命令类创建我们的命令,由View层发起命令,ViewModel层执行命令

RelayCommand 中继命令类的实现

我们不能直接使用ICommand接口创建一个命令,而是需要自己实现一个ICommand接口的类来创建我们的命令,最基本的结构如下

public class RelayCommand : ICommand
{
    // 一个用于通知命令可执行状态发生变化的事件
    public event EventHandler CanExecuteChanged;
    // 用于判断命令是否可以执行的方法,初次绑定和命令执行之前都会执行这个方法
    public bool CanExecute(object parameter)
    {
        // 根据返回值决定该任务是否可以被执行,比如:ApplicationCommands.Copy 命令,如果没有文本被选中,这个命令无法执行, 按钮也处于禁用状态
        return true;
    }
    // 命令执行所进行的任务
    public void Execute(object parameter)
    {
        MessageBox.Show("命令执行了");
    }
}

命令的创建和绑定

在某个类中对中继类进行实例化,并在xaml中进行命令的绑定即可,以下示例将会在点击按钮时弹窗,执行对应的命令任务

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        CommandA = new RelayCommand();
    }
    public RelayCommand CommandA { get; private set; }
}
<Button Command="{Binding CommandA}" Content="测试"/>

完整写法

上例中演示了最基本的命令使用,该命令十分简单,仅仅完成了命令的绑定和执行,我们应该让这个类更加的灵活,不能总执行相同的任务代码,命令的可执行状态也不能老是为true,完整的代码如下

public class RelayCommand : ICommand
{
    // 可执行状态变化时应该被触发的事件
    public event EventHandler CanExecuteChanged;
    // 用于判断命令是否为可执行状态的方法
    public bool CanExecute(object parameter)
    {
        // 如果没有canExecute,则命令永远处于可以执行的状态
        if (canExecute == null) return true;
        // 如果有,则返回该方法的返回值
        return canExecute.Invoke(parameter);
    }
    // 执行的命令的方法
    public void Execute(object parameter)
    {
        // 触发执行的任务
        execute.Invoke(parameter);
    }

    // 一个委托,存储要执行的任务
    Action<object> execute;
    // 一个委托,存储判断命令是否能执行的方法
    Func<object, bool> canExecute;
    // 构造函数,接收委托方法并存储
    public RelayCommand(Action<object> execute) : this(execute, null)
    {
    }
    public RelayCommand(Action<object> execute, Func<object, bool> canExecute)
    {
        this.execute = execute;
        this.canExecute = canExecute;
    }
    // 用于触发CanExecuteChanged事件的一个方法
    public void OnCanExecuteChange()
    {
        CanExecuteChanged(this, EventArgs.Empty);
    }
}

使用示例

该示例有两个按钮,一个按钮用于触发命令,另一个按钮控制命令是否可以被执行

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        // 初始化命令,并指定任务和判断命令是否能执行的方法
        CommandA = new RelayCommand(Fn, CanExecute);
    }
    // 声明命令
    public RelayCommand CommandA { get; private set; }
    // 命令要执行的任务
    public void Fn(object o)
    {
        Console.WriteLine("触发了");
    }
    // 限制命令是否能执行的变量
    bool canRun = true;
    // 判断命令是否能执行的方法
    bool CanExecute(object o)
    {
        return canRun;
    }
    // 按钮点击修改状态的方法
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        canRun = !canRun;
        // 修改后要手动触发该方法,否则页面不会更新命令的状态
        CommandA.OnCanExecuteChange();
    }
}
<Button Command="{Binding CommandA}" Content="测试"/>
<Button Click="Button_Click"  Content="修改状态"/>

MVVM示例1

实现MVVM需要遵循以下几个步骤:

  1. 创建一个Model类,定义应用程序所需的数据和业务逻辑。
  2. 创建一个ViewModel类,继承自INotifyPropertyChanged接口,并实现属性变更通知。在ViewModel中定义与Model相关联的属性,并提供相应的命令来执行用户操作。
  3. 创建一个View类(通常是一个XAML文件),定义应用程序的用户界面。在View中使用数据绑定来连接ViewModel中的属性和命令,并设置相关的样式和行为。
  4. 在App.xaml或其他合适的地方创建一个ViewModel实例,并将其作为View中DataContext属性值。

新建一个窗口:WindowExample1.xaml

创建一个Model类

创建一个Model类,定义应用程序所需的数据和业务逻辑。

新建一个文件夹命名为:Models,并添加一个类文件:User.cs

// Model class
public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}

创建一个ViewModel类

创建一个ViewModel类:UserInfoViewModel.cs,继承自INotifyPropertyChanged接口,并实现属性变更通知。在ViewModel中定义与Model相关联的属性,并提供相应的命令来执行用户操作。

public class UserInfoViewModel : INotifyPropertyChanged
    {
        private User user;

        public UserInfoViewModel()
        {
            user = new User();
            SaveCommand = new RelayCommand(Save);
            CancelCommand = new RelayCommand(Cancel);
        }

        public string UserName
        {
            get { return user.Name; }
            set
            {
                user.Name = value;
                OnPropertyChanged("UserName");
            }
        }

        public int UserAge
        {
            get { return user.Age; }
            set
            {
                user.Age = value;
                OnPropertyChanged("UserAge");
            }
        }

        public string UserInfo
        {
            get { return $"Name:{UserName} Age:{UserAge}"; }
        }
        // 上一章节中封装的RelayCommand类实例
        public ICommand SaveCommand { get; private set; }
        public ICommand CancelCommand { get; private set; }

        private void Save(object parameter)
        {
            // Save user data to database or service
            MessageBox.Show("User data saved!");

            OnPropertyChanged("UserInfo");
        }

        private void Cancel(object parameter)
        {
            // Close dialog window without saving data
            var window = parameter as Window;
            if (window != null)
                window.Close();
        }

        public event PropertyChangedEventHandler PropertyChanged;
        /// <summary>
        /// 实现通知更新
        /// </summary>
        /// <param name="propertyName"></param>
        /// OnPropertyChanged这个属性在WinForm时代就有了,WPF只是向下兼容而已。WPF使用依赖属性自动通知注册者属性值更变。
        /// OnPropertyChanged需要你在属性值每次变化的时候主动调用一个方法,会引发此事件,当Entity绑定到控件时,控件会主动注册OnPropertyChanged事件,所以属性变化的时候控件会自动更新,这就是数据绑定的基础。
        /// OnPropertyChanged 监听属性值的变化 然后前端可以根据值的变化做出一些改变。比如checkbox 当你设定的isCheck值为 false 他就会把勾取消 你再点击一下 他的值变成了True
        /// 然后会响应OnPropertyChanged 然后前端的checkbox就会自动有个勾选的状态 如果是类似name,id类的属性 前端当然就不会有什么改变了
        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

UI

创建一个View类(通常是一个XAML文件),定义应用程序的用户界面。在View中使用数据绑定来连接ViewModel中的属性和命令,并设置相关的样式和行为。

<Window x:Class="MVVMDemo.WindowExample1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:MVVMDemo"
        mc:Ignorable="d"
        Title="WindowExample1" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <Label Content="Name:" Grid.Row="0" Grid.Column="0" Margin="10"/>
        <TextBox Text="{Binding UserName}" Grid.Row="0" Grid.Column="1" Margin="10"/>

        <Label Content="Age:" Grid.Row="1" Grid.Column="0" Margin="10"/>
        <TextBox Text="{Binding UserAge}" Grid.Row="1" Grid.Column="1" Margin="10"/>

        <StackPanel Orientation="Horizontal" HorizontalAlignment="Right"
                    Grid.Row="3" Grid.ColumnSpan="2">
            <!--Foreground="{Binding Foreground,:   当前元素Button绑定目标元素的Foreground属性-->
            <Button Content="Save" Command="{Binding SaveCommand}"
                    CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor,
                  AncestorType={x:Type Window}}}" Margin= "10"/>
            <Button Content="Cancel" Command="{Binding CancelCommand}"
                    CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor,
                  AncestorType={x:Type Window}}}" Margin= "10"/>
        </StackPanel>
    </Grid>
</Window>
namespace MVVMDemo
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class WindowExample1 : Window
    {
        public WindowExample1()
        {
            InitializeComponent();
            this.DataContext = new UserInfoViewModel();
        }
    }
}

MvvmLight框架包

快速上手

image

引入该框架包之后, 默认会在目录下创建ViewModel层的示例代码 ​image​ 同时入口启动文件也发生了变化。

安装之后,代码报错: image 解决办法 imageimage

添加一个 Student 类

在 Models 中新增一个:Student.cs 类

namespace MVVMDemo.Models
{
    public class Student
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public string Sex { get; set; }
    }
}

通过在MainViewModel中创建一些业务代码, 将其与MainWindow.xaml 通过上下文的方式关联起来, 而MainWindow则是通过Binding的写法 引用业务逻辑的部分。

  1. 在MainViewModel中, 添加同一个班级名称, 与学生列表, 分别用于显示在文本 和列表上展示, Command则用于绑定DataGrid的双击命令上, 通过双击, 展示点击行的学生信息: MainViewModel.cs:
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using MVVMDemo.Models;
using System.Collections.ObjectModel;
using System.Windows;
using RelayCommand = GalaSoft.MvvmLight.Command.RelayCommand;
namespace MVVMDemo.ViewModel
{
    /// <summary>
    /// This class contains properties that the main View can data bind to.
    /// <para>
    /// Use the <strong>mvvminpc</strong> snippet to add bindable properties to this ViewModel.
    /// </para>
    /// <para>
    /// You can also use Blend to data bind with the tool's support.
    /// </para>
    /// <para>
    /// See http://www.galasoft.ch/mvvm
    /// </para>
    /// </summary>
    public class MainViewModel : ViewModelBase
    {
        /// <summary>
        /// Initializes a new instance of the MainViewModel class.
        /// </summary>
        public MainViewModel()
        {
            ////if (IsInDesignMode)
            ////{
            ////    // Code runs in Blend --> create design time data.
            ////}
            ////else
            ////{
            ////    // Code runs "for real"
            ////}
            ClassName = "200302班";
            students = new ObservableCollection<Student>();
            students.Add(new Student() { Name="张三",Age=18,Sex="男" });
            students.Add(new Student() { Name="李四",Age=12,Sex="女" });
            students.Add(new Student() { Name="王五",Age=20,Sex="男" });
        }

        public string ClassName { get; set; }

        private ObservableCollection<Student> students;
        public ObservableCollection<Student> Students
        {
            get { return students; }
            set { students = value; RaisePropertyChanged(); }
        }


        private RelayCommand<Student> command;
        public RelayCommand<Student> Command
        {
            get
            {
                if (command == null)
                    command = new RelayCommand<Student>((t)=> Rcommand(t));
                return command;
            }
        }

        private void Rcommand(Student stu)
        {
            MessageBox.Show($"学生的姓名:{stu.Name},学生的年龄:{stu.Age},学生的性别:{stu.Sex}");
        }
    }
}
  1. 设计UI层,在XMAL文件中 添加一个文本用于显示班级名称, 添加一个DataGrid 用于展示学生列表, 同时DataGrid中添加一个绑定的命令 MainWindow.xaml:
<Window x:Class="MVVMDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:MVVMDemo"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="20"></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>

        <!--展示班级-->
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
            <TextBlock Margin="5,0,0,0" Text="班级名称:"></TextBlock>
            <TextBlock Margin="5,0,0,0" Text="{Binding ClassName}"></TextBlock>
        </StackPanel>

        <DataGrid Grid.Row="1" ItemsSource="{Binding Students}" AutoGenerateColumns="False">
            <!--同时DataGrid中添加一个绑定的命令-->
            <!--下面为一种绑定语法, 主要在MouseBinding中, MouseAction 以为触发的事件类型, CommandParameter 则是命令传递的参数, 
            也就是DataGrid选中的一行的类型 Student。Command 则是MainViewModel中定义的Command。RelativeSource FindAncestor,
            主要用于控件模板或可预测的自包含 UI 组合。-->
            <DataGrid.InputBindings>
                <MouseBinding MouseAction="LeftDoubleClick" 
                              CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=DataGrid},Path=SelectedItem}" 
                              Command="{Binding Command}"/>
            </DataGrid.InputBindings>
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Name}" Header="名称"></DataGridTextColumn>
                <DataGridTextColumn Binding="{Binding Age}" Header="年龄"></DataGridTextColumn>
                <DataGridTextColumn Binding="{Binding Sex}" Header="性别"></DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
   
    </Grid>
</Window>
namespace MVVMDemo
{
    /// <summary>
    /// WindowExample2.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new MainViewModel();
        }
    }
}

选中一行,鼠标左键双击测试

RaisePropertyChanged() 实现动态通知更新

  1. 通过set访问器更新ClassName的同时, 调用RaisePropertyChanged 方法, 界面刷新更新后的值; ​image
private string className;
public string ClassName 
{ 
    get { return className; }
    set { className = value; RaisePropertyChanged(); }
}
  1. 添加一个无参数的UpdateCommand , 并设置为 UpdateText 手动把ClassName更新为 “其他班级”: ​image
private RelayCommand updateCommand;
public RelayCommand UpdateCommand
{
    get
    {
        if (updateCommand == null)
            updateCommand = new RelayCommand(() => UpdateText());
        return updateCommand;
    }
}
private void UpdateText()
{
    ClassName = "其他班级";
}
  1. UI层添加一个简单按钮, 绑定后台的UpdateCommand命令 ​image
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
    <Button Content="刷新" Command="{Binding UpdateCommand}"></Button>
    <TextBlock Margin="5,0,0,0" Text="班级名称:"></TextBlock>
    <TextBlock Margin="5,0,0,0" Text="{Binding ClassName}"></TextBlock>
</StackPanel>

其他资料参考

利刃MVVMLight(详细)

CommunityToolkit.Mvvm 框架包

CommunityToolkit.Mvvm​ (前称 Microsoft.Toolkit.Mvvm​MVVM 工具包)是一个现代、快速和模块化的 MVVM 库。 它是 .NET 社区工具包的一部分,围绕以下原则构建:

  • 平台和运行时 Independent.NET - Standard 2.0 .NET Standard 2.1.NET 6 (UI 框架不可知)
  • 易于选取和使用 - 对应用程序结构或编码范例(在“MVVM”外部)没有严格的要求,即灵活使用。
  • 点菜 - 自由选择要使用的组件。
  • 参考实现 - 精简和高性能,为基类库中包含的接口提供实现,但缺少直接使用它们的具体类型。

使用入门

若要从 Visual Studio 中安装包,请执行以下操作:

  1. 在解决方案资源管理器中,右键单击项目并选择“管理 NuGet 包”。 搜索 CommunityToolkit.Mvvm 并安装它。
    NuGet Packages

  2. 添加 using 或 Imports 指令以使用新 API:

    using CommunityToolkit.Mvvm;
    

ObservableObject 可观测对象

WPF 中, 写出 MVVM 设计模式的程序, 自然需要进行前台与后台数据的绑定, 而在原生实现中, 编写一个可绑定的类,继承INotifyPropertyChanged​接口,并且编写该类中可绑定的成员, 是非常麻烦的一件事情。 你需要手动在值变更的时候, 引发 “PropertyChanged” 事件,这在前面的章节已经讨论过了。

ObservableObject​这是通过实现INotifyPropertyChanged​和INotifyPropertyChanging​接口可观察的对象的基类。 它可以用作需要支持属性更改通知的各种对象的起点。我们要封装一个可观测的对象,应该让该类继承自​ObservableObject

ObservableObject​ 具有以下主要功能:

  • 它为和INotifyPropertyChanging​公开PropertyChanged​事件PropertyChanging​提供了基本实现INotifyPropertyChanged​。
  • 它提供了一系列 SetProperty​ 方法,可用于从继承 ObservableObject​自的类型轻松设置属性值,并自动引发相应的事件。
  • 它提供了类似于 SetPropertyAndNotifyOnCompletion​ 此方法, SetProperty​ 但能够设置 Task​ 属性并在分配的任务完成后自动引发通知事件。
  • 它公开了可在派生类型中重写的 OnPropertyChanged​ 和 OnPropertyChanging​ 方法,以自定义如何引发通知事件。

下面是一个可观测对象的示例,并实现了一个自定义的属性进行通知

public class MainViewModel : ObservableObject
{
	private string test = "测试哈哈哈";

	public string MyTest
	{
		get { return test; }
		set { SetProperty(ref test, value); }
	}
}

使用SetProperty​方法更新属性值,并在特定的时候进行通知,引发相关的事件

RelayCommand和RelayCommand<T>

ICommand接口是用于在 .NET 中为 Windows 运行时 应用编写的命令的代码协定。 这些命令为 UI 元素提供命令行为,如Button的Command。

RelayCommand实现了ICommand接口,可以将一个方法或委托绑定到视图(View)上。

这个的使用方法和我们自己实现的RelayCommand​基本相同,示例如下

public class ViewModel : ObservableObject
{
    // 1、定义ICommand类型的属性来存储命令
    public ICommand AddCommand { get; }
    // 2、创建命令所执行的方法
    void Fn()
    {
    }
    // 3、在构造函数中对命令进行初始化
    public MainViewModel()
    {
        AddCommand = new RelayCommand(Fn);
        AddMoreCommand = new RelayCommand<string>(AddMore);
    }
  
    // 有参的命令
    public ICommand AddMoreCommand { get; }
    void AddMore(string a)
    {
    } 
}
<Button Content="点击增加" Command="{Binding AddCommand}"/>

我们可以通过传递一个方法来实现对命令的可执行状态进行控制,使用方式如下

public partial class MainViewModel : ObservableObject
{
    public MainViewModel()
    {
        // 创建命令并指定任务和判断命令是否可执行的方法
        AddCommand = new RelayCommand(Add, CanAdd);
    }
    // 可监听的字段和属性
    private int num;
    public int Num
    {
        get => num; 
        set
        {
            SetProperty(ref num, value);
            // !!!!!!!!!!!!一定要在依赖属性变化时通知对应的命令,否则不会变更命令的状态
            AddCommand.NotifyCanExecuteChanged();
        }
    }
  
    public IRelayCommand AddCommand { get; }
    bool CanAdd() => Num <= 10;
    void Add()
    {
        Num++;
    }
}

AsyncRelayCommand

AsyncRelayCommand提供了和RelayCommand一样的基础命令功能,但是在此基础上,增加了异步。

AsyncRelayCommand具备功能如下:

  • 支持异步操作,可以返回Task。
  • 使用带ConcellationToken重载的版本,可以取消Task。公开了CanBeCanceled和IsCancellationRequested属性,以及Cancel()方法。
  • 公开ExecutionTask属性,可用于监视待处理操作的进度。公开 IsRunning属性,可以用于判断操作是否完成
  • 实现了IAsyncRelayCommand and IAsyncRelayCommand<T>接口。IAsyncRelayCommand就是在IRelayCommand接口的基础上增加异步操作的接口。

使用方法如下

class Test : ObservableObject
{
  // 存储命令的属性
  public AsyncRelayCommand AsyncComm { get; }
  // 执行的异步任务,需要返回一个Task
  private Task Abc()
  {
    Task t = new Task(() =>
    {
        Thread.Sleep(1000);
        Num = 10;
    });
    t.Start();
    return t;
  }
  public MainViewModel()
  {
    // 进行命令的初始化
    AsyncComm = new AsyncRelayCommand(Abc);
  }
}
<!--绑定命令-->
<Button Content="异步命令" Command="{Binding AsyncComm}" CommandParameter="3"/>
<!--IsRunning属性用于获取命令是否正在运行-->
<CheckBox IsChecked="{Binding AsyncComm.IsRunning,Mode=OneWay}" Content="运行中"/>
<TextBlock>
    <Run Text="Status: "/>
    <!--Status用于获取命令的状态-->
    <Run Text="{Binding AsyncComm.ExecutionTask.Status,Mode=OneWay}"/>
</TextBlock>

ObservableValidator

ObservableValidator​ 是实现 INotifyDataErrorInfo​ 接口的基类,它支持验证向其他应用程序模块公开的属性。 它也继承自 ObservableObject​,因此它还可实现 INotifyPropertyChanged​ 和 INotifyPropertyChanging​。 它可用作需要支持属性更改通知和属性验证的各种对象的起点。

ObservableValidator​ 包含以下主要功能:

  • 它提供对 INotifyDataErrorInfo​ 的基本实现,从而公开 ErrorsChanged​ 事件和其他必要的 API。
  • 它提供一系列额外的 SetProperty​ 重载(ObservableObject​ 基类提供的重载除外),这些重载提供在更新属性值之前自动验证属性和引发必要事件的功能。
  • 它公开了许多 TrySetProperty​ 重载,这些重载类似于 SetProperty​ 但仅在验证成功时更新目标属性,并在出错时返回生成的错误以供进一步检查。
  • 它公开了 ValidateProperty​ 方法,这对于手动触发对特定属性的验证非常有用,以防其值尚未更新,但其验证依赖于已更新的另一个属性的值。
  • 它公开了 ValidateAllProperties​ 方法,这会自动执行对当前实例中所有公共实例属性的验证,前提是它们至少应用了一个 [ValidationAttribute]​。
  • 它公开了 ClearAllErrors​ 方法,该方法在重置绑定到用户可能需要再次填充的某个表单的模型时非常有用。
  • 它提供许多构造函数,这些函数允许传递不同的参数来初始化将用于验证属性的 ValidationContext​ 实例。 使用可能需要其他服务或选项才能正常工作的自定义验证特性时,这尤其有用。

属性的验证

可以在需要进行验证的属性上添加特性进行校验

public class RegistrationForm : ObservableValidator
{
    private string name;

    [Required] // 必传
    [MinLength(2)]  // 最小长度
    [MaxLength(100)]  // 最大长度
    public string Name
    {
        get => name;
        set => SetProperty(ref name, value, true);
    }
}

然后,其他组件(如 UI 控件)可以与 viewmodel 交互并修改其状态,以反映 viewmodel 中当前存在的错误,方法是注册到 ErrorsChanged​ 并使用 GetErrors(string)​ 方法检索已修改的每个属性的错误列表。

错误信息的展示

数据绑定模型允许您将与您Binding的对象相关联ValidationRules。 如果用户输入的值无效,你可能希望在应用程序 用户界面 (UI) 上提供一些有关错误的反馈。 提供此类反馈的一种方法是设置Validation.ErrorTemplate附加到自定义ControlTemplate的属性

如果没有设置Validation.ErrorTemplate,当控件包含无效数据时,WPF 将在无效控件周围显示如下图所示的红色边框,

image

这样用户就能清楚这是一个无效的数据,直到用户输入有效的值这个红色的边框才会消失。可是只有一个红色边框,用户并不清楚具体有什么错误,通常需要用其它手段来通知用户具体的错误信息(例如弹出MessageBox)。

一种更好的方式是通过自定义Validaion.ErrorTemplate显示更多的信息。Validaion.ErrorTemplate的类型是ControlTemplate,它的默认值如下:

<ControlTemplate>
    <Border BorderThickness="1"
            BorderBrush="Red">
        <AdornedElementPlaceholder />
    </Border>
</ControlTemplate>

当控件绑定数据无效时默认显示这个ControlTemplate,其中的AdornedElementPlaceholder专门用于Validaion.ErrorTemplate,它用于提供AdornedElement关联的错误控件的定位和尺寸。

就像这样,很简单的就可以为控件添加一个错误的提示,下面代码中的Binding [0].ErrorContent​表示绑定错误列表中第一个错误的错误内容

<Style TargetType="TextBox">
    <Setter Property="Validation.ErrorTemplate">
        <Setter.Value>
            <ControlTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Foreground="Red" Text="{Binding [0].ErrorContent}"/>
                    <Border BorderBrush="Red" BorderThickness="1">
                        <AdornedElementPlaceholder/>
                    </Border>
                </StackPanel>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

但是这样显示的错误信息为默认的信息,不太友好,我们可以在使用特性时手动指定错误信息进行显示

[Required(ErrorMessage ="名称为必填项")]
[MinLength(3, ErrorMessage = "名称不能小于3个字")]
public string Name
{
    get { return name; }
    set { SetProperty(ref name, value); }
}

常用的ValidationAttribute 子类

MSDN文档

[Required(ErrorMessage="请填写名称")]  // 如果属性为null,"",或只包含空白字符,会引发异常
[RegularExpression(pattern:@"^[1-9]\d*$",ErrorMessage ="请选择部门")]  // 正则表达式验证
[Compare("NewPassword",ErrorMessage ="确认密码和新密码不一致,请检查")]  // 用来检测两个字段是否相等
[MaxLength(length:11,ErrorMessage ="请填写合法的手机号")]  // 最大长度检测
[MinLength(length:2,ErrorMessage ="名字必须两个字及以上")]  //
[Range[0,100,ErrorMessage="请输入0到100的数字"]  // 范围检测

CustomValidation,主要是用来执行自定义的验证

构造方法的参数如下

  • Type validatorType :自定义验证方法的类
  • string method:自定义验证的方法名称

先定义检测方法

public class TestValidator
{
    public static ValidationResult TestAge(int age)
    {
        if (age > 10 && age < 20)
        {
            return new ValidationResult("10<age<20,你太年轻了");
        }
        else
        {
            return ValidationResult.Success;
        }
    }
}

然后应用注解

[CustomValidation(typeof(TestValidator),"TestAge")]
public int Age { get; set; }

Messenger 信使

负责 ViewModel与ViewModel、View与ViewModel之间的通信

image

image

我们使用MVVM开发模式进行开发时,ViewModel之间的通信常常是很头疼的事情,好在MVVM Light提供了Messenger类可以轻松的在ViewModel之间传递消息。

诸如 Messenger 之类的系统有时称为事件总线或事件聚合器,事件总线这个概念对你来说可能很陌生,但提到观察者(发布-订阅)模式,你也许就很熟悉。事件总线是对发布-订阅模式的一种实现。它是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需要相互依赖,达到一种解耦的目的。

此类组件连接接收端和发送端(有时分别称为“发布服务器”和“订阅服务器”)。

MVVM Light Messenger 旨在通过简单的前提来精简此场景:任何对象都可以是接收端;任何对象都可以是发送端;任何对象都可以是消息。

词汇也得到了简化。使用消息这类容易理解的词语,而不是使用难以定义的词语(如“事件聚合”)。订阅服务器变为接收端,发布服务器则变为发送端。消息取代了事件。通过语言与实施上的简化,您可以更轻松地开始使用 Messenger 并了解它的工作方式。..

使用【值变更消息】

值消息变更ValueChangedMessage<T>​用于一个VM通知其他的VM当前对象的某些属性发生变化

// 1、创建消息变更对象(Xxx一般为发生变化的属性的名字)
public class XxxChangeMessage : ValueChangeMessage<Xxx类型>
{
  public StudentMessage(Xxx类型 xxx) : base(xxx){ }
}
// 2、在需要监听属性变更的模式注册监听
WeakReferenceMessenger.Default.Register<XxxChangeMessage, string>(this, "一个令牌", (display,message) =>{
  // message.Value 为触发属性变更时传递的参数
  Console.WriteLine("xxx变化了,现在值" + message.Value);
});
// 3、当模块中对应的属性变化时发送消息(参数1的类型必须和第二步的参数类型一致,参数2的令牌必须和第二步的令牌一致)
WeakReferenceMessenger.Default.Send(new XxxChangeMessage(变化的属性), "一个令牌");

使用【请求消息】

请求消息可以被用于从一个模块向另一个模块请求值。为了做到这点,该包包含了一个RequestMessage<T>​基类,它能像这样使用

// 1、创建请求消息对象
public class LoggedInUserRequestMessage : RequestMessage<数据类型>
{
}
// 2、数据的拥有者进行请求的监听
WeakReferenceMessenger.Default.Register<MyViewModel, LoggedInUserRequestMessage>(this, (r, m) =>
{
    // 当请求被触发时,应该调用 m.Reply  传递参数
    m.Reply(要发送的参数);
});
// 3、数据的需求者需要数据时触发请求
数据类型 user = WeakReferenceMessenger.Default.Send<LoggedInUserRequestMessage>();

如果发起请求后对方并未发送数据,将会抛出错误

使用【属性变更消息】

我们肯经常有需求监听另一个模块的属性变化,如输入框事件,除了使用第一种方式之外还可以使用属性变更消息

// 1、触发属性变化事件
WeakReferenceMessenger.Default.Send(new PropertyChangedMessage<string>(this, "属性名", 旧值, 新值), "标识符");
// 2、监听属性变化
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<string>, string>(this, "标识符", PropertyChanged);
private void PropertyChanged(object recipient, PropertyChangedMessage<string> message)
{
    Console.WriteLine($"属性变化, 属性名:{message.PropertyName},新:{message.NewValue},旧:{message.OldValue}");
}

注册消息

上篇给出了注册的方法,但是注册可以有很多种方式,最常见的就是命名方法调用和Lambda表达式调用的方式:

4.1、基本的命名方法注册

// 使用命名方法进行注册
Messenger.Default.Register<String>(this, HandleMessage);

//卸载当前(this)对象注册的所有MVVMLight消息
this.Unloaded += (sender, e) => Messenger.Default.Unregister(this);

private void HandleMessage(String msg)
{
  //Todo
}

4.2、使用 Lambda 注册

Messenger.Default.Register<String>(this, message => {
                // Todo
});
//卸载当前(this)对象注册的所有MVVMLight消息
this.Unloaded += (sender, e) => Messenger.Default.Unregister(this);

4.3、使用令牌(Token)

区分和使用信道:这是最常用的方式。使用专属Token,可以区分不同的信道,并提高复用性。

Messenger中包含一个token参数,发送方和注册方使用同一个token,便可保证数据在该专有信道中流通,所以令牌是筛选和隔离消息的最好办法。

//以ViewAlert位Tokon标志,进行消息发送
Messenger.Default.Send<String>("ViewModel通知View弹出消息框", "ViewAlert");
public MessagerForView()
{
  InitializeComponent();

  //消息标志token:ViewAlert,用于标识只阅读某个或者某些Sender发送的消息,并执行相应的处理,所以Sender那边的token要保持一致
  //执行方法Action:ShowReceiveInfo,用来执行接收到消息后的后续工作,注意这边是支持泛型能力的,所以传递参数很方便。
  Messenger.Default.Register<String>(this, "ViewAlert", ShowReceiveInfo);
  this.DataContext = new MessengerRegisterForVViewModel();
  //卸载当前(this)对象注册的所有MVVMLight消息
  this.Unloaded += (sender, e) => Messenger.Default.Unregister(this);
}

/// <summary>
/// 接收到消息后的后续工作:根据返回来的信息弹出消息框
/// </summary>
/// <param name="msg"></param>
private void ShowReceiveInfo(String msg)
{
  MessageBox.Show(msg);
}

案例

在MVVM模式中,ObservableRecipient作为ViewModel的基类负责接收信息。

xxxViewModel类继承ObservableRecipient,并且重写OnActivated方法,OnActivated负责注册。

为了获得最佳效果和避免内存泄漏,建议使用OnActivated来注册消息,使用OnDeactivated来做清理操作。这种模式允许viewmodel启停多次,同时可以安全地收集,而不会在每次停用的时候产生内存泄漏的风险

nternal partial class  VVVVViewModel: ObservableRecipient
{
  public VVVVViewModel( )
  {
      this.IsActive = true;///开关按钮,true时开始接受信息,false 关闭(可以通过控制该变量来控制是否注册信息)
  }
  /// <summary>
  ///   注册,新建一个通道AddStocksViewModel,发送值变更消息  
  /// </summary>
  protected override void OnActivated()
  {
      WeakReferenceMessenger.Default.Register<PacketpocoMessage, string>(this, "AddStocksViewModel", (display, mess) => ((AddStocksViewModel)display).curentPacketpoco = mess.Value);
 }
 /// <summary>
  /// 注销信息     //重载OnDeactivated事件,在窗体被取消激活时触发。
  /// </summary>
  protected override void OnDeactivated()
  {
      base.OnDeactivated();
      WeakReferenceMessenger.Default.Unregister<PacketpocoMessage>(this);
  }

拓展

AsyncRequestMessage<t>

用于异步的请求对应模块的数据

// 创建消息对象,继承AsyncRequestMessage<T>
public class LoggedInUserRequestMessage : AsyncRequestMessage<User>
{
}

// 属性拥有者监听请求
WeakReferenceMessenger.Default.Register<MyViewModel, LoggedInUserRequestMessage>(this, (r, m) =>
{
    m.Reply(r.GetCurrentUserAsync()); // 需要恢复一个 Task<User>
});

// 从另一个模块请求值(我们可以直接等待请求)
User user = await WeakReferenceMessenger.Default.Send<LoggedInUserRequestMessage>();

Prism框架

Prism简介-Prism中文文档-CSharp开发网 (csharpshare.com)

Prism.Core

Prism.Wpf 和 Prism.Unity

posted @ 2023-12-18 17:49  讨厌敲代码的老郭  阅读(160)  评论(0编辑  收藏  举报