WPF 桌面应用:XAML 与 MVVM 模式

FreeGuideOnline 最新 2026-06-18

WPF 桌面应用:XAML 与 MVVM 模式

WPF(Windows Presentation Foundation)是微软推出的桌面端 UI 框架,它凭借强大的数据绑定、声明式布局和清晰的分层架构,成为 .NET 平台下企业级应用开发的首选。本教程将带领你从零开始,掌握 WPF 的核心——XAML 声明式界面与 MVVM 设计模式,并通过一个完整的实例巩固所学。

为什么选择 WPF?

与传统的 Windows Forms 相比,WPF 提供了:

  • 矢量图形渲染:界面无损缩放,支持丰富的动画和特效
  • 数据驱动型 UI:通过绑定自动同步界面与数据,减少手动更新代码
  • 样式与模板:轻松定制控件外观,实现一致的视觉风格
  • MVVM 友好:天然支持模型-视图-视图模型的分离,提升可测试性与可维护性

XAML:用声明式语言描述界面

XAML(eXtensible Application Markup Language)是 WPF 中定义界面的 XML 方言。它将 UI 元素表示为树形结构的对象,让你能够直观地“画”出界面。

<Window x:Class="TodoApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="任务管理" Height="350" Width="525">
    <Grid>
        <Button Content="添加任务" Width="100" Height="30" />
    </Grid>
</Window>

XAML 基础概念

  • 命名空间:默认命名空间映射 WPF 控件,x: 前缀用于 XAML 语言特性(如 x:Namex:Class
  • 标记扩展:以花括号 {} 表示的特殊语法,用于动态赋值,例如 {Binding}{StaticResource}
  • 属性元素:当属性值无法用字符串简洁表达时,使用嵌套的 Type.PropertyName 形式
  • 依赖属性:WPF 属性的基础,支持绑定、动画和样式继承

常用布局容器

控件 说明
Grid 行列布局,最灵活
StackPanel 水平或垂直堆叠子元素
DockPanel 停靠布局(上下左右)
WrapPanel 自动换行排列
Canvas 绝对定位,适合绘图

合理组合这些容器即可构建出响应式界面。

资源与样式

将颜色、画刷、字符串等定义为资源,可在整个应用中复用。样式则像 CSS 一样作用于控件,定义其外观与行为。

<Window.Resources>
    <Style TargetType="Button">
        <Setter Property="Background" Value="DodgerBlue"/>
        <Setter Property="Foreground" Value="White"/>
        <Setter Property="Margin" Value="5"/>
    </Style>
</Window.Resources>

MVVM 模式:构建可维护的应用架构

MVVM(Model-View-ViewModel)将应用拆分为三层:

  • Model:业务数据与逻辑,通常包含实体类、数据库访问等
  • View:纯 XAML 界面,负责展示,不包含业务逻辑
  • ViewModel:视图的抽象,通过数据绑定和命令将 Model 的数据转换为 View 可直接使用的形式
 View  ←— 数据绑定 —→  ViewModel  ←— 操作 —→  Model

这种分离带来的好处:

  • View 和 ViewModel 可独立开发、测试
  • ViewModel 轻量化,不依赖 UI 线程,易于单元测试
  • 更换界面或技术栈时只需修改 View 层

实现 MVVM 的核心基础设施

数据绑定(Data Binding)

绑定的桥梁是 DataContext。View 会从控件的 DataContext 开始向上查找绑定源。通常将窗口或控件的 DataContext 设置为一个 ViewModel 实例。

// 在窗口的构造函数或 App.xaml.cs 中
public MainWindow()
{
    InitializeComponent();
    DataContext = new MainViewModel();
}

XAML 中使用 {Binding Path=属性名} 来绑定 ViewModel 中的属性。

<TextBox Text="{Binding TaskTitle, UpdateSourceTrigger=PropertyChanged}" />

UpdateSourceTrigger 控制数据更新时机,PropertyChanged 表示每次输入变化即同步回 ViewModel。

INotifyPropertyChanged 接口

当 ViewModel 的属性值变化时,必须通知 View 更新界面。这就需要实现 INotifyPropertyChanged 接口,并在 setter 中触发 PropertyChanged 事件。为了简化代码,通常创建一个基类。

public class BaseViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

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

    protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if (Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}

之后 ViewModel 继承该类,即可轻松发出通知。

命令(ICommand)

按钮等控件的交互不能直接在 ViewModel 中写事件处理方法,而是通过命令。WPF 提供了 ICommand 接口。通常需要实现一个通用的 RelayCommand(或 DelegateCommand)。

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);
}

然后 ViewModel 暴露命令属性:

public ICommand AddTaskCommand { get; }

并在构造函数中实例化:

AddTaskCommand = new RelayCommand(_ => AddTask());

XAML 中绑定:

<Button Content="添加" Command="{Binding AddTaskCommand}" />

动手实践:构建一个简易任务管理应用

我们将创建一个极简任务列表,功能包括:添加任务、显示任务、删除已完成任务。通过这个例子,你会看到 MVVM 如何协同工作。

1. 项目结构

新建 WPF 项目,添加以下文件夹和文件:

TodoApp/
├── Models/
│   └── TaskItem.cs
├── ViewModels/
│   ├── BaseViewModel.cs
│   ├── RelayCommand.cs
│   └── MainViewModel.cs
├── Views/
│   └── MainWindow.xaml
└── App.xaml / App.xaml.cs

2. Model:任务类

