C#之MVVM篇快速入门
MVVM(Model-View-ViewModel)模型-视图-视图模型
- 参考网址:
https://www.cnblogs.com/hsiang/p/15579839.html和https://www.cnblogs.com/mingupupu/p/18218027
- 【模型】指的是后端传递的数据
- 【视图】指的是前端页面
- 【视图模型】mvvm模式的核心,它是连接view和model的桥梁
- 将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。
- 将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。
- 这两个方向都实现的,我们称之为数据的双向绑定。
- 新建
WPF应用(.FW)程序 - 安装
MvvmLight插件
- 项目名称右键-->管理NuGet程序包-->搜索MvvmLight-->安装 # v5.4.1.1
- 注意事项,此插件安装以后,需要删除两处代码
// app.xaml
......
<ResourceDictionary>
<!--注释掉这句(插件自动生成)-->
<!--<vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" xmlns:vm="clr-namespace:WpfAppMVVMFWDemo1.ViewModel" />-->
</ResourceDictionary>
......
- 删除ViewModel目录(插件自动生成)底下的"ViewModelLocator.cs"文件
- 创建Model目录,新建Student类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfAppMVVMFWDemo1.Model
{
public class Student
{
// 唯一标识
public int Id { get; set; }
// 学生姓名
public string Name { get; set; }
// 年龄
public int Age { get; set; }
// 班级
public string Classes { get; set; }
}
}
- 创建
DAL目录,新建LocalDb类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WpfAppMVVMFWDemo1.Model;
namespace WpfAppMVVMFWDemo1.DAL
{
public class LocalDb
{
private List<Student> students;
public LocalDb()
{
init();
}
// 初始化数据
private void init()
{
students = new List<Student>();
for (int i = 0; i < 30; i++)
{
students.Add(new Student()
{
Id = i,
Name = string.Format("学生{0}", i),
Age = new Random(i).Next(0, 100),
Classes = i % 2 == 0 ? "一班" : "二班"
});
}
}
// 查询数据
public List<Student> Query()
{
return students;
}
// 按名字查询
public List<Student> QueryByName(string name)
{
return students.Where((t) => t.Name.Contains(name)).ToList();//FindAll((t) => t.Name.Contains(name));
}
public Student QueryById(int Id)
{
var student = students.FirstOrDefault((t) => t.Id == Id);
if (student != null)
{
return new Student()
{
Id = student.Id,
Name = student.Name,
Age = student.Age,
Classes = student.Classes
};
}
return null;
}
// 新增学生
public void AddStudent(Student student)
{
if (student != null)
{
students.Add(student);
}
}
// 删除学生
public void DelStudent(int Id)
{
var student = students.FirstOrDefault((t) => t.Id == Id); //students.Find((t) => t.Id == Id);
if (student != null)
{
students.Remove(student);
}
}
}
}
- 主窗体
MainWindow内容如下
// xaml
<Window x:Class="WpfAppMVVMFWDemo1.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:WpfAppMVVMFWDemo1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="80" />
<!--"*"表示会占据剩余所有可用空间-->
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="5" VerticalAlignment="Center">
<TextBlock Text="姓名" Margin="10" />
<TextBox x:Name="sname" Text="{Binding Search}" Width="120" Margin="10" Padding="5" />
<Button x:Name="btnQuery" Command="{Binding QueryCommand}" Content="查询" Margin="10" Padding="5" Width="80" />
<Button x:Name="btnReset" Content="重置" Margin="10" Padding="5" Width="80" Command="{Binding ResetCommand}" />
<Button x:Name="btnAdd" Content="创建" Margin="10" Padding="5" Width="80" Command="{Binding AddCommand}" />
</StackPanel>
<DataGrid Grid.Row="1" x:Name="dgInfo" ItemsSource="{Binding GridModelList}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserSortColumns="False" Margin="10" >
<DataGrid.Columns>
<DataGridTextColumn Header="Id" Width="100" Binding="{Binding Id}" />
<DataGridTextColumn Header="姓名" Width="100" Binding="{Binding Name}" />
<DataGridTextColumn Header="年龄" Width="100" Binding="{Binding Age}" />
<DataGridTextColumn Header="班级" Width="100" Binding="{Binding Classes}" />
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<Button x:Name="edit" Content="编辑" Width="60" Margin="3" Height="25" CommandParameter="{Binding Id}" Command="{Binding DataContext.EditCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}}" />
<Button x:Name="delete" Content="删除" Width="60" Margin="3" Height="25" CommandParameter="{Binding Id}" Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}}" />
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
// cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using WpfAppMVVMFWDemo1.ViewModel;
namespace WpfAppMVVMFWDemo1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
MainViewModel viewModel = new MainViewModel();
viewModel.Query();
this.DataContext = viewModel;
}
}
}
- 创建
Views目录,新建窗体StudentWindow
// xaml
<Window x:Class="WpfAppMVVMFWDemo1.Views.StudentWindow"
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:WpfAppMVVMFWDemo1.Views"
mc:Ignorable="d"
Title="StudentWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="60"></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition Height="60"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock FontSize="30" Margin="10">修改学生信息</TextBlock>
<StackPanel Grid.Row="1" Orientation="Vertical">
<TextBlock FontSize="20" Margin="10" Padding="5">姓名</TextBlock>
<TextBox x:Name="txtName" FontSize="20" Padding="5" Text="{Binding Model.Name}"></TextBox>
<TextBlock FontSize="20" Margin="10" Padding="5">年龄</TextBlock>
<TextBox x:Name="txtAge" FontSize="20" Padding="5" Text="{Binding Model.Age}"></TextBox>
<TextBlock FontSize="20" Margin="10" Padding="5">班级</TextBlock>
<TextBox x:Name="txtClasses" FontSize="20" Padding="5" Text="{Binding Model.Classes}"></TextBox>
</StackPanel>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="btnSave" Content="保存" Margin="10" FontSize="20" Width="100" Click="btnSave_Click" ></Button>
<Button x:Name="btnCancel" Content="取消" Margin="10" FontSize="20" Width="100" Click="btnCancel_Click" ></Button>
</StackPanel>
</Grid>
</Window>
// cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using WpfAppMVVMFWDemo1.Model;
namespace WpfAppMVVMFWDemo1.Views
{
/// <summary>
/// StudentWindow.xaml 的交互逻辑
/// </summary>
public partial class StudentWindow : Window
{
public StudentWindow(Student student)
{
InitializeComponent();
this.DataContext = new { Model=student };
}
private void btnSave_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = true;
}
private void btnCancel_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = false;
}
}
}
ViewModel.MainViewModel.cs
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using WpfAppMVVMFWDemo1.DAL;
using WpfAppMVVMFWDemo1.Model;
using WpfAppMVVMFWDemo1.Views;
namespace WpfAppMVVMFWDemo1.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
{
private LocalDb localDb;
private ObservableCollection<Student> gridModelList;
public ObservableCollection<Student> GridModelList
{
get { return gridModelList; }
set
{
gridModelList = value;
RaisePropertyChanged();
}
}
private string search;
public string Search
{
get { return search; }
set
{
search = value;
RaisePropertyChanged();
}
}
public MainViewModel()
{
localDb = new LocalDb();
QueryCommand = new RelayCommand(this.Query);
ResetCommand = new RelayCommand(this.Reset);
EditCommand = new RelayCommand<int>(this.Edit);
DeleteCommand = new RelayCommand<int>(this.Delete);
AddCommand = new RelayCommand(this.Add);
}
public RelayCommand QueryCommand { get; set; }
public RelayCommand ResetCommand { get; set; }
public RelayCommand<int> EditCommand { get; set; }
public RelayCommand<int> DeleteCommand { get; set; }
public RelayCommand AddCommand { get; set; }
public void Query()
{
List<Student> students;
if (string.IsNullOrEmpty(search))
{
students = localDb.Query();
}
else
{
students = localDb.QueryByName(search);
}
GridModelList = new ObservableCollection<Student>();
if (students != null)
{
students.ForEach((t) =>
{
GridModelList.Add(t);
});
}
}
public void Reset()
{
this.Search = string.Empty;
this.Query();
}
public void Edit(int Id)
{
var model = localDb.QueryById(Id);
if (model != null)
{
StudentWindow view = new StudentWindow(model);
var r = view.ShowDialog();
if (r.Value)
{
var newModel = GridModelList.FirstOrDefault(t => t.Id == model.Id);
if (newModel != null)
{
newModel.Name = model.Name;
newModel.Age = model.Age;
newModel.Classes = model.Classes;
}
this.Query();
}
}
}
public void Delete(int Id)
{
var model = localDb.QueryById(Id);
if (model != null)
{
var r = MessageBox.Show($"确定要删除吗【{model.Name}】?", "提示", MessageBoxButton.YesNo);
if (r == MessageBoxResult.Yes)
{
localDb.DelStudent(Id);
this.Query();
}
}
}
public void Add()
{
Student model = new Student();
StudentWindow view = new StudentWindow(model);
var r = view.ShowDialog();
if (r.Value)
{
model.Id = GridModelList.Max(t => t.Id) + 1;
localDb.AddStudent(model);
this.Query();
}
}
}
}
- 小结:
MVVM具有低耦合,可重用,可测试,独立开发的优点,核心要素就两个
- 属性发生变化时的通知,即可达到数据的实时更新。
- 命令是实现用户与程序之间数据和算法的桥梁。

