WPF + MvvmLight 完整技术指南与实战项目

0 阅读12分钟

第一部分:MvvmLight 框架深度解析

1.1 框架定位与现状

MvvmLight 框架介绍

MvvmLight 是由 Laurent Bugnion 于 2009 年创建的开源 MVVM 框架,曾是 .NET 平台最主流的 MVVM 解决方案之一。

核心特性
特性说明
轻量级核心库仅约 200KB,采用无侵入式设计,对项目架构无强依赖
易用性内置 ViewModelBaseRelayCommand 核心组件,大幅简化 MVVM 模式的开发成本
实用性集成 Messenger 消息机制,完美解决 ViewModel 之间的通信问题
⚠️ 重要维护提示
  • 维护状态:已进入遗留维护模式,最后一个主要版本发布于 2018 年
  • 兼容性:基于 .NET Standard 2.0 构建,可兼容 .NET Core、.NET 5+ 及后续版本
  • 官方建议:微软官方推荐新项目优先使用 CommunityToolkit.Mvvm 替代

1.2 核心架构原理

三层架构图解

image

关键技术原理

(1)属性通知机制(INotifyPropertyChanged)

MvvmLight 的 ViewModelBase 继承 ObservableObject(属性通知) + ICleanup(资源清理)方法:

/// <summary>
/// MVVM 模式中 ViewModel 类的基类
/// 继承 ObservableObject(属性通知) + ICleanup(资源清理)
/// </summary>
public abstract class ViewModelBase : ObservableObject, ICleanup
{
    /// <summary>
    /// 消息器实例(用于组件间消息通信)
    /// </summary>
    private IMessenger _messengerInstance;

    /// <summary>
    /// 判断当前是否处于设计模式(VS/Blend 设计界面中)
    /// </summary>
    public bool IsInDesignMode => IsInDesignModeStatic;

    /// <summary>
    /// 静态属性:全局判断是否处于设计模式
    /// </summary>
    public static bool IsInDesignModeStatic => DesignerLibrary.IsInDesignMode;

    /// <summary>
    /// 消息器实例属性
    /// 如果未手动设置,则使用框架默认的全局消息器
    /// </summary>
    protected IMessenger MessengerInstance
    {
        get
        {
            return _messengerInstance ?? Messenger.Default;
        }
        set
        {
            _messengerInstance = value;
        }
    }

    /// <summary>
    /// 默认构造函数
    /// </summary>
    public ViewModelBase()
        : this(null)
    {
    }

    /// <summary>
    /// 带消息器参数的构造函数
    /// </summary>
    /// <param name="messenger">自定义消息器实例</param>
    public ViewModelBase(IMessenger messenger)
    {
        MessengerInstance = messenger;
    }

    /// <summary>
    /// 资源清理方法(ICleanup 接口实现)
    /// 作用:注销当前实例的所有消息注册,防止内存泄漏
    /// </summary>
    public virtual void Cleanup()
    {
        MessengerInstance.Unregister(this);
    }

    /// <summary>
    /// 广播属性变化消息
    /// 用于跨ViewModel/组件通知属性变更
    /// </summary>
    /// <typeparam name="T">属性类型</typeparam>
    /// <param name="oldValue">旧值</param>
    /// <param name="newValue">新值</param>
    /// <param name="propertyName">属性名</param>
    protected virtual void Broadcast<T>(T oldValue, T newValue, string propertyName)
    {
        // 创建属性变化消息
        PropertyChangedMessage<T> message = new PropertyChangedMessage<T>(this, oldValue, newValue, propertyName);
        // 通过消息器发送消息
        MessengerInstance.Send(message);
    }

