wpf

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

WPF

WPF

Trigger On

触发场景

  • working on WPF UI, MVVM, binding, commands, or desktop modernization
  • migrating WPF from .NET Framework to .NET
  • integrating newer Windows capabilities into a WPF app
  • implementing data binding, styles, templates, or control customization
  • 开发WPF UI、MVVM、绑定、命令或进行桌面现代化
  • 将WPF从.NET Framework迁移至.NET
  • 在WPF应用中集成Windows新功能
  • 实现数据绑定、样式、模板或控件自定义

Documentation

文档资料

References

参考资料

  • patterns.md - MVVM patterns, binding patterns, command patterns, and reusable architectural approaches
  • anti-patterns.md - Common WPF mistakes and how to avoid them
  • patterns.md - MVVM模式、绑定模式、命令模式及可复用架构方案
  • anti-patterns.md - 常见WPF错误及规避方法

Workflow

工作流程

  1. Confirm Windows-only scope — WPF is Windows-only even when the wider .NET stack is cross-platform
  2. Apply MVVM pattern — keep views dumb, logic in ViewModels, use commands
  3. Manage data binding explicitly — choose correct binding modes, validate at runtime
  4. Use styles and templates deliberately — keep UI composable, avoid page-specific hacks
  5. Handle threading correctly — use Dispatcher for UI updates, async/await for long operations
  6. Validate both designer and runtime — XAML composition failures often surface only at runtime
  1. 确认Windows专属范围 —— 即使.NET整体技术栈支持跨平台,WPF仍仅适用于Windows系统
  2. 应用MVVM模式 —— 视图仅负责展示,业务逻辑置于ViewModel中,使用命令实现交互
  3. 显式管理数据绑定 —— 选择正确的绑定模式,在运行时进行验证
  4. 合理使用样式与模板 —— 保持UI可组合性,避免页面特定的临时解决方案
  5. 正确处理线程 —— 使用Dispatcher更新UI,通过async/await处理耗时操作
  6. 同时验证设计器与运行时 —— XAML组合错误通常仅在运行时才会显现

Current Upstream Notes

当前上游说明

  • The August 2026 WPF overview reiterates WPF as a Windows-only desktop UI stack with XAML, data binding, styling, templates, resources, and vector/rich-media composition. Keep WPF-specific guidance separate from WinUI or MAUI unless the task is explicitly a migration or comparison.
  • WPF exists on both .NET Framework and modern .NET. For modernization work, inventory compatibility constraints and use the current desktop migration guidance before moving project files, interop, deployment, or XAML resource dictionaries.
  • 2026年8月的WPF概述重申,WPF是基于XAML、数据绑定、样式、模板、资源及矢量/富媒体组合的Windows专属桌面UI技术栈。除非任务明确涉及迁移或对比,否则需将WPF特定指导与WinUI或MAUI区分开。
  • WPF同时存在于.NET Framework和现代.NET平台。进行现代化工作时,需先梳理兼容性约束,遵循当前桌面迁移指南,再迁移项目文件、互操作代码、部署配置或XAML资源字典。

Project Structure

项目结构

MyWpfApp/
├── MyWpfApp/
│   ├── App.xaml                # Application entry
│   ├── MainWindow.xaml         # Main window
│   ├── Views/                  # XAML views/windows
│   ├── ViewModels/             # MVVM ViewModels
│   ├── Models/                 # Domain models
│   ├── Services/               # Business logic
│   ├── Converters/             # Value converters
│   ├── Resources/              # Styles, templates, dictionaries
│   └── Controls/               # Custom controls
└── MyWpfApp.Tests/
MyWpfApp/
├── MyWpfApp/
│   ├── App.xaml                # 应用入口
│   ├── MainWindow.xaml         # 主窗口
│   ├── Views/                  # XAML视图/窗口
│   ├── ViewModels/             # MVVM视图模型
│   ├── Models/                 # 领域模型
│   ├── Services/               # 业务逻辑服务
│   ├── Converters/             # 值转换器
│   ├── Resources/              # 样式、模板、字典
│   └── Controls/               # 自定义控件
└── MyWpfApp.Tests/

MVVM Pattern

