08 MVVM框架

发布时间 2023-12-18 17:53:45作者: 讨厌敲代码的老郭

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

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

我们也可以自定义自己的命令,需要使用``​这个接口,这个接口用于实现一个命令类,我们根据自己实现的命令类创建我们的命令,由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>

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

提供了和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>