顾乔芝士网

持续更新的前后端开发技术栈

MVVM 模式:Commands 与 INotifyPropertyChanged

在 MVVM(Model–View–ViewModel)架构中,ViewModel 扮演了视图(View)和业务模型(Model)之间的“桥梁”角色,它通过两大机制将数据和用户操作双向绑定到界面:

  1. INotifyPropertyChanged:让 ViewModel 中的属性变化能自动通知 View 更新。
  2. ICommand(Commands):将用户的操作(按钮点击、菜单命令等)封装成命令对象,并暴露给 View 绑定。

下面分步介绍,并给出最常见的 WPF/C# 实现示例。

——

1. ViewModelBase 与 INotifyPropertyChanged

所有需要被视图绑定并在属性改变时通知界面的 ViewModel 通常继承一个基类,它实现了 INotifyPropertyChanged:

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace MyApp.ViewModels
{
    public abstract class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;

        protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        /// <summary>
        /// Helper to set backing field and raise notification only if value changed.
        /// </summary>
        protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
        {
            if (Equals(field, value)) return false;
            field = value;
            RaisePropertyChanged(propertyName);
            return true;
        }
    }
}
  • INotifyPropertyChanged 中的 PropertyChanged 事件是 WPF 数据绑定框架监听的“源”。
  • RaisePropertyChanged 用来触发界面更新,[CallerMemberName] 让调用者不必手动传属性名。
  • SetProperty 辅助方法可避免重复赋值和手动写通知。

——

2. ICommand 与 RelayCommand(命令绑定)

WPF 中所有可绑定的命令都实现了 ICommand 接口,它定义了两个方法和一个事件:

public interface ICommand
{
    bool CanExecute(object? parameter);
    void Execute(object? parameter);
    event EventHandler? CanExecuteChanged;
}

一个常见的通用命令实现叫 RelayCommand 或 DelegateCommand:

using System;
using System.Windows.Input;

namespace MyApp.Commands
{
    public class RelayCommand : ICommand
    {
        private readonly Action<object?> _execute;
        private readonly Func<object?, bool>? _canExecute;

        public RelayCommand(Action<object?> execute, Func<object?, bool>? canExecute = null)
        {
            _execute    = execute    ?? throw new ArgumentNullException(nameof(execute));
            _canExecute = canExecute;
        }

        public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;

        public void Execute(object? parameter) => _execute(parameter);

        public event EventHandler? CanExecuteChanged;

        /// <summary>
        /// 通知 CanExecute 状态已改变,视图会重新查询 CanExecute。
        /// </summary>
        public void RaiseCanExecuteChanged() =>
            CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }
}
  • 构造时传入执行逻辑 _execute 和可选的授权逻辑 _canExecute。
  • RaiseCanExecuteChanged 调用后,WPF 会重新启用/禁用关联控件(如按钮)。

——

3. 在 ViewModel 中使用属性与命令

using MyApp.Commands;
using System;
using System.Threading.Tasks;
using System.Windows.Input;

namespace MyApp.ViewModels
{
    public class MainViewModel : ViewModelBase
    {
        private string _status = "Ready";
        public string Status
        {
            get => _status;
            set => SetProperty(ref _status, value);
        }

        public ICommand StartCommand  { get; }
        public ICommand CancelCommand { get; }

        private bool _canCancel;
        public bool CanCancel
        {
            get => _canCancel;
            set
            {
                if (SetProperty(ref _canCancel, value))
                    // 当取消条件变化时,通知命令重新评估
                    (CancelCommand as RelayCommand)?.RaiseCanExecuteChanged();
            }
        }

        public MainViewModel()
        {
            StartCommand  = new RelayCommand(async _ => await StartWorkAsync(), _ => !CanCancel);
            CancelCommand = new RelayCommand(_ => CancelWork(),     _ => CanCancel);
        }

        private async Task StartWorkAsync()
        {
            CanCancel = true;
            Status    = "Working...";
            try
            {
                // 模拟异步任务
                await Task.Delay(5000);
                Status = "Completed";
            }
            catch (OperationCanceledException)
            {
                Status = "Canceled";
            }
            finally
            {
                CanCancel = false;
            }
        }

        private void CancelWork()
        {
            // 这里应调用 CancellationTokenSource.Cancel() 等
            Status = "Cancel requested";
        }
    }
}
  • Status 属性变更时自动通知界面更新。
  • StartCommand 和 CancelCommand 通过 CanExecute 控制按钮的可用状态。
  • 当 CanCancel 改变时,会调用 RaiseCanExecuteChanged(),让按钮根据 CanExecute 逻辑启用/禁用。

——

4. 在 XAML 中绑定

<Window x:Class="MyApp.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:vm="clr-namespace:MyApp.ViewModels"
        Title="MVVM Demo" Height="200" Width="300">

    <Window.DataContext>
        <vm:MainViewModel/>
    </Window.DataContext>

    <StackPanel Margin="20" VerticalAlignment="Center">
        <!-- 双向绑定文本并实时更新 ViewModel -->
        <TextBox Text="{Binding Status, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                 Margin="0,0,0,10"/>
        <!-- 命令绑定:按钮点击时调用 ICommand.Execute -->
        <Button Content="Start"
                Command="{Binding StartCommand}"
                Margin="0,0,0,5"/>
        <Button Content="Cancel"
                Command="{Binding CancelCommand}"/>
    </StackPanel>
</Window>
  • DataContext 指定了 ViewModel 实例。
  • Text="{Binding Status, Mode=TwoWay}":双向绑定,用户输入也会写回 Status。
  • Command="{Binding StartCommand}":将按钮点击事件自动映射到 ICommand.Execute,并由 CanExecute 控制按钮启用状态。

——

小结

  • ViewModel 实现 INotifyPropertyChanged 通知属性变化,View 可自动更新。
  • 通过 ICommand 封装用户操作,View 通过命令绑定将按钮、菜单项等与逻辑解耦。
  • XAML 只负责声明绑定关系,不包含业务逻辑,极大提高了代码可测试性和复用性。
控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言