MVVM 架构

Model-View-ViewModel 架构模式完整指南

MVVM 简介

MVVM (Model-View-ViewModel) 是一种软件架构模式,专门为 UI 框架设计,实现了 UI 与业务逻辑的分离。

MVVM 架构图

Model

数据模型和业务逻辑

ViewModel

视图模型和 UI 逻辑

View

用户界面

各层职责

Model(模型层)

负责数据和业务逻辑,不包含任何 UI 相关代码。Model 类通常包含属性、方法和数据访问逻辑。

View(视图层)

负责 UI 展示,通过数据绑定与 ViewModel 交互。View 只关注外观和布局,不包含业务逻辑。

ViewModel(视图模型层)

作为 View 和 Model 之间的桥梁,处理 UI 逻辑,通过命令和属性与 View 通信。

MVVM 的优势

关注点分离

UI 逻辑与业务逻辑完全分离,代码结构清晰

可测试性

ViewModel 可以独立于 UI 进行单元测试

可维护性

代码组织清晰,易于理解和维护

设计师协作

设计师可以独立处理 XAML,不影响开发

代码复用

ViewModel 可以在多个 View 中复用

团队协作

前后端开发可以并行进行

Model 层

Model 层包含数据模型和业务逻辑,是应用程序的核心。

基础 Model 类

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public DateTime BirthDate { get; set; }
    
    public int GetAge()
    {
        var today = DateTime.Today;
        var age = today.Year - BirthDate.Year;
        if (BirthDate.Date > today.AddYears(-age)) age--;
        return age;
    }
}

带通知的 Model

public class ObservablePerson : INotifyPropertyChanged
{
    private string _name;
    
