WPF 命令 ICommand 从原理到实战

在 WPF 开发中,只要用到数据绑定,就绕不开一个核心接口:INotifyPropertyChanged。 它是实现"ViewModel 属性变化 → UI 自动更新"的基石,也是 MVVM 架构的灵魂。

一、为什么要用 INotifyPropertyChanged?

WPF 数据绑定默认是一次性绑定 : 界面加载时,ViewModel 的值会同步到控件; 但属性后续变化,UI 不会自动刷新

比如:

csharp 复制代码
public string Name { get; set; }

你在代码里改 Name = "新值",界面 TextBox 不会变。

原因很简单: UI 不知道属性变了。

INotifyPropertyChanged 的作用就是: 当属性值改变时,主动通知 UI 去更新。


二、接口定义

它位于 System.ComponentModel 命名空间,结构非常简单:

csharp 复制代码
namespace System.ComponentModel
{
    /// <summary>
    /// 通知客户端:属性值已发生变化
    /// (WPF绑定自动更新UI的核心接口)
    /// </summary>
    public interface INotifyPropertyChanged
    {
        /// <summary>
        /// 属性值改变时触发的事件
        /// UI会监听此事件自动刷新
        /// </summary>
        event PropertyChangedEventHandler? PropertyChanged;
    }
}

只有一个事件:

  • PropertyChanged:属性变化时触发,告诉绑定引擎更新界面。

三、最基础实现

csharp 复制代码
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace _00022.WPF_彻底搞懂_INotifyPropertyChanged.Core
{
    /// <summary>
    /// ViewModel 基类
    /// 实现 INotifyPropertyChanged 接口,用于WPF绑定属性通知
    /// 所有页面ViewModel都可以继承这个基类
    /// </summary>
    public class ViewModelBase : INotifyPropertyChanged
    {
        /// <summary>
        /// 属性变更事件(WPF绑定系统会监听这个事件)
        /// </summary>
        public event PropertyChangedEventHandler? PropertyChanged;

        /// <summary>
        /// 触发属性变更通知
        /// [CallerMemberName]:自动获取调用该方法的属性名,不用手动传参
        /// </summary>
        /// <param name="propertyName">属性名称</param>
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            // 如果有订阅者,就触发属性变更事件
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        /// <summary>
        /// 通用属性赋值方法
        /// 作用:判断新值是否与旧值相同,不同才赋值并触发通知
        /// 避免重复触发UI刷新,提升性能
        /// </summary>
        /// <typeparam name="T">属性类型</typeparam>
        /// <param name="field">后台私有字段</param>
        /// <param name="value">新值</param>
        /// <param name="propertyName">属性名(自动获取)</param>
        /// <returns>是否发生了值变更</returns>
        protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
        {
            // 如果新旧值相等,直接返回不做处理
            if (EqualityComparer<T>.Default.Equals(field, value))
                return false;

            // 更新字段值
            field = value;
            // 触发属性变更通知
            OnPropertyChanged(propertyName);
            return true;
        }

        // 导航命令、加载命令、全局命令可放这里

        /// <summary>
        /// 初始化方法(可重写)
        /// </summary>
        public virtual void Init() { }

        /// <summary>
        /// 清理数据方法(可重写)
        /// </summary>
        public virtual void Clear() { }
    }
}

关键点

  1. CallerMemberName 编译器自动帮你填属性名,不用手写字符串,避免写错。

  2. 必须在属性 set 中调用 OnPropertyChanged() 否则 UI 收不到通知。


四、进阶:封装通用基类 ViewModelBase

实际项目不会每个 VM 都写一遍,通常抽成基类:

csharp 复制代码
// 引入核心类(RelayCommand 所在)
using _00022.WPF_彻底搞懂_INotifyPropertyChanged.Core;
// 引入实体模型(User 类)
using _00022.WPF_彻底搞懂_INotifyPropertyChanged.Model;
using System;
// 可观察集合(列表自动刷新 UI)
using System.Collections.ObjectModel;
// WPF 命令接口
using System.Windows.Input;

// ViewModel 命名空间
namespace _00022.WPF_彻底搞懂_INotifyPropertyChanged.ViewModels
{
    /// <summary>
    /// 主页面 ViewModel
    /// 继承 ViewModelBase 拥有属性通知功能
    /// </summary>
    public class MainViewModel : ViewModelBase
    {
        // 输入框绑定的文本
        private string _inputText;
        // 选中的用户项
        private User _selectedUser;

        /// <summary>
        /// 输入框文本属性(UI 双向绑定)
        /// </summary>
        public string InputText
        {
            get => _inputText;
            set => SetProperty(ref _inputText, value);
        }