    /// <summary>
    /// 触发属性变更通知 + 可选广播消息
    /// [CallerMemberName]:自动获取调用的属性名
    /// </summary>
    /// <typeparam name="T">属性类型</typeparam>
    /// <param name="propertyName">属性名</param>
    /// <param name="oldValue">旧值</param>
    /// <param name="newValue">新值</param>
    /// <param name="broadcast">是否广播消息</param>
    public virtual void RaisePropertyChanged<T>([CallerMemberName] string propertyName = null, T oldValue = default(T), T newValue = default(T), bool broadcast = false)
    {
        // 不允许空属性名
        if (string.IsNullOrEmpty(propertyName))
        {
            throw new ArgumentException("This method cannot be called with an empty string", "propertyName");
        }

        // 触发基础的属性变更通知(刷新UI)
        RaisePropertyChanged(propertyName);
        
        // 如果开启广播,则发送消息
        if (broadcast)
        {
            Broadcast(oldValue, newValue, propertyName);
        }
    }

    /// <summary>
    /// 表达式树方式触发属性变更通知 + 可选广播消息
    /// 强类型,避免字符串错误
    /// </summary>
    /// <typeparam name="T">属性类型</typeparam>
    /// <param name="propertyExpression">属性表达式</param>
    /// <param name="oldValue">旧值</param>
    /// <param name="newValue">新值</param>
    /// <param name="broadcast">是否广播消息</param>
    public virtual void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression, T oldValue, T newValue, bool broadcast)
    {
        // 触发基础通知
        RaisePropertyChanged(propertyExpression);
        
        // 开启广播则发送消息
        if (broadcast)
        {
            string propertyName = ObservableObject.GetPropertyName(propertyExpression);
            Broadcast(oldValue, newValue, propertyName);
        }
    }

    /// <summary>
    /// 赋值 + 通知 + 消息广播(表达式树版本)
    /// </summary>
    /// <typeparam name="T">属性类型</typeparam>
    /// <param name="propertyExpression">属性表达式</param>
    /// <param name="field">后台字段</param>
    /// <param name="newValue">新值</param>
    /// <param name="broadcast">是否广播</param>
    /// <returns>是否发生值变更</returns>
    protected bool Set<T>(Expression<Func<T>> propertyExpression, ref T field, T newValue, bool broadcast)
    {
        // 值未变化,不执行操作
        if (EqualityComparer<T>.Default.Equals(field, newValue))
        {
            return false;
        }

        T oldValue = field;
        field = newValue;
        // 触发通知 + 广播
        RaisePropertyChanged(propertyExpression, oldValue, field, broadcast);
        return true;
    }

    /// <summary>
    /// 赋值 + 通知 + 消息广播(字符串属性名版本)
    /// </summary>
    /// <typeparam name="T">属性类型</typeparam>
    /// <param name="propertyName">属性名</param>
    /// <param name="field">后台字段</param>
    /// <param name="newValue">新值</param>
    /// <param name="broadcast">是否广播</param>
    /// <returns>是否发生值变更</returns>
    protected bool Set<T>(string propertyName, ref T field, T newValue = default(T), bool broadcast = false)
    {
        if (EqualityComparer<T>.Default.Equals(field, newValue))
        {
            return false;
        }

        T oldValue = field;
        field = newValue;
        RaisePropertyChanged(propertyName, oldValue, field, broadcast);
        return true;
    }

    /// <summary>
    /// 最常用赋值方法:自动获取属性名 + 可选消息广播
    /// 简化ViewModel属性编写
    /// </summary>
    /// <typeparam name="T">属性类型</typeparam>
    /// <param name="field">后台字段</param>
    /// <param name="newValue">新值</param>
    /// <param name="broadcast">是否广播消息</param>
    /// <param name="propertyName">属性名(自动获取)</param>
    /// <returns>是否发生值变更</returns>
    protected bool Set<T>(ref T field, T newValue = default(T), bool broadcast = false, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, newValue))
        {
            return false;
        }

        T oldValue = field;
        field = newValue;
        RaisePropertyChanged(propertyName, oldValue, field, broadcast);
        return true;
    }
}

ObservableObject:

// 可观察对象基类,实现 INotifyPropertyChanged 接口
// 作用:属性变化时通知UI自动刷新
public class ObservableObject : INotifyPropertyChanged
{
    // 给子类提供 PropertyChanged 事件的访问入口
    protected PropertyChangedEventHandler PropertyChangedHandler => this.PropertyChanged;