    public string Name
    {
        get => _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 interface IPersonService
{
    Task<IEnumerable<Person>> GetAllAsync();
    Task<Person> GetByIdAsync(int id);
    Task<Person> AddAsync(Person person);
    Task<Person> UpdateAsync(Person person);
    Task DeleteAsync(int id);
}

public class PersonService : IPersonService
{
    private readonly DbContext _context;
    
    public PersonService(DbContext context)
    {
        _context = context;
    }

    public async Task<IEnumerable<Person>> GetAllAsync()
    {
        return await _context.People.ToListAsync();
    }

    public async Task<Person> GetByIdAsync(int id)
    {
        return await _context.People.FindAsync(id);
    }

    public async Task<Person> AddAsync(Person person)
    {
        _context.People.Add(person);
        await _context.SaveChangesAsync();
        return person;
    }

    public async Task<Person> UpdateAsync(Person person)
    {
        _context.People.Update(person);
        await _context.SaveChangesAsync();
        return person;
    }

    public async Task DeleteAsync(int id)
    {
        var person = await _context.People.FindAsync(id);
        if (person != null)
        {
            _context.People.Remove(person);
            await _context.SaveChangesAsync();
        }
    }
}

View 层

View 层由 XAML 和代码隐藏组成,负责 UI 的展示和用户交互。

MainWindow.xaml

<Window x:Class="WpfApp.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="人员管理" Height="600" Width="900">
    
    <Grid Margin="20">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        
        <!-- 工具栏 -->
        <StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,20">
            <Button Content="添加" Command="{Binding AddCommand}" 
                    Width="80" Margin="0,0,10,0" />
            <Button Content="编辑" Command="{Binding EditCommand}" 
                    Width="80" Margin="0,0,10,0" />
            <Button Content="删除" Command="{Binding DeleteCommand}" 
                    Width="80" />
        </StackPanel>
        
        <!-- 数据网格 -->
        <DataGrid Grid.Row="1" 
                  ItemsSource="{Binding People}"
                  SelectedItem="{Binding SelectedPerson}"
                  AutoGenerateColumns="False"
                  CanUserAddRows="False">
            <DataGrid.Columns>
                <DataGridTextColumn Header="ID" Binding="{Binding Id}" Width="*" />
                <DataGridTextColumn Header="姓名" Binding="{Binding Name}" Width="2*" />
                <DataGridTextColumn Header="邮箱" Binding="{Binding Email}" Width="3*" />
                <DataGridTextColumn Header="年龄" Binding="{Binding Age}" Width="*" />
            </DataGrid.Columns>
        </DataGrid>
        
        <!-- 状态栏 -->
        <StatusBar Grid.Row="2">
            <StatusBarItem>
                <TextBlock Text="{Binding StatusMessage}" />
            </StatusBarItem>
        </StatusBar>
    </Grid>
</Window>

MainWindow.xaml.cs

using System.Windows;

namespace WpfApp.Views
{
    public partial class MainWindow : Window
    {
        public MainWindow(MainViewModel viewModel)
        {
            InitializeComponent();
            DataContext = viewModel;
        }
    }
}

UserControl 示例

<UserControl x:Class="WpfApp.Views.PersonEditView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    
    <Grid Margin="20">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        
        <TextBlock Grid.Row="0" Text="姓名:" Margin="0,0,0,5" />
        <TextBox Grid.Row="1" Text="{Binding Person.Name, UpdateSourceTrigger=PropertyChanged}" 
                 Margin="0,0,0,15" />
        
        <TextBlock Grid.Row="2" Text="邮箱:" Margin="0,0,0,5" />
        <TextBox Grid.Row="3" Text="{Binding Person.Email, UpdateSourceTrigger=PropertyChanged}" 
                 Margin="0,0,0,15" />
        
        <StackPanel Grid.Row="5" Orientation="Horizontal" HorizontalAlignment="Right">
            <Button Content="保存" Command="{Binding SaveCommand}" 
                    Width="80" Margin="0,0,10,0" />
            <Button Content="取消" Command="{Binding CancelCommand}" 
                    Width="80" />
        </StackPanel>
    </Grid>
</UserControl>

ViewModel 层

ViewModel 层是 MVVM 的核心,负责 UI 逻辑和数据绑定。

基础 ViewModel

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

public partial class MainViewModel : ObservableObject
{
    private readonly IPersonService _personService;
    
    [ObservableProperty]
    private ObservableCollection<Person> _people = new();
    
    [ObservableProperty]
    private Person _selectedPerson;
    
    [ObservableProperty]
    private string _statusMessage = "就绪";

    public IRelayCommand AddCommand { get; }
    public IRelayCommand EditCommand { get; }
    public IRelayCommand DeleteCommand { get; }
    public IAsyncRelayCommand LoadCommand { get; }

    public MainViewModel(IPersonService personService)
    {
        _personService = personService;
        
        AddCommand = new RelayCommand(Add);
        EditCommand = new RelayCommand(Edit, CanEdit);
        DeleteCommand = new RelayCommand(Delete, CanDelete);
        LoadCommand = new AsyncRelayCommand(LoadAsync);
        
        LoadCommand.Execute(null);
    }

    private void Add()
    {
        var person = new Person { Name = "新用户", Email = "" };
        _people.Add(person);
        SelectedPerson = person;
        StatusMessage = "已添加新用户";
    }

    private bool CanEdit() => SelectedPerson != null;
    
    private void Edit()
    {
        StatusMessage = $"编辑用户: {SelectedPerson.Name}";
    }

    private bool CanDelete() => SelectedPerson != null;
    
    private void Delete()
    {
        if (SelectedPerson != null)
        {
            _people.Remove(SelectedPerson);
            StatusMessage = "用户已删除";
        }
    }

    private async Task LoadAsync()
    {
        try
        {
            StatusMessage = "加载中...";
            var people = await _personService.GetAllAsync();
            People.Clear();
            foreach (var person in people)
            {
                People.Add(person);
            }
            StatusMessage = $"已加载 {People.Count} 个用户";
        }
        catch (Exception ex)
        {
            StatusMessage = $"加载失败: {ex.Message}";
        }
    }

    partial void OnSelectedPersonChanged(Person value)
    {
        EditCommand.NotifyCanExecuteChanged();
        DeleteCommand.NotifyCanExecuteChanged();
    }
}

编辑 ViewModel

public partial class PersonEditViewModel : ObservableObject
{
    [ObservableProperty]
    private Person _person;
    
    public IRelayCommand SaveCommand { get; }
    public IRelayCommand CancelCommand { get; }

    public PersonEditViewModel(Person person)
    {
        _person = person;
        
        SaveCommand = new RelayCommand(Save, CanSave);
        CancelCommand = new RelayCommand(Cancel);
    }

    private bool CanSave() => !string.IsNullOrWhiteSpace(Person.Name) && 
                             !string.IsNullOrWhiteSpace(Person.Email);

    private void Save()
    {
        StatusMessage = "保存成功";
        CloseRequested?.Invoke(this, EventArgs.Empty);
    }

    private void Cancel()
    {
        CloseRequested?.Invoke(this, EventArgs.Empty);
    }

    public event EventHandler CloseRequested;
}

DataContext

DataContext 是 WPF 数据绑定的核心,它定义了数据上下文。

设置 DataContext

// App.xaml.cs
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        
        // 配置依赖注入
        var services = new ServiceCollection();
        ConfigureServices(services);
        var serviceProvider = services.BuildServiceProvider();
        
        // 创建主窗口
        var mainWindow = serviceProvider.GetRequiredService<MainWindow>();
        mainWindow.Show();
    }

    private void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<DbContext>();
        services.AddScoped<IPersonService, PersonService>();
        services.AddTransient<MainViewModel>();
        services.AddTransient<MainWindow>();
    }
}

XAML 中设置 DataContext

