-
C#关于MVVM模式——数据绑定与视图分离
第二部分:桌面应用开发
13.MVVM模式——数据绑定与视图分离
实例介绍
你有没有遇到过这样的情况?之前写的任务管理工具,视图(XAML)和逻vb.net教程C#教程python教程SQL教程access 2010教程
辑(C#后台)混在一起:比如按钮点击事件直接写在MainWindow.xaml.cs里,DataGrid的数据更新要手动调用TaskDataGrid.ItemsSource = tasks。这样的代码有个大问题——耦合度太高:改个按钮逻辑要动视图代码,换个界面布局要动逻辑代码,维护起来像拆炸弹。
后来我用MVVM模式改造了这个工具:把视图和逻辑彻底分开,视图只负责显示,逻辑放在ViewModel里,用数据绑定代替手动更新,用命令代替点击事件。改造后,我要加个“批量删除”功能,只需要在ViewModel里加个命令,视图里绑定按钮就行,完全不用碰MainWindow的代码。这一节,我就带你用MVVM改造任务管理工具,掌握数据绑定和视图分离的核心技巧。
需求分析
MVVM模式要解决的核心问题:
1.视图与逻辑分离:视图(XAML)只写界面,逻辑(数据处理、业务逻辑)放在ViewModel里,互不依赖;
2.数据自动同步:ViewModel的属性变化时,视图自动更新(不用手动调用控件的Update方法);
3.命令代替事件:按钮点击、菜单选择等交互,用ICommand绑定代替Click事件处理,避免视图和逻辑耦合;
4.可测试性:ViewModel可以独立测试,不用启动界面就能验证逻辑;
5.团队协作:设计师改界面(XAML),开发者改逻辑(ViewModel),互不干扰。
目标:用MVVM模式改造任务管理工具,实现“添加任务”“删除任务”功能,视图和逻辑完全分离,代码可维护性提升。
代码实现
前置条件:.NET 6+、WPF、CommunityToolkit.Mvvm(简化MVVM代码,NuGet安装);
延续之前的任务管理工具案例,Model复用TaskItem,View复用MainWindow.xaml。
准备工作:安装依赖
bash
# 安装CommunityToolkit.Mvvm(提供ObservableObject、RelayCommand等)
dotnet add package CommunityToolkit.Mvvm
场景1:定义Model(纯数据模型)
Model是纯数据载体,不包含业务逻辑,也不依赖WPF框架。
csharp
// Model:任务实体(纯数据,无逻辑)
public class TaskItem
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public DateTime DueDate { get; set; }
public bool IsCompleted { get; set; }
}
场景2:创建ViewModel(核心逻辑层)
ViewModel是视图和模型之间的桥梁,实现业务逻辑,支持数据绑定和命令。
csharp
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
using System.Windows.Input;
// ViewModel:任务管理逻辑(继承ObservableObject,自动实现INotifyPropertyChanged)
public partial class TaskViewModel : ObservableObject
{
// 1. 数据绑定属性:任务列表(用ObservableCollection,支持集合变化通知)
[ObservableProperty] // CommunityToolkit自动生成TaskList属性和PropertyChanged事件
private ObservableCollection<TaskItem> _taskList = new();
// 2. 数据绑定属性:新任务标题(双向绑定,视图输入框变化时更新ViewModel)
[ObservableProperty]
private string _newTaskTitle = string.Empty;
// 3. 数据绑定属性:新任务截止日期
[ObservableProperty]
private DateTime _newTaskDueDate = DateTime.Now.AddDays(1);
// 4. 命令:添加任务(用RelayCommand,简化ICommand实现)
[RelayCommand] // 自动生成AddTaskCommand属性
private void AddTask()
{
if (string.IsNullOrWhiteSpace(NewTaskTitle))
return; // 标题为空不添加
// 模拟添加任务到数据库(后续替换为TaskService)
var newTask = new TaskItem
{
Title = NewTaskTitle,
DueDate = NewTaskDueDate,
IsCompleted = false
};
TaskList.Add(newTask);
// 清空输入框(双向绑定,视图自动更新)
NewTaskTitle = string.Empty;
NewTaskDueDate = DateTime.Now.AddDays(1);
}
// 5. 命令:删除选中任务(带参数的RelayCommand)
[RelayCommand]
private void DeleteTask(TaskItem? selectedTask)
{
if (selectedTask == null)
return;
TaskList.Remove(selectedTask);
}
// 构造函数:加载初始数据
public TaskViewModel()
{
// 模拟从数据库加载任务(后续替换为TaskService.GetPendingTasksAsync)
TaskList.Add(new TaskItem { Title = "学习MVVM", DueDate = DateTime.Now.AddDays(3), IsCompleted = false });
TaskList.Add(new TaskItem { Title = "写ViewModel", DueDate = DateTime.Now.AddDays(5), IsCompleted = true });
}
}
场景3:视图绑定ViewModel(XAML)
视图(MainWindow.xaml)通过DataContext绑定ViewModel,用{Binding}语法绑定属性和命令。
xml
<Window x:Class="TaskManager.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="clr-namespace:TaskManager.ViewModels"
Title="任务管理工具(MVVM版)" Width="800" Height="600" StartupLocation="CenterScreen">
<!-- 设置DataContext为ViewModel(视图绑定ViewModel) -->
<Window.DataContext>
<viewModels:TaskViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- 输入区域 -->
<RowDefinition Height="*"/> <!-- 任务列表 -->
<RowDefinition Height="Auto"/> <!-- 按钮区域 -->
</Grid.RowDefinitions>
<!-- 1. 任务输入区域(双向绑定ViewModel的属性) -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="10" Spacing="10">
<TextBox Width="300" PlaceholderText="输入任务标题"
Text="{Binding NewTaskTitle, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<DatePicker Width="150" SelectedDate="{Binding NewTaskDueDate, Mode=TwoWay}"/>
<Button Content="添加任务"
Command="{Binding AddTaskCommand}"/> <!-- 绑定AddTaskCommand -->
</StackPanel>
<!-- 2. 任务列表(绑定ViewModel的TaskList) -->
<DataGrid Grid.Row="1" Margin="10"
ItemsSource="{Binding TaskList}" <!-- 绑定任务列表 -->
SelectedItem="{Binding SelectedTask, Mode=TwoWay}" <!-- 双向绑定选中项 -->
AlternatingRowBackground="#E8F4FF"
CanUserAddRows="False" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="任务标题" Binding="{Binding Title}" Width="*"/>
<DataGridTextColumn Header="截止日期" Binding="{Binding DueDate, StringFormat='yyyy-MM-dd'}" Width="150"/>
<DataGridCheckBoxColumn Header="已完成" Binding="{Binding IsCompleted}" Width="80"/>
</DataGrid.Columns>
</DataGrid>
<!-- 3. 删除按钮(绑定DeleteTaskCommand,传递选中项) -->
<Button Grid.Row="2" Content="删除选中任务" Margin="10"
Command="{Binding DeleteTaskCommand}"
CommandParameter="{Binding SelectedItem, ElementName=TaskDataGrid}"/> <!-- 传递选中项 -->
</Grid>
</Window>
场景4:后台代码简化(MainWindow.xaml.cs)
MVVM模式下,视图的后台代码几乎为空,只负责初始化窗口。
csharp
using System.Windows;
namespace TaskManager
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// 不需要任何逻辑代码!所有逻辑都在ViewModel里
}
}
}
逐行讲解
场景2:ViewModel详解
1.ObservableObject:CommunityToolkit.Mvvm提供的基类,自动实现INotifyPropertyChanged接口,当属性变化时通知视图更新。
2.[ObservableProperty]:自动生成属性的getter、setter,以及PropertyChanged事件触发。比如[ObservableProperty] private string _newTaskTitle会生成public string NewTaskTitle { get; set; },并在设置时触发PropertyChanged。
3.[RelayCommand]:自动生成ICommand属性,比如[RelayCommand] private void AddTask()会生成public ICommand AddTaskCommand { get; },按钮绑定这个Command即可。
4.ObservableCollection
5.双向绑定:NewTaskTitle是双向绑定属性,当ViewModel里设置NewTaskTitle = string.Empty时,视图的TextBox自动清空;反之,用户输入时,ViewModel的NewTaskTitle自动更新。
场景3:视图绑定详解
1.Window.DataContext:设置视图的数据源为ViewModel,所有绑定都基于这个DataContext。
2.Text="{Binding NewTaskTitle, Mode=TwoWay}":双向绑定TextBox的Text到ViewModel的NewTaskTitle属性,UpdateSourceTrigger=PropertyChanged表示用户输入时立即更新ViewModel(默认是LostFocus)。
3.Command="{Binding AddTaskCommand}":按钮绑定ViewModel的AddTaskCommand,点击按钮时自动执行AddTask方法。
4.CommandParameter="{Binding SelectedItem, ElementName=TaskDataGrid}":删除按钮传递DataGrid的选中项给DeleteTaskCommand,ViewModel的DeleteTask方法接收这个参数。
基础知识拓展
- MVVM核心概念
| 概念 | 说明 |
|---|---|
| Model | 纯数据模型,包含业务数据(如TaskItem),不依赖任何框架。 |
| View | 界面展示层(XAML),负责显示数据和接收用户输入,不包含业务逻辑。 |
| ViewModel | 逻辑处理层,连接View和Model,实现INotifyPropertyChanged(通知视图更新)和ICommand(处理用户交互)。 |
| INotifyPropertyChanged | 通知视图属性变化的接口,ViewModel实现它后,属性变化时视图自动更新。 |
| ICommand | 处理用户交互的接口,代替传统的Click事件,实现命令和视图分离。 |
- 数据绑定模式
| 模式 | 说明 | 应用场景 |
|---|---|---|
| OneWay | ViewModel→View,视图只显示数据,不修改。 | 显示任务列表、状态栏文本。 |
| TwoWay | ViewModel↔View,双向同步。 | 输入框、日期选择器等用户输入控件。 |
| OneTime | 只绑定一次,后续变化不更新。 | 静态文本(如软件版本号)。 |
| OneWayToSource | View→ViewModel,视图变化时更新ViewModel,反之不更新。 | 密码输入框(不显示ViewModel的值)。 |
- MVVM vs 传统事件驱动
| 对比项 | 传统事件驱动 | MVVM模式 |
|---|---|---|
| 耦合度 | 视图和逻辑高度耦合(Click事件处理写在MainWindow.xaml.cs)。 | 视图和逻辑完全分离(ViewModel不引用View)。 |
| 可测试性 | 必须启动界面才能测试逻辑。 | ViewModel可独立测试(直接调用方法验证结果)。 |
| 维护性 | 修改逻辑需要改视图代码,风险高。 | 修改逻辑只需改ViewModel,视图不变。 |
| 代码量 | 手动更新视图(如TaskDataGrid.ItemsSource = tasks),代码多。 | 数据绑定自动更新,代码简洁。 |
-
最佳实践
1.用CommunityToolkit.Mvvm简化代码:避免手动实现INotifyPropertyChanged和ICommand,用[ObservableProperty]和[RelayCommand]注解。
2.用ObservableCollection代替List:集合变化时视图自动更新。
3.ViewModel注入服务:通过构造函数注入TaskService、数据库上下文等,实现依赖注入,提高可测试性。
4.避免在ViewModel里引用View:ViewModel不能包含Button、TextBox等视图控件,保持纯逻辑。
总结
MVVM模式的核心是数据绑定和视图分离,通过ViewModel连接View和Model,让代码更易维护、可测试。本节的关键要点:
1.ViewModel实现INotifyPropertyChanged:属性变化时通知视图更新。
2.用ICommand代替事件:处理用户交互,实现逻辑和视图分离。
3.双向绑定:用户输入和ViewModel属性自动同步。
4.视图后台代码为空:所有逻辑放在ViewModel里,视图只负责展示。
掌握MVVM后,你可以轻松应对复杂的桌面应用开发,尤其是WPF这类支持数据绑定的框架,代码质量和开发效率会大幅提升。
本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49460.html