        /// <summary>
        /// 当前选中的用户(UI 绑定选中项)
        /// </summary>
        public User SelectedUser
        {
            get => _selectedUser;
            set => SetProperty(ref _selectedUser, value);
        }

        /// <summary>
        /// 用户列表(绑定到列表控件,自动刷新 UI)
        /// </summary>
        public ObservableCollection<User> Users { get; }

        /// <summary>
        /// 提交命令(按钮绑定)
        /// </summary>
        public ICommand SubmitCommand { get; }

        /// <summary>
        /// 构造函数:初始化数据 + 命令
        /// </summary>
        public MainViewModel()
        {
            // 初始化用户列表,给两条默认数据
            Users = new ObservableCollection<User>
            {
                new User { Id = 1, Name = "张三", Age = 20 },
                new User { Id = 2, Name = "李四", Age = 25 }
            };

            // 给命令赋值:点击按钮执行 UpdateRandomText
            SubmitCommand = new RelayCommand(UpdateRandomText);
        }

        /// <summary>
        /// 按钮点击执行:生成随机文本
        /// </summary>
        private void UpdateRandomText()
        {
            InputText = $"随机文本_{new Random().Next(1000, 9999)}_{_selectedUser?.Name}_{_selectedUser?.Age}";
        }

        /// <summary>
        /// 外部调用:直接更新输入框文本
        /// </summary>
        public void UpdateText(string text)
        {
            InputText = text;
        }
    }
}

使用:

csharp 复制代码
public class MainViewModel : ViewModelBase
{
    /// <summary>
    /// 输入框文本属性(UI 双向绑定)
    /// </summary>
    public string InputText
    {
         get => _inputText;
         set => SetProperty(ref _inputText, value);
    }
}

非常简洁、规范、通用。


五、XAML 绑定示例

xml 复制代码
<Grid Margin="20">
     <StackPanel>
        <TextBox Text="{Binding InputText, UpdateSourceTrigger=PropertyChanged}"
                 FontSize="18" Margin="0 0 10 0"/>

        <ListBox ItemsSource="{Binding Users}"
                 SelectedItem="{Binding SelectedUser}"
                 Height="150" Margin="0 0 10 0">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Id}" Width="30"/>
                        <TextBlock Text="{Binding Name}" Width="100"/>
                        <TextBlock Text="{Binding Age}"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <!-- 只保留 Command,去掉 Click -->
        <Button Content="随机更新文本" Command="{Binding SubmitCommand}" FontSize="16"/>
    </StackPanel>
</Grid>

后台绑定 DataContext:

csharp 复制代码
// 持有 ViewModel 实例(供界面绑定使用)
public MainViewModel ViewModel { get; }

// 窗口构造函数
public MainWindow()
{
    // 初始化界面控件
    InitializeComponent();

    // 创建 ViewModel 实例
    ViewModel = new MainViewModel();
    // 关键:将 DataContext 设置为 ViewModel
    // 让界面能绑定到 ViewModel 里的属性和命令
    DataContext = ViewModel;
}

六、常见问题

  1. UI 不更新

    • 没继承 INotifyPropertyChanged
    • 没调用 OnPropertyChanged
    • 属性名写错
  2. 输入框改了,ViewModel 收不到 TextBox 默认 LostFocus 才更新,加上: UpdateSourceTrigger=PropertyChanged

  3. 多线程更新 UI 报错 用 Dispatcher 包裹:

    csharp 复制代码
    Application.Current.Dispatcher.Invoke(() =>
    {
        Name = "新名称";
    });

七、总结

  • INotifyPropertyChanged = WPF 数据绑定的心脏
  • 所有 MVVM ViewModel 都应继承实现它
  • 封装成 ViewModelBase 是行业标准写法
  • 属性变化 → 触发事件 → UI 自动更新

掌握它,你才算真正入门 WPF 数据绑定。


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

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

  • 私信输入数字: fib9

  • 获取代码下载链接

相关推荐
止语Lab1 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活1 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困1 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn1 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
用户298698530141 小时前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
用户77283104908401 小时前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端
Conan在掘金1 小时前
鸿蒙报错速查:Function lacks ending return statement,返回路径漏 return 就炸,根因 + 真解法
后端
人间凡尔赛2 小时前
AI-Native 云原生架构:2026 年从容器编排到智能体编排的范式革命
后端·云原生·架构
IT_陈寒2 小时前
Vue的响应式让我加班到凌晨3点,原来问题出在这
前端·人工智能·后端
卷无止境2 小时前
提升 Python 代码健壮性的方法大盘点
后端·python