MVVM模式

ViewModel with MVVM Toolkit

使用MVVM Toolkit的ViewModel

csharp
public partial class CustomersViewModel : ObservableObject
{
    private readonly ICustomerService _customerService;

    [ObservableProperty]
    private ObservableCollection<Customer> _customers = [];

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
    private Customer? _selectedCustomer;

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(RefreshCommand))]
    private bool _isLoading;

    public CustomersViewModel(ICustomerService customerService)
    {
        _customerService = customerService;
    }

    [RelayCommand(CanExecute = nameof(CanRefresh))]
    private async Task RefreshAsync()
    {
        IsLoading = true;
        try
        {
            var items = await _customerService.GetAllAsync();
            Customers = new ObservableCollection<Customer>(items);
        }
        finally
        {
            IsLoading = false;
        }
    }

    private bool CanRefresh() => !IsLoading;

    [RelayCommand(CanExecute = nameof(CanSave))]
    private async Task SaveAsync()
    {
        if (SelectedCustomer is null) return;
        await _customerService.SaveAsync(SelectedCustomer);
    }

    private bool CanSave() => SelectedCustomer is not null;
}
csharp
public partial class CustomersViewModel : ObservableObject
{
    private readonly ICustomerService _customerService;

    [ObservableProperty]
    private ObservableCollection<Customer> _customers = [];

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
    private Customer? _selectedCustomer;

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(RefreshCommand))]
    private bool _isLoading;

    public CustomersViewModel(ICustomerService customerService)
    {
        _customerService = customerService;
    }

    [RelayCommand(CanExecute = nameof(CanRefresh))]
    private async Task RefreshAsync()
    {
        IsLoading = true;
        try
        {
            var items = await _customerService.GetAllAsync();
            Customers = new ObservableCollection<Customer>(items);
        }
        finally
        {
            IsLoading = false;
        }
    }

    private bool CanRefresh() => !IsLoading;

    [RelayCommand(CanExecute = nameof(CanSave))]
    private async Task SaveAsync()
    {
        if (SelectedCustomer is null) return;
        await _customerService.SaveAsync(SelectedCustomer);
    }

    private bool CanSave() => SelectedCustomer is not null;
}

View Binding

视图绑定

xml
<Window x:Class="MyWpfApp.Views.CustomersView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:MyWpfApp.ViewModels"
        d:DataContext="{d:DesignInstance Type=vm:CustomersViewModel}">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <ToolBar Grid.Row="0">
            <Button Content="Refresh"
                    Command="{Binding RefreshCommand}"/>
            <Button Content="Save"
                    Command="{Binding SaveCommand}"/>
        </ToolBar>

        <DataGrid Grid.Row="1"
                  ItemsSource="{Binding Customers}"
                  SelectedItem="{Binding SelectedCustomer}"
                  AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Name"
                                    Binding="{Binding Name}"/>
                <DataGridTextColumn Header="Email"
                                    Binding="{Binding Email}"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>
xml
<Window x:Class="MyWpfApp.Views.CustomersView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:MyWpfApp.ViewModels"
        d:DataContext="{d:DesignInstance Type=vm:CustomersViewModel}">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <ToolBar Grid.Row="0">
            <Button Content="Refresh"
                    Command="{Binding RefreshCommand}"/>
            <Button Content="Save"
                    Command="{Binding SaveCommand}"/>
        </ToolBar>

        <DataGrid Grid.Row="1"
                  ItemsSource="{Binding Customers}"
                  SelectedItem="{Binding SelectedCustomer}"
                  AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Name"
                                    Binding="{Binding Name}"/>
                <DataGridTextColumn Header="Email"
                                    Binding="{Binding Email}"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

Dependency Injection

依赖注入

csharp
public partial class App : Application
{
    private readonly IHost _host;

    public App()
    {
        _host = Host.CreateDefaultBuilder()
            .ConfigureServices((context, services) =>
            {
                // Services
                services.AddSingleton<ICustomerService, CustomerService>();
                services.AddSingleton<INavigationService, NavigationService>();

                // ViewModels
                services.AddTransient<CustomersViewModel>();
                services.AddTransient<CustomerDetailViewModel>();

                // Views
                services.AddTransient<MainWindow>();
                services.AddTransient<CustomersView>();
            })
            .Build();
    }