// Models/TaskItem.cs
public class TaskItem
{
    public string Title { get; set; }
    public bool IsCompleted { get; set; }
}

3. ViewModel:主视图模型

// ViewModels/MainViewModel.cs
public class MainViewModel : BaseViewModel
{
    private string _newTaskTitle;
    public string NewTaskTitle
    {
        get => _newTaskTitle;
        set => SetProperty(ref _newTaskTitle, value);
    }

    public ObservableCollection<TaskItem> Tasks { get; } = new ObservableCollection<TaskItem>();

    public ICommand AddTaskCommand { get; }
    public ICommand RemoveCompletedCommand { get; }

    public MainViewModel()
    {
        AddTaskCommand = new RelayCommand(_ =>
        {
            if (!string.IsNullOrWhiteSpace(NewTaskTitle))
            {
                Tasks.Add(new TaskItem { Title = NewTaskTitle });
                NewTaskTitle = string.Empty; // 清空输入框
            }
        });

        RemoveCompletedCommand = new RelayCommand(_ =>
        {
            var completed = Tasks.Where(t => t.IsCompleted).ToList();
            foreach (var item in completed)
                Tasks.Remove(item);
        });
    }
}

注意我们使用了 ObservableCollection,它会在增删项时自动通知界面刷新列表。

4. View:XAML 界面

<!-- Views/MainWindow.xaml -->
<Window x:Class="TodoApp.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="简易任务管理" Height="400" Width="500">
    <Window.DataContext>
        <local:MainViewModel xmlns:local="clr-namespace:TodoApp.ViewModels" />
    </Window.DataContext>
    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <!-- 输入区域 -->
        <StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10">
            <TextBox Text="{Binding NewTaskTitle, UpdateSourceTrigger=PropertyChanged}"
                     Width="300" Height="25" Margin="0,0,5,0"/>
            <Button Content="添加任务" Command="{Binding AddTaskCommand}"
                    Width="100" Height="25"/>
        </StackPanel>

        <!-- 任务列表 -->
        <ListBox Grid.Row="1" ItemsSource="{Binding Tasks}" DisplayMemberPath="Title">
            <ListBox.ItemContainerStyle>
                <Style TargetType="ListBoxItem">
                    <Setter Property="IsSelected" Value="{Binding Path=IsCompleted, Mode=TwoWay}" />
                </Style>
            </ListBox.ItemContainerStyle>
        </ListBox>

        <!-- 删除按钮 -->
        <Button Grid.Row="2" Content="清除已完成任务"
                Command="{Binding RemoveCompletedCommand}"
                Width="140" Height="25" HorizontalAlignment="Right" Margin="0,5,0,0"/>
    </Grid>
</Window>

我们使用 ListBox.ItemContainerStyle 将每个列表项的 IsSelected 绑定到 TaskItem.IsCompleted,实现了点击即标记完成的效果。

5. 运行应用

  • 编译运行,输入任务标题,点击“添加”,新任务出现在列表中。
  • 单击列表中的任务,其 IsCompleted 被设为 true(选中状态)。
  • 点击“清除已完成任务”,所有已选择的项被移除。

至此,一个遵循 MVVM 的完整 WPF 应用就完成了。

MVVM 进阶与最佳实践

依赖注入与 ViewModel 定位

手动设置 DataContext 不够灵活,实际项目中常使用依赖注入(DI)容器(如 Microsoft.Extensions.DependencyInjection)管理 ViewModel,并通过一个 ViewModelLocator 进行绑定。

<Window DataContext="{Binding Main, Source={StaticResource Locator}}" />

消息通信(Messenger)

跨 ViewModel 解耦通信通常依赖消息传递库(如 CommunityToolkit.Mvvm 中的 Messenger)。可以发送和注册消息,而无需相互引用。

单元测试 ViewModel

因为 ViewModel 不依赖 UI,可以轻松编写单元测试。注入模拟的 Model 或服务,验证命令执行后状态是否正确。

[Test]
public void AddTask_WithValidTitle_ShouldAddToCollection()
{
    var vm = new MainViewModel();
    vm.NewTaskTitle = "测试任务";
    vm.AddTaskCommand.Execute(null);

    Assert.AreEqual(1, vm.Tasks.Count);
    Assert.AreEqual("测试任务", vm.Tasks[0].Title);
}

常见坑与解决方案

  • 忘记实现 INotifyPropertyChanged:属性变化界面不更新,务必在 setter 中调用通知方法。
  • 绑定源错误:检查 DataContext 树,确保绑定的路径存在且可访问。
  • 命令 CanExecute 不会自动刷新:需要手动调用 CommandManager.InvalidateRequerySuggested() 或在 RelayCommand 中监听相关变化。
  • 大量数据性能问题:对于万级列表,使用虚拟化(VirtualizingStackPanel)或增量加载。
  • 线程问题:ViewModel 中如果触发 UI 更新,必须回到 UI 线程;可以将耗时操作放在后台线程,更新集合时使用 Application.Current.Dispatcher.Invoke

总结

借助 XAML 的声明式能力和 MVVM 的分层思想,你能以极低的耦合度构建出可扩展、可测试的桌面应用。从数据绑定到命令,从简单属性通知到完整架构,每一步都体现了现代桌面开发的最佳实践。继续深入,你可以探索更高级的主题,如 Prism 框架、控件模板自定义、异步编程在 MVVM 中的应用等。

现在,打开 Visual Studio,用刚刚学到的知识去创建属于你自己的 WPF 应用吧!