<!-- 直接设置 -->
<Window.DataContext>
    <local:MainViewModel />
</Window.DataContext>

<!-- 使用资源 -->
<Window.Resources>
    <local:MainViewModel x:Key="ViewModel" />
</Window.Resources>

<!-- 使用绑定 -->
<ContentControl Content="{Binding}">
    <ContentControl.ContentTemplate>
        <DataTemplate DataType="{x:Type local:MainViewModel}">
            <local:MainView />
        </DataTemplate>
    </ContentControl.ContentTemplate>
</ContentControl>

DataContext 继承

<!-- DataContext 会向下继承 -->
<Grid DataContext="{Binding ViewModel}">
    <StackPanel>
        <!-- 继承 Grid 的 DataContext -->
        <TextBlock Text="{Binding Property1}" />
        <TextBlock Text="{Binding Property2}" />
        
        <!-- 可以覆盖继承的 DataContext -->
        <StackPanel DataContext="{Binding SubViewModel}">
            <TextBlock Text="{Binding SubProperty}" />
        </StackPanel>
    </StackPanel>
</Grid>

命令绑定

命令绑定实现了 UI 事件与 ViewModel 方法的解耦。

RelayCommand 实现

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 event EventHandler CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }

    public bool CanExecute(object parameter) => _canExecute?.Invoke(parameter) ?? true;
    
    public void Execute(object parameter) => _execute(parameter);
}

使用 CommunityToolkit.Mvvm

public partial class MainViewModel : ObservableObject
{
    [RelayCommand]
    private void Save()
    {
        // 保存逻辑
    }

    [RelayCommand(CanExecute = nameof(CanDelete))]
    private void Delete()
    {
        // 删除逻辑
    }

    private bool CanDelete() => SelectedPerson != null;

    [RelayCommand]
    private async Task LoadAsync()
    {
        // 异步加载逻辑
    }

    [RelayCommand(AllowConcurrentExecutions = false)]
    private async Task ProcessAsync()
    {
        // 防止并发执行
    }
}

XAML 中绑定命令

<Button Content="保存" Command="{Binding SaveCommand}" />

<!-- 带参数的命令 -->
<Button Content="删除" 
        Command="{Binding DeleteCommand}" 
        CommandParameter="{Binding SelectedPerson}" />

<!-- 菜单项命令 -->
<MenuItem Header="新建" Command="{Binding NewCommand}" />

<!-- 键盘快捷键 -->
<Button Content="保存" 
        Command="{Binding SaveCommand}">
    <Button.InputBindings>
        <KeyBinding Key="S" Modifiers="Ctrl" 
                   Command="{Binding SaveCommand}" />
    </Button.InputBindings>
</Button>

CommunityToolkit.Mvvm

CommunityToolkit.Mvvm 是微软官方提供的 MVVM 工具库,提供了大量实用的工具和特性。

安装

dotnet add package CommunityToolkit.Mvvm

ObservableObject

public partial class MainViewModel : ObservableObject
{
    [ObservableProperty]
    private string _title = "欢迎使用 WPF";

    [ObservableProperty]
    private int _count = 0;

    [ObservableProperty]
    private bool _isLoading = false;

    // 自动生成属性和 OnPropertyChanged 调用
}

RelayCommand

public partial class MainViewModel : ObservableObject
{
    [RelayCommand]
    private void Increment()
    {
        Count++;
    }

    [RelayCommand(CanExecute = nameof(CanSave))]
    private void Save()
    {
        // 保存逻辑
    }

    private bool CanSave() => Count > 0;

    [RelayCommand]
    private async Task LoadAsync()
    {
        IsLoading = true;
        try
        {
            await Task.Delay(1000);
        }
        finally
        {
            IsLoading = false;
        }
    }
}

ObservableValidator

public partial class PersonViewModel : ObservableValidator
{
    [ObservableProperty]
    [Required(ErrorMessage = "姓名不能为空")]
    [MinLength(2, ErrorMessage = "姓名至少2个字符")]
    private string _name;

    [ObservableProperty]
    [Required(ErrorMessage = "邮箱不能为空")]
    [EmailAddress(ErrorMessage = "邮箱格式不正确")]
    private string _email;

    [ObservableProperty]
    [Range(0, 150, ErrorMessage = "年龄必须在0-150之间")]
    private int _age;
}

Source Generators

// 使用 Source Generator 生成属性
public partial class PersonViewModel : ObservableObject
{
    [ObservableProperty]
    private string _name;

    // 编译器自动生成:
    // public string Name
    // {
    //     get => _name;
    //     set => SetProperty(ref _name, value);
    // }
}

💡 CommunityToolkit.Mvvm 优势

  • 使用 Source Generators 提高性能
  • 减少样板代码,提高开发效率
  • 内置验证功能
  • 支持异步命令
  • 微软官方维护,稳定可靠