    protected override async void OnStartup(StartupEventArgs e)
    {
        await _host.StartAsync();
        var mainWindow = _host.Services.GetRequiredService<MainWindow>();
        mainWindow.Show();
        base.OnStartup(e);
    }

    protected override async void OnExit(ExitEventArgs e)
    {
        await _host.StopAsync();
        _host.Dispose();
        base.OnExit(e);
    }
}
csharp
public partial class App : Application
{
    private readonly IHost _host;

    public App()
    {
        _host = Host.CreateDefaultBuilder()
            .ConfigureServices((context, services) =>
            {
                // 服务
                services.AddSingleton<ICustomerService, CustomerService>();
                services.AddSingleton<INavigationService, NavigationService>();

                // 视图模型
                services.AddTransient<CustomersViewModel>();
                services.AddTransient<CustomerDetailViewModel>();

                // 视图
                services.AddTransient<MainWindow>();
                services.AddTransient<CustomersView>();
            })
            .Build();
    }

    protected override async void OnStartup(StartupEventArgs e)
    {
        await _host.StartAsync();
        var mainWindow = _host.Services.GetRequiredService<MainWindow>();
        mainWindow.Show();
        base.OnStartup(e);
    }

    protected override async void OnExit(ExitEventArgs e)
    {
        await _host.StopAsync();
        _host.Dispose();
        base.OnExit(e);
    }
}

Data Binding Modes

数据绑定模式

xml
<!-- OneTime: Read once at initialization -->
<TextBlock Text="{Binding CreatedDate, Mode=OneTime}"/>

<!-- OneWay: Source to target only (default for most properties) -->
<TextBlock Text="{Binding Name, Mode=OneWay}"/>

<!-- TwoWay: Bidirectional synchronization -->
<TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>

<!-- OneWayToSource: Target to source only -->
<TextBox Text="{Binding SearchFilter, Mode=OneWayToSource}"/>
xml
<!-- OneTime: 初始化时读取一次 -->
<TextBlock Text="{Binding CreatedDate, Mode=OneTime}"/>

<!-- OneWay: 仅从源到目标(大多数属性的默认模式) -->
<TextBlock Text="{Binding Name, Mode=OneWay}"/>

<!-- TwoWay: 双向同步 -->
<TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>

<!-- OneWayToSource: 仅从目标到源 -->
<TextBox Text="{Binding SearchFilter, Mode=OneWayToSource}"/>

Value Converters

值转换器

csharp
public class BooleanToVisibilityConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is bool boolValue)
        {
            return boolValue ? Visibility.Visible : Visibility.Collapsed;
        }
        return Visibility.Collapsed;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value is Visibility.Visible;
    }
}

// Multi-value converter
public class MultiplyConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length == 2 && values[0] is double a && values[1] is double b)
        {
            return a * b;
        }
        return 0.0;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
csharp
public class BooleanToVisibilityConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is bool boolValue)
        {
            return boolValue ? Visibility.Visible : Visibility.Collapsed;
        }
        return Visibility.Collapsed;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value is Visibility.Visible;
    }
}

// 多值转换器
public class MultiplyConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length == 2 && values[0] is double a && values[1] is double b)
        {
            return a * b;
        }
        return 0.0;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Styles and Templates

样式与模板

Resource Dictionary

资源字典

xml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <!-- Implicit style for all Buttons -->
    <Style TargetType="Button">
        <Setter Property="Padding" Value="10,5"/>
        <Setter Property="Margin" Value="5"/>
        <Setter Property="Background" Value="#0078D4"/>
        <Setter Property="Foreground" Value="White"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Border Background="{TemplateBinding Background}"
                            CornerRadius="4"
                            Padding="{TemplateBinding Padding}">
                        <ContentPresenter HorizontalAlignment="Center"
                                          VerticalAlignment="Center"/>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <!-- Named style -->
    <Style x:Key="DangerButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
        <Setter Property="Background" Value="#D32F2F"/>
    </Style>