    // 属性变更事件:属性值改变后触发
    public event PropertyChangedEventHandler PropertyChanged;

    // 验证属性名是否存在
    // 仅在DEBUG模式生效,防止属性改名后报错
    [Conditional("DEBUG")]
    [DebuggerStepThrough]
    public void VerifyPropertyName(string propertyName)
    {
        // 获取当前类的类型信息
        TypeInfo typeInfo = GetType().GetTypeInfo();
        
        // 属性名为空 或 当前类直接包含该属性 → 直接返回
        if (string.IsNullOrEmpty(propertyName) || (object)typeInfo.GetDeclaredProperty(propertyName) != null)
        {
            return;
        }

        // 递归检查父类是否包含该属性
        bool flag = false;
        while ((object)typeInfo.BaseType != typeof(object))
        {
            typeInfo = typeInfo.BaseType.GetTypeInfo();
            if ((object)typeInfo.GetDeclaredProperty(propertyName) != null)
            {
                flag = true;
                break;
            }
        }

        // 整个继承链都找不到该属性 → 抛出异常
        if (!flag)
        {
            throw new ArgumentException("Property not found", propertyName);
        }
    }

    // 触发属性变更通知
    // [CallerMemberName]:自动获取调用者的属性名
    public virtual void RaisePropertyChanged([CallerMemberName] string propertyName = null)
    {
        // 触发事件通知UI
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    // 用表达式树的方式触发属性变更通知(强类型,无字符串)
    public virtual void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression)
    {
        if (this.PropertyChanged != null)
        {
            // 从表达式中提取属性名
            string propertyName = GetPropertyName(propertyExpression);
            if (!string.IsNullOrEmpty(propertyName))
            {
                RaisePropertyChanged(propertyName);
            }
        }
    }

    // 从表达式树中提取属性名
    // 输入:()=>this.Name  输出:"Name"
    protected static string GetPropertyName<T>(Expression<Func<T>> propertyExpression)
    {
        // 表达式不能为空
        if (propertyExpression == null)
        {
            throw new ArgumentNullException("propertyExpression");
        }

        // 把表达式解析为属性,并返回属性名
        return ((((propertyExpression.Body as MemberExpression) 
            ?? throw new ArgumentException("Invalid argument", "propertyExpression")).Member as PropertyInfo) 
            ?? throw new ArgumentException("Argument is not a property", "propertyExpression")).Name;
    }

    // 赋值并触发通知(表达式树版本)
    protected bool Set<T>(Expression<Func<T>> propertyExpression, ref T field, T newValue)
    {
        // 值没变化 → 不处理
        if (EqualityComparer<T>.Default.Equals(field, newValue))
        {
            return false;
        }

        // 更新值
        field = newValue;
        // 触发通知
        RaisePropertyChanged(propertyExpression);
        return true;
    }

    // 赋值并触发通知(属性名字符串版本)
    protected bool Set<T>(string propertyName, ref T field, T newValue)
    {
        // 值没变化 → 不处理
        if (EqualityComparer<T>.Default.Equals(field, newValue))
        {
            return false;
        }

        // 更新值
        field = newValue;
        // 触发通知
        RaisePropertyChanged(propertyName);
        return true;
    }

    // 最常用赋值方法:自动获取属性名,简化调用
    protected bool Set<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null)
    {
        return Set(propertyName, ref field, newValue);
    }
}

(2)命令绑定原理(ICommand)

RelayCommand 实现了 ICommand 接口,将方法包装为命令对象:

/// <summary>
/// 中继命令:WPF命令绑定核心类
/// 作用:把按钮等控件的点击事件 绑定到ViewModel的方法
/// </summary>
public class RelayCommand : ICommand
{
    // 要执行的方法(弱引用,防止内存泄漏)
    private readonly WeakAction _execute;
    
