在 MVVM(Model–View–ViewModel)架构中,ViewModel 扮演了视图(View)和业务模型(Model)之间的“桥梁”角色,它通过两大机制将数据和用户操作双向绑定到界面:
- INotifyPropertyChanged:让 ViewModel 中的属性变化能自动通知 View 更新。
- 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 只负责声明绑定关系,不包含业务逻辑,极大提高了代码可测试性和复用性。