MVVM框架与项目实例
- 一、MVVM框架
-
- [1. 概述](#1. 概述)
- [2. 核心组件与优势](#2. 核心组件与优势)
- 一、MVVM项目
-
- 1.普通项目
- [2. MVVM架构](#2. MVVM架构)
- [3. MVVM项目实例](#3. MVVM项目实例)
-
- [1. 项目准备](#1. 项目准备)
- [2. LoginViewModel与Login](#2. LoginViewModel与Login)
- [2. MainWindowViewModel](#2. MainWindowViewModel)
- [4. MVVM项目优化](#4. MVVM项目优化)
-
- [1. BaseViewModel](#1. BaseViewModel)
- [2. RealyCommand](#2. RealyCommand)
- [3. 效果展示](#3. 效果展示)
- 总结
一、MVVM框架
1. 概述
官方文档:https://learn.microsoft.com/zh-cn/dotnet/architecture/maui/mvvm

2. 核心组件与优势
一、MVVM项目
1.普通项目
2. MVVM架构

3. MVVM项目实例
1. 项目准备
以一个简单的图书管理系统为例,包含登录和图书删除两个功能。
Model创建Book、DAL、User,内容如下:
csharp
namespace Model
{
//书籍类
public class Book
{
public Book(string name, string isbn, string author)
{
Name = name;
Isbn = isbn;
Author = author;
}
public string Name { get; set; }
public string Isbn { get; set; }
public string Author { get; set; }
}
}
csharp
namespace Model
{
//数据访问层,操作数据库
public class DAL
{
//模拟查询数据库书籍表数据
public ObservableCollection<Book> GetBookList()
{
ObservableCollection<Book> list = new ObservableCollection<Book>();
list.Add(new Book("Java高级编程", "111", "李老师"));
list.Add(new Book("C++高级编程", "222", "黄老师"));
list.Add(new Book("C#高级编程", "333", "马老师"));
return list;
}
//模拟查询数据库用户表数据
public List<User> GetUserList()
{
List<User> list = new List<User>();
list.Add(new User("admin", "123", "1111111"));
list.Add(new User("张三", "123", "2222222"));
return list;
}
//通过用户名查找用户
public User FindUserByName(String name)
{
List<User> users = GetUserList();
foreach (User u in users)
{
if (u.Name == name)
return u;
}
return null;
}
}
csharp
namespace Model
{
//用户类
public class User
{
public User(string name, string pwd, string phone)
{
Name = name;
Pwd = pwd;
Phone = phone;
}
public string Name { get; set; }
public string Pwd { get; set; }
public string Phone { get; set; }
}
}
View创建Login,内容如下:
xml
<Grid>
<Label x:Name="label" Content="用户名" HorizontalAlignment="Left" Height="30" Margin="245,115,0,0" VerticalAlignment="Top" Width="100"/>
<TextBox x:Name="textBoxName" Text="" HorizontalAlignment="Left" Height="30" Margin="360,115,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="195"/>
<Label x:Name="label2" Content="密码" HorizontalAlignment="Left" Height="30" Margin="245,175,0,0" VerticalAlignment="Top" Width="100" RenderTransformOrigin="0.402,2.66"/>
<TextBox x:Name="textBoxPwd" Text="" HorizontalAlignment="Left" Height="30" Margin="360,175,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="195"/>
<Button x:Name="buttonLogin" Content="登录" HorizontalAlignment="Left" Height="40" Margin="340,310,0,0" VerticalAlignment="Top" Width="140" />
<Label x:Name="labelError" Content="" HorizontalAlignment="Left" Height="33" Margin="225,235,0,0" VerticalAlignment="Top" Width="390" Foreground="#FFF30303"/>
</Grid>
2. LoginViewModel与Login
ViewModel创建LoginViewModel,为界面上的用户名和密码输入框设计属性,并继承InotifyPertyChanged接口,实现数据双向更新
接来对窗口使用DataContext进行上下文数据绑定。
将View绑定到ViewModel上
创建LoginCommand类,继承接口ICommand并实现。
其对LoginCommand类的Execute函数改造,实现命令处理逻辑,以及检查登录信息是否正确。

csharp
using Model;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
namespace ViewModel
{
//自定义命令类
class LoginCommand : ICommand
{
public event EventHandler CanExecuteChanged;
private LoginViewModel vm;
public LoginCommand(LoginViewModel vm)
{
this.vm = vm;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
//Execute方法中实现命令处理逻辑
string Name = vm.Name;
string Pwd = vm.Pwd;
if(string.IsNullOrEmpty(vm.Name) || string.IsNullOrEmpty(vm.Pwd))
{
vm.Error = "用户名密码不能为空!";
return;
}
//检查登录信息
User u = vm.dal.FindUserByName(vm.Name);
if (u != null && u.Pwd == vm.Pwd)
{
Window w = parameter as Window;
w.DialogResult = true;//关闭窗口
}
else
vm.Error = "用户名密码错误!";
}
}
internal class LoginViewModel : INotifyPropertyChanged
{
//属性对应,界面上用户名TextBox控件
private string name = "admin";
public string Name
{
get { return name; }
set
{
name = value;
OnPropertyChanged("Name");
}
}
//属性对应,界面上密码TextBox控件
private string pwd;
public string Pwd
{
get { return pwd; }
set
{
pwd = value;
OnPropertyChanged("Pwd");
}
}
//属性对应,界面上报错Label控件
private string error = "暂无信息";
public string Error
{
get { return error; }
set
{
error = value;
OnPropertyChanged("Error");
}
}
//命令属性,对应界面上的登录事件
public LoginCommand LoginCommand { get; set; }
//数据访问层
public DAL dal = new DAL();
public event PropertyChangedEventHandler PropertyChanged;
//通知属性更改
public void OnPropertyChanged(string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
//构造函数
public LoginViewModel()
{
LoginCommand = new LoginCommand(this);
}
}
}
xml
<Window x:Class="View.Login"
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:View"
mc:Ignorable="d"
Title="Login" Height="450" Width="800" FontSize="18" Closing="Window_Closing">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="5*"/>
</Grid.ColumnDefinitions>
<Label x:Name="label" Content="用户名" HorizontalAlignment="Left" Height="30" Margin="245,115,0,0" VerticalAlignment="Top" Width="100" Grid.ColumnSpan="2"/>
<TextBox x:Name="textBoxName" Text="{Binding Name}" HorizontalAlignment="Left" Height="30" Margin="60,115,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="195" Grid.Column="1"/>
<Label x:Name="label2" Content="密码" HorizontalAlignment="Left" Height="30" Margin="245,175,0,0" VerticalAlignment="Top" Width="100" RenderTransformOrigin="0.402,2.66" Grid.ColumnSpan="2"/>
<TextBox x:Name="textBoxPwd" Text="{Binding Pwd}" HorizontalAlignment="Left" Height="30" Margin="60,175,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="195" Grid.Column="1"/>
<Button x:Name="buttonLogin" Content="登录"
Command="{Binding LoginCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window} } }"
HorizontalAlignment="Left" Height="40" Margin="40,310,0,0" VerticalAlignment="Top" Width="140" Grid.Column="1" />
<Label x:Name="labelError" Content="{Binding Error}" HorizontalAlignment="Left" Height="33" Margin="225,235,0,0" VerticalAlignment="Top" Width="390" Foreground="#FFF30303" Grid.ColumnSpan="2"/>
</Grid>
</Window>
2. MainWindowViewModel
在ViewModel创建图书管理的MainWindowViewModel,实现图书删除。
接来对窗口使用DataContext进行上下文数据绑定。
将View绑定到ViewModel上

创建DelCommand类,继承接口ICommand并实现。
其对DelCommand类的Execute函数改造,实现命令处理逻辑,删除书籍。
csharp
using Model;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace ViewModel
{
class DelCommand : ICommand
{
public event EventHandler CanExecuteChanged;
private MainWindowViewModel vm;
public DelCommand(MainWindowViewModel vm)
{
this.vm = vm;
}
public bool CanExecute(object parameter)
{
return true;
}
//传入的参数是命令的绑定的参数Book类的对象
public void Execute(object parameter)
{
Book b = parameter as Book;
//删除执行的内容
vm.Books.Remove(b);
}
}
internal class MainWindowViewModel : INotifyPropertyChanged
{
//数据访问层
public DAL dal = new DAL();
public MainWindowViewModel()
{
Books = dal.GetBookList() ;
DelCommand = new DelCommand(this);
}
//对应界面上的 ListView显示的数据
//ObservableCollection 可以通知界面更新的
private ObservableCollection<Book> books;
public ObservableCollection<Book> Books
{
get { return books; }
set {
books = value;
OnPropertyChanged("Books");
}
}
//删除命令 ,对应界面的删除按钮的点击操作
public DelCommand DelCommand { get; set; }
//通知属性已经更改
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
xml
<Window x:Class="View.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:View"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800" FontSize="18" >
<Grid>
<ListView x:Name="listView"
ItemsSource="{Binding Books}"
HorizontalAlignment="Center" Height="275" VerticalAlignment="Center" Width="556" d:ItemsSource="{d:SampleData ItemCount=5}">
<ListView.View>
<GridView>
<GridViewColumn Header="书名" Width="200" DisplayMemberBinding="{Binding Name}" />
<GridViewColumn Header="ISBN" Width="100" DisplayMemberBinding="{Binding Isbn}" />
<GridViewColumn Header="作者" Width="Auto" DisplayMemberBinding="{Binding Author }" />
<GridViewColumn Header="操作" Width="Auto" >
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button Width="60" Content="删除"
Command="{Binding RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type Window}},Path=DataContext.DelCommand}"
CommandParameter="{Binding }" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
4. MVVM项目优化

1. BaseViewModel

2. RealyCommand

3. 效果展示

修改LoginCommand
修改DelCommand
下面为优化后的实际代码
创建BaseViewModel
csharp
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ViewModel
{
internal class BaseViewMode : INotifyPropertyChanged
{
//通知属性已经更改
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
创建RelayCommand
csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace ViewModel
{
internal class RelayCommand : ICommand
{
//命令执行的函数,参数为object,无返回值的
private Action<object> executeAction;
//判断命名能否执行的函数,参数为object,返回值为 bool
private Func<object, bool> canExecuteFunc;
public event EventHandler CanExecuteChanged;
//构造函数传入参数,以后不用为每个命令单独写一个类了,每个命令都可以使用这个类
//只不过执行的内容不同
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
executeAction = execute;
canExecuteFunc = canExecute;
}
public bool CanExecute(object parameter)
{
if (canExecuteFunc != null)
{
return canExecuteFunc(parameter);
}
return true;
}
public void Execute(object parameter)
{
if (executeAction != null)
{
executeAction(parameter);
}
}
}
}
优化LoginViewModel
csharp
using Model;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
namespace ViewModel
{
internal class LoginViewModel : BaseViewMode
{
//构造函数
public LoginViewModel()
{
LoginCommand = new RelayCommand(parameter =>
{
//Execute方法中实现命令处理逻辑
if (string.IsNullOrEmpty(Name) || string.IsNullOrEmpty(Pwd))
{
Error = "用户名密码不能为空!";
return;
}
//检查登录信息
User u = dal.FindUserByName(Name);
if (u != null && u.Pwd == Pwd)
{
Window w = parameter as Window;
w.DialogResult = true;//关闭窗口
}
else
Error = "用户名密码错误!";
});
}
//属性对应,界面上用户名TextBox控件
private string name = "admin";
public string Name
{
get { return name; }
set
{
name = value;
OnPropertyChanged("Name");
}
}
//属性对应,界面上密码TextBox控件
private string pwd;
public string Pwd
{
get { return pwd; }
set
{
pwd = value;
OnPropertyChanged("Pwd");
}
}
//属性对应,界面上报错Label控件
private string error = "暂无信息";
public string Error
{
get { return error; }
set
{
error = value;
OnPropertyChanged("Error");
}
}
//命令属性,对应界面上的登录事件
public ICommand LoginCommand { get; set; }
//数据访问层
public DAL dal = new DAL();
}
}
优化MainWindowViewModel
csharp
using Model;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace ViewModel
{
class DelCommand : ICommand
{
public event EventHandler CanExecuteChanged;
private MainWindowViewModel vm;
public DelCommand(MainWindowViewModel vm)
{
this.vm = vm;
}
public bool CanExecute(object parameter)
{
return true;
}
//传入的参数是命令的绑定的参数Book类的对象
public void Execute(object parameter)
{
Book b = parameter as Book;
//删除执行的内容
vm.Books.Remove(b);
}
}
internal class MainWindowViewModel : INotifyPropertyChanged
{
//数据访问层
public DAL dal = new DAL();
public MainWindowViewModel()
{
Books = dal.GetBookList() ;
DelCommand = new DelCommand(this);
}
//对应界面上的 ListView显示的数据
//ObservableCollection 可以通知界面更新的
private ObservableCollection<Book> books;
public ObservableCollection<Book> Books
{
get { return books; }
set {
books = value;
OnPropertyChanged("Books");
}
}
//删除命令 ,对应界面的删除按钮的点击操作
public DelCommand DelCommand { get; set; }
//通知属性已经更改
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
总结
- MVVM有效的帮助界面与实现的分离,易维护,可重用,易测试,可并行开发
- ViewModel继承InotifyPertyChanged接口,实现数据双向更新
- 继承接口ICommand实现自定义命令替代事件,对Execute函数改造,实现命令处理逻辑。
- ObservableCollection类表示一个动态数据集合,它实现了INotifyPropertyChanged 接囗的数据集合,在添加项、移除项或刷新整个列表时,此集合将提供通知。