    // 判断命令是否可执行的条件(弱引用)
    private readonly WeakFunc<bool> _canExecute;

    // 命令可执行状态改变时触发的事件(界面按钮自动启用/禁用)
    public event EventHandler CanExecuteChanged;

    /// <summary>
    /// 构造方法:只传执行方法,默认命令永远可执行
    /// </summary>
    public RelayCommand(Action execute, bool keepTargetAlive = false)
        : this(execute, null, keepTargetAlive) { }

    /// <summary>
    /// 构造方法:传 执行方法 + 是否可执行方法
    /// </summary>
    public RelayCommand(Action execute, Func<bool> canExecute, bool keepTargetAlive = false)
    {
        // 执行方法不能为空,否则抛异常
        if (execute == null) throw new ArgumentNullException("execute");

        // 保存执行逻辑
        _execute = new WeakAction(execute, keepTargetAlive);
        
        // 保存可执行判断逻辑
        if (canExecute != null)
            _canExecute = new WeakFunc<bool>(canExecute, keepTargetAlive);
    }

    /// <summary>
    /// 手动触发命令状态刷新(让界面重新判断按钮是否可用)
    /// </summary>
    public void RaiseCanExecuteChanged()
    {
        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }

    /// <summary>
    /// 判断命令是否可以执行
    /// </summary>
    public bool CanExecute(object parameter)
    {
        // 如果有可执行判断条件,就执行判断
        if (_canExecute != null)
        {
            if (_canExecute.IsStatic || _canExecute.IsAlive)
                return _canExecute.Execute();

            return false;
        }
        
        // 没有条件,默认永远可执行
        return true;
    }

    /// <summary>
    /// 执行命令(按钮点击时调用)
    /// </summary>
    public virtual void Execute(object parameter)
    {
        // 可执行 + 方法有效 → 调用绑定的方法
        if (CanExecute(parameter) && _execute != null && (_execute.IsStatic || _execute.IsAlive))
            _execute.Execute();
    }
}

(3)消息通信机制(Messenger)

基于弱引用(WeakReference)的事件总线,避免内存泄漏:

public static class Messenger
{
    // 注册表结构:Dictionary<Type, List<WeakActionAndToken>>
    
    public static void Register<TMessage>(object recipient, Action<TMessage> action, object token = null)
    {
        // 使用 WeakReference 包装 recipient,避免强引用导致内存泄漏
        var weakReference = new WeakReference(recipient);
        // 存储到注册表...
    }
    
    public static void Send<TMessage>(TMessage message, object token = null)
    {
        // 1. 根据 TMessage 类型查找注册表
        // 2. 遍历所有弱引用
        // 3. 检查引用目标是否存活(IsAlive)
        // 4. 调用 action(message)
    }
    
    // 发送消息给特定接收者
    public static void Send<TMessage, TTarget>(TMessage message, TTarget target, object token = null)
}

1.3 主流 MVVM 框架深度对比(精简优化版)

核心维度MvvmLightCommunityToolkit.MvvmPrism
维护状态⚠️ 停止维护
2018年后基本停止更新,仅做兼容修复
微软官方
持续活跃维护,官方生态一部分
社区成熟
企业级稳定维护,迭代频繁
属性通知运行时反射
手写 Set() 方法
编译时源生成
[ObservableProperty] 特性,零手写
AOT 友好,性能极强
运行时反射
手写 SetProperty()
异步命令❌ 不支持
需手动封装 ICommand
原生支持
[AsyncRelayCommand] 完美支持异步命令
原生支持
内置完善的异步命令机制
代码体量中等
需手动编写大量属性与通知逻辑
极简
特性驱动,自动生成代码,开发效率最高
较全
框架体积与代码量相对较大
学习曲线平缓
入门简单,API 直观
平缓
上手极快,官方文档完善
陡峭
概念多,需学习导航、DI 等架构模式
核心优势轻量、历史用户基数大现代、高效、性能最优
官方主推,未来主流
全能、企业级
功能完备,适合大型复杂系统
适用场景⚠️ 仅维护
适合维护老旧遗留项目,不适合新项目
新项目首选
WPF/WinUI/MAUI 等现代应用开发
大型企业应用
中大型项目,需要完备架构体系

