深入理解WPF MVVM模式:从概念到实战
什么是MVVM模式?
MVVM(Model-View-ViewModel)是一种专门为WPF和XAML平台设计的软件架构模式。它起源于2005年微软的WPF和Silverlight平台,是一种基于MVC的改进模式,专门为数据绑定丰富的应用程序设计,现已成为WPF开发的事实标准。
MVVM的核心思想是关注点分离,它将应用程序分为以下三个主要部分:
| 名称 | 职责描述 | 特点 |
| Model |
数据模型层,负责业务逻辑和数据操作 |
|
| View | 用户界面层,负责视觉呈现和用户交互 |
|
| ViewModel | 视图的状态和行为抽象 |
|
其架构图如下所示

从架构图我们可以看到数据分为正向和反向两种
正向数据我们可以看到
-
Model提供原始数据
-
ViewModel对数据进行处理转换
-
View显示处理后的数据
反向数据我们可以看到
-
View接收用户输入
-
ViewModel处理用户命令
-
Model更新数据存储
MVVM的核心原理
上面我们大概知道了什么是MVVM以及MVVM的数据架构图和数据流向,下面我们来看一下它的核心原理。它主要从数据绑定、命令和通知机制来阐述
一、数据绑定
WPF的数据绑定机制是MVVM模式的基石。它允许View和ViewModel自动同步数据,无需编写繁琐的更新代码。我们通常将数据绑定比喻成“桥梁”或“中介”,它连接了数据源(例如业务逻辑中的数据)和UI(用户界面)。当数据发生变化时,UI自动更新;当用户通过UI修改数据时,数据源也自动更新。这样,开发者就不需要手动写代码去同步数据和UI。我们来看一个简单的例子
假如我们有一个Person的ViewModel类, 它有一个属性UserName
public string UserName
{
get { return _userName; }
set
{
_userName = value;
OnPropertyChanged(); // 通知View更新
}
}
我们可以在View中绑定它
<!-- View中的绑定 -->
<TextBlock Text="{Binding UserName}"/>
当我们运行程序,UserName的值就会显示在View上,为什么会这样呢?我们来剖析一下底层原理,
1. 首先,数据源(Person.UserName)需要能够通知绑定对象它发生了变化。这通常要求数据源实现INotifyPropertyChanged接口(下面会介绍)。
2. 当Name属性的setter被调用时,会触发PropertyChanged事件,绑定对象接收到这个事件后,就会去更新目标(TextBox.Text)
3. 如果数据绑定是双向的(Mode=TwoWay), 那么当在UI上设置绑定的数据时,数据源也会同步更新
所以,数据绑定的原理可以简单概括为:
-
建立连接:绑定对象记录下数据源和目标。
-
监听变化:绑定对象监听数据源的变化事件(如果数据源支持)和目标的变化事件(根据绑定模式)。
-
更新数据:当一方发生变化时,绑定对象就更新另一方。
这样,数据和UI就保持同步了。
二、 命令(Commands)
MVVM使用命令模式处理用户操作,替代传统的事件处理程序。WPF中的命令(Command)是一种强大的输入处理机制,它能将UI操作(如按钮点击、菜单选择)与执行逻辑解耦,并自动控制UI元素的启用状态,是实现MVVM模式的核心。
命令分为内置命令和自定义命令,下面是它的简单介绍
| 维度 | 概念 | 特点 |
| 核心概念 | WPF预定义或自定义的路由命令,通过命令绑定关联执行逻辑 | 实现ICommand接口的轻量命令,通常直接在ViewModel中定义逻辑 |
| 适用场景 | 处理与UI控件关系紧密的通用操作(如剪贴板)或在元素树中冒泡/隧道路由的事件 | MVVM模式,ViewModel需要直接控制并测试业务逻辑 |
| 优势 | 与WPF控件深度集成,支持输入手势(如快捷键),事件路由 | 与ViewModel紧密绑定,代码简洁,高可测试性和可移植性 |
下面我们根据代码来学习一下自定义命令
我们通过command来实现一个RelayCommand,它将执行逻辑和判断条件通过委托注入,直接定义在ViewModel中。
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool> _canExecute;
public RelayCommand(Action execute, Func<bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true;
public void Execute(object parameter) => _execute();
}
CommandManager.RequerySuggested 使得命令能在用户交互(如输入、焦点改变)时自动重新检查 CanExecute 状态。
下面我们来定义ViewModel的实现
public class MainViewModel : INotifyPropertyChanged
{
private string _message;
public string Message
{
get => _message;
set
{
_message = value;
OnPropertyChanged(nameof(Message));
// 通知Message属性变化,会通过CommandManager触发CanExecuteChanged重新检查
}
}
// 声明命令属性
public ICommand SaveCommand { get; }
public MainViewModel()
{
// 初始化命令,关联执行方法和能否执行的判断条件
SaveCommand = new RelayCommand(
execute: OnSave,
canExecute: CanSave
);
}
private bool CanSave()
{
// 只有当Message不为空时,"保存"按钮才可用
return !string.IsNullOrWhiteSpace(Message);
}
private void OnSave()
{
// 执行保存业务逻辑
System.Windows.MessageBox.Show($"保存成功:{Message}");
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
最后,将View的DataContext设置为这个ViewModel,并在控件上绑定命令
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<StackPanel Margin="10">
<TextBox Text="{Binding Message, UpdateSourceTrigger=PropertyChanged}"
Margin="0,0,0,10"/>
<Button Content="保存"
Command="{Binding SaveCommand}"
Height="30"/>
</StackPanel>
TextBox 的绑定设置了 UpdateSourceTrigger=PropertyChanged,这确保用户每输入一个字符,Message 属性都立即更新,从而触发 SaveCommand 的 CanExecute 重新评估,按钮的启用状态会随之立即改变。
CanExecuteChanged 是关键:这个事件通知UI更新命令的可用状态。使用 CommandManager.RequerySuggested 可以自动响应许多用户交互,但在某些特定业务逻辑状态改变时,你可能需要手动调用 CanExecuteChanged 事件来刷新UI
注意:如果你在后台线程中修改了影响 CanExecute 结果的变量,务必通过 Dispatcher 在UI线程上触发 CanExecuteChanged 事件,否则会导致跨线程访问异常
三、 通知机制(INotifyPropertyChanged)
INotifyPropertyChanged 是数据绑定的"心跳监测器",当数据变化时自动通知UI更新,让界面"活"起来!
在没有实现INotifyPropertyChanged的情况下,我们更新UI通常是手动更新,
// 传统方式:需要手动更新UI
UserName = "张三";
UpdateUserNameDisplay(); // 必须手动调用更新方法
这样很容易增加代码的复杂性和可维护性,在介绍数据绑定时我们可以看到,UserName的setter body里会多一个 OnPropertyChanged(nameof(Name)); 的方法调用。它就是实现自动更新的关键
public class Person : INotifyPropertyChanged
{
private string _name;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(nameof(Name)); // 触发通知
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
这段代码的关键是
// 这行代码创建了一个"广播频道"
public event PropertyChangedEventHandler PropertyChanged;
它是数据变化的信号发射器,当数据发生变化时,它向WPF绑定系统和UI控件,发送信号提醒更新。

其中:
-
PropertyChangedEventHandler是预定义的委托类型 -
初始值为
null(还没有被订阅) -
WPF绑定系统会自动"订阅"这个事件
-
OnPropertyChanged方法是一个辅助方法,用于安全地触发PropertyChanged事件。它检查事件是否被订阅(不为null),然后触发事件。
这样当View控件属性值改变,就可以同步更新了。
另外Binding的Mode属性决定了数据流的方向和绑定更新的时机
-
OneWay(单向绑定):当源属性变化时,目标属性会自动更新。但是,目标属性的更改不会更新源。
-
TwoWay(双向绑定):源属性变化时更新目标,目标属性变化时也会更新源。
-
OneTime(一次性绑定):仅在绑定创建时更新目标属性,之后源属性的变化不会影响目标。适用于静态数据。
-
OneWayToSource(反向单向绑定):与OneWay相反,当目标属性变化时更新源属性,但源属性的变化不会影响目标。
-
Default(默认模式):根据目标属性的实际情况决定绑定模式。例如,TextBox.Text默认为TwoWay,而TextBlock.Text默认为OneWay。

浙公网安备 33010602011771号