wpf
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWPF
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
工作流程
- Confirm Windows-only scope — WPF is Windows-only even when the wider .NET stack is cross-platform
- Apply MVVM pattern — keep views dumb, logic in ViewModels, use commands
- Manage data binding explicitly — choose correct binding modes, validate at runtime
- Use styles and templates deliberately — keep UI composable, avoid page-specific hacks
- Handle threading correctly — use Dispatcher for UI updates, async/await for long operations
- Validate both designer and runtime — XAML composition failures often surface only at runtime
- 确认Windows专属范围 —— 即使.NET整体技术栈支持跨平台,WPF仍仅适用于Windows系统
- 应用MVVM模式 —— 视图仅负责展示,业务逻辑置于ViewModel中,使用命令实现交互
- 显式管理数据绑定 —— 选择正确的绑定模式,在运行时进行验证
- 合理使用样式与模板 —— 保持UI可组合性,避免页面特定的临时解决方案
- 正确处理线程 —— 使用Dispatcher更新UI,通过async/await处理耗时操作
- 同时验证设计器与运行时 —— 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-Pattern | Why It's Bad | Better Approach |
|---|---|---|
| Logic in code-behind | Hard to test, tight coupling | Use MVVM with ViewModels |
| Synchronous blocking calls | UI freezes | Use async/await |
| Manual INotifyPropertyChanged | Boilerplate, error-prone | Use MVVM Toolkit attributes |
| Hardcoded colors/sizes | Inconsistent, hard to theme | Use resource dictionaries |
| Direct Dispatcher.Invoke everywhere | Complex, error-prone | Prefer async/await marshaling |
| God ViewModel | Unmaintainable | Split into focused ViewModels |
| Skipping binding validation | Runtime errors hidden | Use ValidatesOnDataErrors |
| Event handlers for everything | Memory leaks, coupling | Use commands and bindings |
| Anti-Pattern | 问题所在 | 更佳方案 |
|---|---|---|
| 代码后置中包含逻辑 | 难以测试,耦合紧密 | 使用MVVM模式,将逻辑置于ViewModel |
| 同步阻塞调用 | UI冻结 | 使用async/await |
| 手动实现INotifyPropertyChanged | 冗余代码多,易出错 | 使用MVVM Toolkit属性 |
| 硬编码颜色/尺寸 | UI不一致,难以主题化 | 使用资源字典 |
| 随处直接调用Dispatcher.Invoke | 复杂度高,易出错 | 优先使用async/await自动线程切换 |
| 全能ViewModel | 难以维护 | 拆分为专注特定功能的ViewModel |
| 跳过绑定验证 | 运行时错误被隐藏 | 使用ValidatesOnDataErrors |
| 所有交互都使用事件处理器 | 内存泄漏,耦合紧密 | 使用命令与绑定 |
Best Practices
最佳实践
-
Use compiled bindings in .NET 5+:
- Enable for performance
x:CompileBindings="True"
- Enable
-
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; -
Use weak event patterns for long-lived subscriptions:csharp
WeakEventManager<Source, EventArgs>.AddHandler(source, "EventName", Handler); -
Virtualize large collections:xml
<ListBox VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling" ItemsSource="{Binding LargeCollection}"/> -
Freeze Freezables when possible:csharp
var brush = new SolidColorBrush(Colors.Blue); brush.Freeze(); // Thread-safe, better performance -
Use design-time data:xml
<Window d:DataContext="{d:DesignInstance Type=vm:MainViewModel, IsDesignTimeCreatable=True}">
-
在.NET 5+中使用编译绑定:
- 启用提升性能
x:CompileBindings="True"
- 启用
-
实现INotifyDataErrorInfo进行验证:csharp
[ObservableProperty] [NotifyDataErrorInfo] [Required(ErrorMessage = "名称为必填项")] [MinLength(2, ErrorMessage = "名称长度至少为2个字符")] private string _name = string.Empty; -
对长期订阅使用弱事件模式:csharp
WeakEventManager<Source, EventArgs>.AddHandler(source, "EventName", Handler); -
虚拟化大型集合:xml
<ListBox VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling" ItemsSource="{Binding LargeCollection}"/> -
尽可能冻结Freezable对象:csharp
var brush = new SolidColorBrush(Colors.Blue); brush.Freeze(); // 线程安全,性能更优 -
使用设计时数据: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使用正确
- 样式与资源组织合理