ViewModel(视图模型)解析
- 包含视图所需的所有数据和命令
- 数据(绑定): INotifyPropertyChanged接口
- 命令(绑定): ICommand对象
public class User
{
public string? Name { get; set; }
public string? Email { get; set; }
}
public static class UserManager
{
public static ObservableCollection<User> DataBaseUsers = new ObservableCollection<User>()
{
new User() { Name = "小王", Email = "123@qq.com" },
new User() { Name = "小红", Email = "456@qq.com" },
new User() { Name = "小五", Email = "789@qq.com" }
};
public static ObservableCollection<User> GetUsers()
{
return DataBaseUsers;
}
public static void AddUser(User user)
{
DataBaseUsers.Add(user);
}
}
ObservableCollection 详解
基本概念
ObservableCollectionSystem.Collections.ObjectModel 命名空间中。它与普通集合最大的区别在于实现了数据变更通知机制。
核心特性
1. 自动通知机制
public class ObservableCollection<T> : Collection<T>, INotifyCollectionChanged, INotifyPropertyChanged
- 实现了
INotifyCollectionChanged接口 - 实现了
INotifyPropertyChanged接口 - 当集合内容变化时自动发出通知
2. 主要事件
// 集合内容变化时触发(添加、删除、移动、重置等)
public event NotifyCollectionChangedEventHandler CollectionChanged;
// 属性变化时触发(如Count属性)
public event PropertyChangedEventHandler PropertyChanged;
在示例代码中的作用
public static ObservableCollection<User> DataBaseUsers = new ObservableCollection<User>()
{
new User() { Name = "小王", Email = "123@qq.com" },
new User() { Name = "小红", Email = "456@qq.com" },
new User() { Name = "小五", Email = "789@qq.com" }
};
public static void AddUser(User user)
{
DataBaseUsers.Add(user); // 这里会触发CollectionChanged事件
}
实际应用场景
1. WPF/XAML 数据绑定
<!-- XAML 中绑定到 ObservableCollection -->
<ListBox ItemsSource="{Binding DataBaseUsers}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Email}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
2. MVVM 模式中的典型用法
public class UserViewModel
{
public ObservableCollection<User> Users { get; set; }
public UserViewModel()
{
Users = UserManager.GetUsers();
// 当UserManager中添加新用户时,UI会自动更新
}
}
与普通集合的区别
| 特性 | List |
ObservableCollection |
|---|---|---|
| 数据变更通知 | ❌ 无 | ✅ 自动通知 |
| UI自动更新 | ❌ 需要手动刷新 | ✅ 自动同步 |
| 内存占用 | 较小 | 稍大(维护事件) |
| 使用场景 | 纯数据处理 | UI数据绑定 |
触发通知的操作
var users = new ObservableCollection<User>();
// 以下操作都会触发CollectionChanged事件:
users.Add(new User()); // 添加
users.RemoveAt(0); // 删除
users[0] = new User(); // 替换
users.Move(0, 1); // 移动
users.Clear(); // 清空
事件处理示例
// 监听集合变化
DataBaseUsers.CollectionChanged += (sender, e) =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Console.WriteLine($"添加了 {e.NewItems.Count} 个用户");
break;
case NotifyCollectionChangedAction.Remove:
Console.WriteLine($"删除了 {e.OldItems.Count} 个用户");
break;
case NotifyCollectionChangedAction.Reset:
Console.WriteLine("集合被重置");
break;
}
};
优点总结
- 自动化 - UI自动同步,无需手动刷新
- 响应式 - 实时响应数据变化
- 简化代码 - 减少样板代码
- 框架集成 - 与WPF、Xamarin等完美集成
注意事项
- 线程安全:ObservableCollection不是线程安全的,跨线程操作需要调度
- 性能:频繁的大批量操作可能影响性能,可使用
AddRange扩展方法优化 - 内存:事件订阅可能导致内存泄漏,注意及时取消订阅
这就是为什么在您的代码中使用 ObservableCollection - 它使得用户界面的数据绑定变得简单而高效!
RelayComand解析
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace WpfApp2MVVMTestInit.Commands
{
public class RelayComand: ICommand
{
public event EventHandler CanExecuteChanged;
private Action<object> _Excute { get; set; }
private Predicate<object> _CanExcute { get; set; }
public RelayComand(Action<object> ExcuteMethod,Predicate<object> canExcuteMethod)
{
_Excute = ExcuteMethod;
_CanExcute = canExcuteMethod;
}
public bool CanExecute(object parameter)
{
return _CanExcute(parameter);
}
public void Execute(object parameter) {
_Excute(parameter);
}
}
}
这是一个WPF MVVM模式中常用的RelayCommand(中继命令)实现。让我逐部分解释:
类结构概述
public class RelayComand : ICommand
- 实现了
ICommand接口,这是WPF命令系统的核心 - 用于将UI操作(如按钮点击)绑定到ViewModel中的方法
核心字段
private Action<object> _Excute { get; set; }
private Predicate<object> _CanExcute { get; set; }
_Excute: 存储要执行的实际方法(无返回值,接受object参数)_CanExcute: 存储判断命令是否可执行的方法(返回bool,接受object参数)
构造函数
public RelayComand(Action<object> ExcuteMethod, Predicate<object> canExcuteMethod)
接受两个委托参数:
- 执行逻辑的方法
- 判断是否可执行的方法
ICommand接口实现
CanExecute 方法
public bool CanExecute(object parameter)
{
return _CanExcute(parameter);
}
- 决定命令是否可用(控制UI元素的启用/禁用状态)
- 返回true时按钮可用,false时按钮变灰
Execute 方法
public void Execute(object parameter) {
_Excute(parameter);
}
- 实际执行命令时调用的方法
- 包含主要的业务逻辑
CanExecuteChanged 事件
public event EventHandler CanExecuteChanged;
- 当命令的可用状态改变时触发
- WPF自动监听此事件来更新UI状态
使用示例
// 在ViewModel中
public ICommand SaveCommand { get; private set; }
public ViewModel()
{
SaveCommand = new RelayComand(
ExecuteSave, // 执行方法
CanExecuteSave // 判断是否可执行的方法
);
}
private void ExecuteSave(object parameter)
{
// 保存逻辑
MessageBox.Show("数据已保存!");
}
private bool CanExecuteSave(object parameter)
{
// 只有数据有效时才允许保存
return !string.IsNullOrEmpty(FileName);
}
XAML中的绑定
<Button Content="保存"
Command="{Binding SaveCommand}"
CommandParameter="一些参数"/>
这个RelayCommand是MVVM模式的关键组件,它解耦了UI和业务逻辑,使代码更易于测试和维护。

浙公网安备 33010602011771号