</ResourceDictionary>
xml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <!-- 所有Button的隐式样式 -->
    <Style TargetType="Button">
        <Setter Property="Padding" Value="10,5"/>
        <Setter Property="Margin" Value="5"/>
        <Setter Property="Background" Value="#0078D4"/>
        <Setter Property="Foreground" Value="White"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Border Background="{TemplateBinding Background}"
                            CornerRadius="4"
                            Padding="{TemplateBinding Padding}">
                        <ContentPresenter HorizontalAlignment="Center"
                                          VerticalAlignment="Center"/>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <!-- 命名样式 -->
    <Style x:Key="DangerButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
        <Setter Property="Background" Value="#D32F2F"/>
    </Style>
</ResourceDictionary>

Threading and Dispatcher

线程与Dispatcher

csharp
// Update UI from background thread
await Task.Run(async () =>
{
    var data = await LoadDataAsync();

    // Must use Dispatcher to update UI
    Application.Current.Dispatcher.Invoke(() =>
    {
        Items.Clear();
        foreach (var item in data)
        {
            Items.Add(item);
        }
    });
});

// Better: Use async/await properly
private async Task LoadDataAsync()
{
    IsLoading = true;
    try
    {
        // This runs on background thread
        var data = await _service.GetDataAsync();

        // This automatically marshals to UI thread
        Items = new ObservableCollection<Item>(data);
    }
    finally
    {
        IsLoading = false;
    }
}
csharp
// 从后台线程更新UI
await Task.Run(async () =>
{
    var data = await LoadDataAsync();

    // 必须使用Dispatcher更新UI
    Application.Current.Dispatcher.Invoke(() =>
    {
        Items.Clear();
        foreach (var item in data)
        {
            Items.Add(item);
        }
    });
});

// 更佳方式:正确使用async/await
private async Task LoadDataAsync()
{
    IsLoading = true;
    try
    {
        // 此部分在后台线程运行
        var data = await _service.GetDataAsync();

        // 自动切换回UI线程
        Items = new ObservableCollection<Item>(data);
    }
    finally
    {
        IsLoading = false;
    }
}

Anti-Patterns to Avoid

需要避免的反模式

Anti-PatternWhy It's BadBetter Approach
Logic in code-behindHard to test, tight couplingUse MVVM with ViewModels
Synchronous blocking callsUI freezesUse async/await
Manual INotifyPropertyChangedBoilerplate, error-proneUse MVVM Toolkit attributes
Hardcoded colors/sizesInconsistent, hard to themeUse resource dictionaries
Direct Dispatcher.Invoke everywhereComplex, error-pronePrefer async/await marshaling
God ViewModelUnmaintainableSplit into focused ViewModels
Skipping binding validationRuntime errors hiddenUse ValidatesOnDataErrors
Event handlers for everythingMemory leaks, couplingUse commands and bindings
Anti-Pattern问题所在更佳方案
代码后置中包含逻辑难以测试,耦合紧密使用MVVM模式,将逻辑置于ViewModel
同步阻塞调用UI冻结使用async/await
手动实现INotifyPropertyChanged冗余代码多,易出错使用MVVM Toolkit属性
硬编码颜色/尺寸UI不一致,难以主题化使用资源字典
随处直接调用Dispatcher.Invoke复杂度高,易出错优先使用async/await自动线程切换
全能ViewModel难以维护拆分为专注特定功能的ViewModel
跳过绑定验证运行时错误被隐藏使用ValidatesOnDataErrors
所有交互都使用事件处理器内存泄漏,耦合紧密使用命令与绑定

Best Practices