💡 一句话选型总结

  • MvvmLight已淘汰。只适合维护老项目,新项目绝对不要用。
  • CommunityToolkit.Mvvm全能首选。代码最少、性能最好、官方支持,是目前 WPF/WinUI 开发的最优解
  • Prism企业重器。功能最全,但学习成本高,适合超大型团队和复杂项目。

第二部分:完整实战项目

基于以上原理,创建一个增强版学生管理系统,包含完整的 CRUD、验证、状态提示、对话框和异步操作。

2.1 项目结构

00023.WPF + MvvmLight 完整技术指南与实战项目/
├── Helpers/                     // 消息通信辅助类
│   └── MessengerHelper.cs      // MVVM Light 消息发送与接收
├── Model/                       // 数据模型
│   └── Student.cs              // 学生实体类
├── View/                        // 界面视图
│   ├── EditStudentWindow.xaml  // 编辑学生窗口
│   │   └── EditStudentWindow.xaml.cs
│   └── MainWindow.xaml         // 主窗口
│       └── MainWindow.xaml.cs
├── ViewModel/                   // 视图模型(业务逻辑)
│   └── MainViewModel.cs        // 主窗口业务逻辑、命令、数据
├── App.xaml / App.xaml.cs       // 程序入口
├── AssemblyInfo.cs              // 程序集信息
└── ViewModelLocator.cs          // ViewModel 定位器

2.2 Model/Student.cs(实体层)

using GalaSoft.MvvmLight;

namespace _00023.WPF___MvvmLight_完整技术指南与实战项目.Model
{
    /// <summary>
    /// 学生实体模型(继承MVVM Light通知基类)
    /// </summary>
    public class Student : ObservableObject
    {
        // 私有字段
        private int _id;
        private string _name =string.Empty;
        private int _age;

        /// <summary>
        /// 学生ID
        /// </summary>
        public int Id
        {
            get => _id;
            // Set方法自动触发属性变更通知,实现UI自动更新
            set => Set(ref _id, value);
        }

        /// <summary>
        /// 学生姓名
        /// </summary>
        public string Name
        {
            get => _name;
            set => Set(ref _name, value);
        }

        /// <summary>
        /// 学生年龄
        /// </summary>
        public int Age
        {
            get => _age;
            set => Set(ref _age, value);
        }
    }
}

2.3 ViewModel/MainViewModel.cs(核心业务)

using _00023.WPF___MvvmLight_完整技术指南与实战项目.Helpers;
using _00023.WPF___MvvmLight_完整技术指南与实战项目.Model;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System.Collections.ObjectModel;
using System.Windows.Input;

namespace _00023.WPF___MvvmLight_完整技术指南与实战项目.ViewModel
{
    /// <summary>
    /// 主窗口ViewModel,处理学生列表的增删改
    /// </summary>
    public class MainViewModel : ViewModelBase
    {
        /// <summary>
        /// 学生列表(绑定到界面,自动刷新)
        /// </summary>
        public ObservableCollection<Student> Students { get; set; }

        /// <summary>
        /// 当前选中的学生
        /// </summary>
        private Student _selectedStudent;

        public Student SelectedStudent
        {
            get => _selectedStudent;
            set
            {
                Set(ref _selectedStudent, value);

                // 👇 加这一行!通知命令刷新可用状态
                ((RelayCommand)EditCommand).RaiseCanExecuteChanged();
            }
        }
        /// <summary>
        /// 界面输入的姓名
        /// </summary>
        private string _inputName;
        public string InputName
        {
            get => _inputName;
            set => Set(ref _inputName, value);
        }

        /// <summary>
        /// 界面输入的年龄
        /// </summary>
        private int _inputAge;
        public int InputAge
        {
            get => _inputAge;
            set => Set(ref _inputAge, value);
        }

