MVVM是一种设计模式,特别适用于WPF等XAML-based的应用程序开发。MVVM模式主要包含三个部分:Model(模型)、View(视图)和ViewModel(视图模型)。
- Model(模型):模型代表的是业务逻辑和数据。它包含了应用程序中用于处理的核心数据对象。模型通常包含业务规则、数据访问和存储逻辑。
- View(视图):视图是用户看到和与之交互的界面。在WPF中,视图通常由XAML定义,并且包含各种用户界面元素,如按钮、文本框、列表等。
- ViewModel(视图模型):视图模型是视图的抽象,它包含视图所需的所有数据和命令。视图模型通过实现
INotifyPropertyChanged
接口和使用ICommand
对象,将视图的状态和行为抽象化,从而实现了视图和模型的解耦。
MVVM模式的主要优点是分离了视图和模型,使得视图和业务逻辑之间的依赖性降低,提高了代码的可维护性和可测试性。此外,通过数据绑定和命令绑定,MVVM模式可以减少大量的样板代码,使得代码更加简洁和易于理解。
不使用MVVM的例子
在Winform中我们使用了事件驱动编程,同样在WPF中我们也可以使用事件驱动编程。
事件驱动编程是一种编程范式,程序的执行由外部事件决定。当一个事件发生时,会触发与之关联的事件处理器(EVent Handler)。事件处理器是一个函数或方法,用于响应特定事件。
流程图如下:
这里通过WPF实现一个事件驱动编程的例子:
首先是UI界面的xaml代码
<Window x:Class="demo_11_2.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:demo_11_2"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800"
Loaded="Window_Loaded">
<StackPanel>
<ToolBar>
<Label Content="姓名:"></Label>
<TextBox x:Name="nameTextBox" Width="50"></TextBox>
<Label Content="邮箱:"></Label>
<TextBox x:Name="emailTextBox" Width="100"></TextBox>
<Button Content="添加"
Click="AddUser"></Button>
</ToolBar>
<StackPanel>
<DataGrid x:Name="dataGrid1"></DataGrid>
</StackPanel>
</StackPanel>
</Window>
UI界面如是
这里使用了两个事件,窗体加载时间和按钮点击事件:
Loaded="Window_Loaded">
<Button Content="添加"
Click="AddUser"></Button>
添加两个类:用户类和用户管理类
public class User
{
public string? Name { get; set; }
public string? Email { get; set; }
}
public class UserManager
{
public static ObservableCollection<User> DataBaseUsers = new ObservableCollection<User>()
{
new User() { Name = "tom", Email = "[email protected]" },
new User() { Name = "jerry", Email = "[email protected]" },
new User() { Name = "speicher", Email = "[email protected]" }
};
public static ObservableCollection<User> GetUsers()
{
return DataBaseUsers;
}
public static void AddUser(User user)
{
DataBaseUsers.Add(user);
}
}
窗体加载事件处理程序:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
dataGrid1.ItemsSource = UserManager.GetUsers();
}
按钮点击事件处理程序:
private void AddUser(object sender, RoutedEventArgs e)
{
User user = new User();
user.Name = nameTextBox.Text;
user.Email = emailTextBox.Text;
UserManager.AddUser(user);
MessageBox.Show("成功添加用户!");
}
如此便可成功添加
使用MVVM的例子
上面使用的是事件驱动编程,我们在winform开发中经常这样干。对于一些小项目这样做很方便,但是如果业务逻辑很多,这样做就难以维护,因为UI与业务逻辑严重耦合。
使用MVVM,首先新建一个Commands文件夹,新建一个RelayComand类
public class RelayCommand:ICommand
{
public event EventHandler? CanExecuteChanged;
private Action<object> _Excute { get; set; }
private Predicate<object> _CanExcute { get; set; }
public RelayCommand(Action<object> ExcuteMethod, Predicate<object> CanExcuteMeth)
{
_Excute = ExcuteMethod;
_CanExcute = CanExcuteMeth;
}
public bool CanExecute(object? parameter)
{
return _CanExcute(parameter);
}
public void Execute(object? parameter)
{
_Excute(parameter);
}
}
RelayCommand实现了ICommand接口。
先来介绍一下ICommand
接口:在WPF中,ICommand
是一个接口,它定义了一种机制,用于在用户界面(UI)中处理事件,这种机制与用户界面的具体行为进行了解耦。这是实现MVVM设计模式的关键部分。
ICommand
接口包含两个方法和一个事件:
Execute(object parameter)
:当调用此命令时,应执行的操作。CanExecute(object parameter)
:如果可以执行Execute
方法,则返回true
;否则返回false
。这可以用于启用或禁用控件,例如按钮。CanExecuteChanged
事件:当CanExecute
的返回值可能发生更改时,应引发此事件。
ICommand结构图如下:
ICommand反编译代码:
public interface ICommand
{
event EventHandler? CanExecuteChanged;
bool CanExecute(object? parameter);
void Execute(object? parameter);
}
继续说RelayCommand
RelayCommand
是一种常用于WPF和MVVM模式的设计模式,它是一种特殊的命令类型。在MVVM模式中,RelayCommand
允许将命令的处理逻辑从视图模型中分离出来,使得视图模型不需要知道命令的具体执行逻辑,从而实现了视图模型和命令处理逻辑的解耦。
RelayCommand
通常包含两个主要部分:CanExecute
和Execute
。CanExecute
是一个返回布尔值的函数,用于确定命令是否可以执行。Execute
是一个执行命令的函数,当CanExecute
返回true
时,Execute
将被调用。
这种设计模式使得你可以在不改变视图模型的情况下,更改命令的处理逻辑,提高了代码的可维护性和可重用性。
简单来说就是RelayCommand
是ICommand
接口的一个常见实现,它允许你将Execute
和CanExecute
的逻辑定义为委托,从而实现对命令的灵活处理。
RelayCommand类中定义两个委托:
private Action<object> _Excute { get; set; }
private Predicate<object> _CanExcute { get; set; }
Action<object>
是一个委托,它封装了一个接受单个参数并且没有返回值的方法。这个参数的类型是object
。
对应于这一部分:
public void Execute(object? parameter)
{
_Excute(parameter);
}
Predicate<object>
是一个委托,它封装了一个接受单个参数并返回一个bool
值的方法。这个参数的类型是object
。
public bool CanExecute(object? parameter)
{
return _CanExcute(parameter);
}
其RelayCommand构造函数:
public RelayCommand(Action<object> ExcuteMethod, Predicate<object> CanExcuteMeth)
{
_Excute = ExcuteMethod;
_CanExcute = CanExcuteMeth;
}
然后说一下ViewModel
ViewModel是一个抽象,它代表了View的状态和行为。ViewModel包含了View所需的数据,并提供了命令以响应View上的用户操作。ViewModel不知道View的具体实现,它只知道如何提供View所需的状态和行为。
ViewModel的主要职责包括:
- 数据绑定:ViewModel提供了View所需的数据。这些数据通常是以属性的形式提供的,当这些属性的值改变时,ViewModel会通过实现
INotifyPropertyChanged
接口来通知View。 - 命令绑定:ViewModel提供了命令以响应View上的用户操作。这些命令通常是以
ICommand
接口的实现的形式提供的。 - 视图逻辑:ViewModel包含了View的逻辑,例如,决定何时显示或隐藏某个元素,何时启用或禁用某个按钮等。
新建一个ViewModel文件夹,在该文件夹中新建一个MainViewModel类:
public class MainViewModel
{
public ObservableCollection<User> Users { get; set; }
public ICommand AddUserCommand { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public MainViewModel()
{
Users = UserManager.GetUsers();
AddUserCommand = new RelayCommand(AddUser, CanAddUser);
}
private bool CanAddUser(object obj)
{
return true;
}
private void AddUser(object obj)
{
User user = new User();
user.Name = Name;
user.Email = Email;
UserManager.AddUser(user);
}
}
View与ViewModel之间的关系:
VVM关系图
首先最重要的就是数据绑定,现在View的xaml如下:
<Window
x:Class="demo11_1.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:local="clr-namespace:demo11_1"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<StackPanel>
<ToolBar>
<Label Content="name:" />
<TextBox Width="60" Text="{Binding Name}" />
<Label Content="email:" />
<TextBox Width="120" Text="{Binding Email}" />
<Button
Width="150"
Command="{Binding AddUserCommand}"
Content="Add" />
</ToolBar>
<StackPanel>
<DataGrid ItemsSource="{Binding Users}" />
</StackPanel>
</StackPanel>
</Window>
cs如下:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
MainViewModel mainViewModel = new MainViewModel();
this.DataContext = mainViewModel;
}
}
下图这几处为数据绑定,对应VVM关系图的DataBindings流程。
下图为命令绑定,对应于关系图的Commands,
如此便实现了与事件驱动一样的添加效果。
但是VVM关系图中的Send Notifications还没有体现,Send Notifications表示ViewModel中的更改会通知View。
现在我们来以一个例子说明一下Send Notifications是如何实现的。
在MainViewModel中添加一个测试命令: public ICommand AddUserCommand { get; set; }
构造函数添加如下: TestCommand = new Commands.RelayCommand(Test, CanTest);
需要的方法如下:
private bool CanTest(object obj)
{
return true;
}
private void Test(object obj)
{
Name = "demo";
Email = "[email protected]";
}
前台xaml代码加入: <Button Content="测试" Command="{Binding TestCommand }"></Button>
现在去deubg,我们会发现没有成功,原因是我们的ViewModel没有实现INotifyPropertyChanged
接口。
在WPF中,INotifyPropertyChanged
接口用于实现数据绑定中的属性更改通知。当绑定到UI元素的数据源中的属性值发生更改时,INotifyPropertyChanged
接口可以通知UI元素更新。
INotifyPropertyChanged
接口只定义了一个事件:PropertyChanged
。当属性值发生更改时,应触发此事件。事件参数PropertyChangedEventArgs
包含更改的属性的名称。
现在我们的MainViewModel实现一下INotifyPropertyChanged接口,如下所示:
public class MainViewModel : INotifyPropertyChanged
{
public ObservableCollection<User> Users { get; set; }
public ICommand AddUserCommand { get; set; }
public ICommand TestCommand { get; set; }
private string? _name;
public string? Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
}
private string? _email;
public string? Email
{
get { return _email; }
set
{
if (_email != value)
{
_email = value;
OnPropertyChanged(nameof(Email));
}
}
}
public MainViewModel()
{
Users = UserManager.GetUsers();
AddUserCommand = new RelayCommand(AddUser, CanAddUser);
TestCommand = new RelayCommand(Test, CanTest);
}
private bool CanTest(object obj)
{
return true;
}
private void Test(object obj)
{
Name = "demo";
Email = "[email protected]";
}
private bool CanAddUser(object obj)
{
return true;
}
private void AddUser(object obj)
{
User user = new User();
user.Name = Name;
user.Email = Email;
UserManager.AddUser(user);
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
如此,便可通知前台界面,消息框成功获取MainViewModel要发送的信息。
此过程对应VVM图中的SendNotifications。
现在来说ViewModel—Model。
VMM关系图
Model(模型):Model代表了业务逻辑和数据。它包含了应用程序中的数据和对数据的操作,例如,从数据库中获取数据,或者向数据库中添加数据。Model是独立于UI的,它不知道UI的存在。
ViewModel(视图模型):ViewModel是Model和View之间的桥梁。它包含了View所需的数据(这些数据来自于Model),并提供了命令以响应View上的用户操作。ViewModel将Model的数据转换为View可以显示的数据,同时,它也将View上的用户操作转换为对Model的操作。
这个例子中我们的数据来源于Model文件夹下的User类与UserManager类,这里的send notifications 如何解释?
首先我们修改MainViewModel类的Test方法:
private void Test(object obj)
{
Users[0].Name = "demo";
Users[1].Email = "[email protected]";
//Name = "demo";
//Email = "[email protected]";
}
发现现在并不会发送通知,实现View上的修改,这是因为User类并没有实现INotifyPropertyChanged接口,现在修改User类实现INotifyPropertyChanged接口:
public class User : INotifyPropertyChanged
{
private string? _name;
public string? Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
}
private string? _email;
public string? Email
{
get { return _email; }
set
{
if (_email != value)
{
_email = value;
OnPropertyChanged(nameof(Email));
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
如此便实现了通知,成功将表格中的小王修改为emo。
使用MVVM相关库请看这篇文章,使用MVVM相关库了解MVVM模式