最佳实践

  1. Use compiled bindings in .NET 5+:
    • Enable
      x:CompileBindings="True"
      for performance
  2. Implement INotifyDataErrorInfo for validation:
    csharp
    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required(ErrorMessage = "Name is required")]
    [MinLength(2, ErrorMessage = "Name must be at least 2 characters")]
    private string _name = string.Empty;
  3. Use weak event patterns for long-lived subscriptions:
    csharp
    WeakEventManager<Source, EventArgs>.AddHandler(source, "EventName", Handler);
  4. Virtualize large collections:
    xml
    <ListBox VirtualizingPanel.IsVirtualizing="True"
             VirtualizingPanel.VirtualizationMode="Recycling"
             ItemsSource="{Binding LargeCollection}"/>
  5. Freeze Freezables when possible:
    csharp
    var brush = new SolidColorBrush(Colors.Blue);
    brush.Freeze(); // Thread-safe, better performance
  6. Use design-time data:
    xml
    <Window d:DataContext="{d:DesignInstance Type=vm:MainViewModel, IsDesignTimeCreatable=True}">
  1. 在.NET 5+中使用编译绑定:
    • 启用
      x:CompileBindings="True"
      提升性能
  2. 实现INotifyDataErrorInfo进行验证:
    csharp
    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required(ErrorMessage = "名称为必填项")]
    [MinLength(2, ErrorMessage = "名称长度至少为2个字符")]
    private string _name = string.Empty;
  3. 对长期订阅使用弱事件模式:
    csharp
    WeakEventManager<Source, EventArgs>.AddHandler(source, "EventName", Handler);
  4. 虚拟化大型集合:
    xml
    <ListBox VirtualizingPanel.IsVirtualizing="True"
             VirtualizingPanel.VirtualizationMode="Recycling"
             ItemsSource="{Binding LargeCollection}"/>
  5. 尽可能冻结Freezable对象:
    csharp
    var brush = new SolidColorBrush(Colors.Blue);
    brush.Freeze(); // 线程安全,性能更优
  6. 使用设计时数据:
    xml
    <Window d:DataContext="{d:DesignInstance Type=vm:MainViewModel, IsDesignTimeCreatable=True}">

Testing

测试

csharp
[Fact]
public async Task RefreshCommand_LoadsCustomers()
{
    var mockService = new Mock<ICustomerService>();
    mockService.Setup(s => s.GetAllAsync())
        .ReturnsAsync(new[] { new Customer { Name = "Test" } });

    var viewModel = new CustomersViewModel(mockService.Object);

    await viewModel.RefreshCommand.ExecuteAsync(null);

    Assert.Single(viewModel.Customers);
    Assert.Equal("Test", viewModel.Customers[0].Name);
}

[Fact]
public void SaveCommand_CannotExecute_WhenNoSelection()
{
    var mockService = new Mock<ICustomerService>();
    var viewModel = new CustomersViewModel(mockService.Object);

    viewModel.SelectedCustomer = null;

    Assert.False(viewModel.SaveCommand.CanExecute(null));
}
csharp
[Fact]
public async Task RefreshCommand_LoadsCustomers()
{
    var mockService = new Mock<ICustomerService>();
    mockService.Setup(s => s.GetAllAsync())
        .ReturnsAsync(new[] { new Customer { Name = "Test" } });

    var viewModel = new CustomersViewModel(mockService.Object);

    await viewModel.RefreshCommand.ExecuteAsync(null);

    Assert.Single(viewModel.Customers);
    Assert.Equal("Test", viewModel.Customers[0].Name);
}

[Fact]
public void SaveCommand_CannotExecute_WhenNoSelection()
{
    var mockService = new Mock<ICustomerService>();
    var viewModel = new CustomersViewModel(mockService.Object);

    viewModel.SelectedCustomer = null;

    Assert.False(viewModel.SaveCommand.CanExecute(null));
}

Deliver

交付成果

  • cleaner WPF views and view-model boundaries
  • safer binding and threading behavior
  • migration guidance grounded in actual Windows constraints
  • MVVM pattern with testable ViewModels
  • 更简洁的WPF视图与视图模型边界
  • 更安全的绑定与线程处理行为
  • 基于实际Windows约束的迁移指南
  • 具备可测试性的MVVM模式实现

Validate

验证要点

  • binding and command flows are explicit
  • code-behind is not carrying hidden business logic
  • Windows-only assumptions are acknowledged
  • threading and dispatcher usage is correct
  • styles and resources are properly organized
  • 绑定与命令流程清晰明确
  • 代码后置未包含隐藏的业务逻辑
  • 已确认Windows专属的前提假设
  • 线程与Dispatcher使用正确
  • 样式与资源组织合理