        // 三个按钮命令:添加、删除、编辑
        public ICommand AddCommand { get; }
        public ICommand DeleteCommand { get; }
        public ICommand EditCommand { get; }

        /// <summary>
        /// 构造函数:初始化列表和命令
        /// </summary>
        public MainViewModel()
        {
            Students = new ObservableCollection<Student>();
            AddCommand = new RelayCommand(ExecuteAdd);
            DeleteCommand = new RelayCommand(ExecuteDelete);
            EditCommand = new RelayCommand(ExecuteEdit, CanExecuteEdit);
        }

        /// <summary>
        /// 执行添加学生
        /// </summary>
        private void ExecuteAdd()
        {
            // 输入校验:姓名不能为空,年龄必须大于0
            if (string.IsNullOrWhiteSpace(InputName) || InputAge <= 0)
                return;

            // 创建新学生对象
            var student = new Student
            {
                Id = Students.Count + 1,
                Name = InputName,
                Age = InputAge
            };

            // 添加到列表
            Students.Add(student);

            // 清空输入框
            InputName = string.Empty;
            InputAge = 0;
        }

        /// <summary>
        /// 执行删除选中学生
        /// </summary>
        private void ExecuteDelete()
        {
            if (SelectedStudent != null)
                Students.Remove(SelectedStudent);
        }

        /// <summary>
        /// 编辑按钮是否可用:必须选中学生才可用
        /// </summary>
        private bool CanExecuteEdit()
        {
            return SelectedStudent != null;
        }

        /// <summary>
        /// 执行编辑:发送消息给编辑窗口
        /// </summary>
        private void ExecuteEdit()
        {
            MessengerHelper.Send(SelectedStudent, "EditStudent");
        }

        /// <summary>
        /// 清理资源:注销消息
        /// </summary>
        public override void Cleanup()
        {
            MessengerHelper.Unregister(this);
            base.Cleanup();
        }
    }
}

2.4 ViewModelLocator.cs(MvvmLight 依赖注入)

using CommonServiceLocator;
using GalaSoft.MvvmLight.Ioc;
using _00023.WPF___MvvmLight_完整技术指南与实战项目.ViewModel;

namespace _00023.WPF___MvvmLight_完整技术指南与实战项目
{
    /// <summary>
    /// 视图模型定位器:统一管理和提供ViewModel实例
    /// </summary>
    public class ViewModelLocator
    {
        /// <summary>
        /// 对外提供 MainViewModel 实例(界面绑定用)
        /// </summary>
        public MainViewModel MainViewModel => SimpleIoc.Default.GetInstance<MainViewModel>();

        /// <summary>
        /// 构造函数:注册服务、配置Ioc容器
        /// </summary>
        public ViewModelLocator()
        {
            // 设置Ioc服务定位器
            ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
            // 注册 MainViewModel 到Ioc容器
            SimpleIoc.Default.Register<MainViewModel>();
        }
    }
}

2.5 View/MainWindow.xaml(主界面)

<Window x:Class="_00023.WPF___MvvmLight_完整技术指南与实战项目.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:conv="clr-namespace:_00023.WPF___MvvmLight_完整技术指南与实战项目.Converters"
        mc:Ignorable="d"
        DataContext="{Binding MainViewModel, Source={StaticResource Locator}}"
        Title="学生管理系统" 
        Height="520" 
        Width="850"
        WindowStartupLocation="CenterScreen"
        FontSize="14">

    <Window.Resources>
        <conv:NullToBoolConverter x:Key="NullToBoolConverter"/>
        <conv:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
        <conv:BoolInverter x:Key="BoolInverter"/>
    </Window.Resources>

    <Border Padding="25">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="15"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>

            <StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Center">
                <TextBox Text="{Binding InputName}" Width="160" Height="32" Padding="6" Margin="0 0 8 0"/>
                <TextBox Text="{Binding InputAge}" Width="120" Height="32" Padding="6" Margin="0 0 8 0"/>
                <Button Content="添加" Command="{Binding AddCommand}" Width="100" Height="32"/>
                <Button Content="编辑" Command="{Binding EditCommand}" Width="100" Height="32" Margin="8 0 0 0"
                        IsEnabled="{Binding SelectedStudent, Converter={StaticResource NullToBoolConverter}}"/>
                <Button Content="删除" Command="{Binding DeleteCommand}" Width="100" Height="32" Margin="8 0 0 0"
                        IsEnabled="{Binding SelectedStudent, Converter={StaticResource NullToBoolConverter}}"/>
            </StackPanel>

            <DataGrid Grid.Row="2"
                      ItemsSource="{Binding Students}"
                      SelectedItem="{Binding SelectedStudent}"
                      AutoGenerateColumns="True"
                      IsReadOnly="True"
                      HorizontalAlignment="Center"
                      Width="780"
                      Height="380"/>
        </Grid>
    </Border>
