深入理解WPF MVVM模式:从概念到实战

什么是MVVM模式?

MVVM(Model-View-ViewModel)是一种专门为WPF和XAML平台设计的软件架构模式。它起源于2005年微软的WPF和Silverlight平台,是一种基于MVC的改进模式,专门为数据绑定丰富的应用程序设计,现已成为WPF开发的事实标准。

MVVM的核心思想是关注点分离,它将应用程序分为以下三个主要部分:

名称 职责描述 特点
Model

数据模型层,负责业务逻辑和数据操作

  • 包含业务实体和数据访问逻辑

  • 独立于表示层

  • 实现数据验证和业务规则

View 用户界面层,负责视觉呈现和用户交互
  • 通过DataBinding与ViewModel连接

  • 不包含业务逻辑

  • 使用XAML声明式语言

  • 支持命令绑定和事件触发

ViewModel 视图的状态和行为抽象
  • 包含视图的显示逻辑

  • 实现INotifyPropertyChanged接口

  • 提供命令(ICommand)处理用户操作

  • 不直接引用View

其架构图如下所示

MVVM

从架构图我们可以看到数据分为正向和反向两种

正向数据我们可以看到

  • 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上设置绑定的数据时,数据源也会同步更新

所以,数据绑定的原理可以简单概括为:

  1. 建立连接:绑定对象记录下数据源和目标。

  2. 监听变化:绑定对象监听数据源的变化事件(如果数据源支持)和目标的变化事件(根据绑定模式)。

  3. 更新数据:当一方发生变化时,绑定对象就更新另一方。

这样,数据和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控件,发送信号提醒更新。

image

其中:

  • PropertyChangedEventHandler 是预定义的委托类型

  • 初始值为 null(还没有被订阅)

  • WPF绑定系统会自动"订阅"这个事件

  • OnPropertyChanged 方法是一个辅助方法,用于安全地触发 PropertyChanged 事件。它检查事件是否被订阅(不为null),然后触发事件。

这样当View控件属性值改变,就可以同步更新了。

另外Binding的Mode属性决定了数据流的方向和绑定更新的时机

  1. OneWay(单向绑定):当源属性变化时,目标属性会自动更新。但是,目标属性的更改不会更新源。

  2. TwoWay(双向绑定):源属性变化时更新目标,目标属性变化时也会更新源。

  3. OneTime(一次性绑定):仅在绑定创建时更新目标属性,之后源属性的变化不会影响目标。适用于静态数据。

  4. OneWayToSource(反向单向绑定):与OneWay相反,当目标属性变化时更新源属性,但源属性的变化不会影响目标。

  5. Default(默认模式):根据目标属性的实际情况决定绑定模式。例如,TextBox.Text默认为TwoWay,而TextBlock.Text默认为OneWay。

posted @ 2025-11-18 15:34  竹林溪风  阅读(39)  评论(0)    收藏  举报