</Window>

2.6 MessengerHelper(消息管理最佳实践)

using GalaSoft.MvvmLight.Messaging;
using System;

namespace _00023.WPF___MvvmLight_完整技术指南与实战项目.Helpers
{
    /// <summary>
    /// 消息助手类(封装 MVVM Light 消息器)
    /// 作用:ViewModel 之间、页面之间 发送/接收消息,解耦通信
    /// </summary>
    public static class MessengerHelper
    {
        /// <summary>
        /// 注册消息监听
        /// </summary>
        /// <param name="recipient">消息接收者</param>
        /// <param name="token">消息标识(区分不同消息)</param>
        /// <param name="action">收到消息后执行的方法</param>
        public static void Register<T>(object recipient, string token, Action<T> action)
        {
            Messenger.Default.Register(recipient, token, action);
        }

        /// <summary>
        /// 发送消息
        /// </summary>
        /// <param name="message">要发送的内容</param>
        /// <param name="token">消息标识</param>
        public static void Send<T>(T message, string token)
        {
            Messenger.Default.Send(message, token);
        }

        /// <summary>
        /// 取消注册(释放监听)
        /// </summary>
        /// <param name="recipient">接收者对象</param>
        public static void Unregister(object recipient)
        {
            Messenger.Default.Unregister(recipient);
        }
    }
}

第三部分:功能清单与运行效果

功能 实现方式 技术亮点 ✅ 添加学生 输入验证 → 创建对象 → 加入 ObservableCollection 自动 UI 刷新,验证错误提示 ✅ 删除学生 选中确认对话框 → 弱引用消息通知 防止误删,操作反馈 ✅ 编辑学生 Messenger 发送消息 → 对话框显示 ViewModel 间解耦通信 image 有点简陋,但逻辑走通


第四部分:核心知识点总结

4.1 必须掌握的三核心

image

4.2 关键设计模式

属性变更通知链
// Model 继承 ObservableObject → 属性 Set() → UI 自动更新
public class Student : ObservableObject  // 在 Model 层就具备通知能力
{
    public string Name
    {
        get => _name;
        set => Set(nameof(Name), ref _name, value);  // 自动通知 UI
    }
}
命令与逻辑分离
// ViewModel 只通过 Command 暴露操作,不直接操作 UI
public ICommand AddCommand { get; }  // 绑定到 XAML
// 业务逻辑完全在 ViewModel 中处理
消息驱动对话框(解耦)
// 避免在 ViewModel 中直接引用 View(如直接实例化 Window)
Messenger.Default.Send(SelectedStudent, "EditStudent");  // 发送消息
// EditStudentViewModel 接收消息并显示对话框

建议:

  • 维护旧项目:继续使用 MvvmLight(稳定可靠)
  • 新项目启动:直接学习 CommunityToolkit.Mvvm(微软官方推荐,性能更好,支持 AOT)

👋 关注我!持续分享 C# 实战技巧、代码示例 & 技术干货

  • 获取示例代码,轻松上手!

  • 私信输入数字: k42v

  • 博主网站:tool.bugcome.com

    image