关于 wpf
WPF 是面向 Windows 桌面应用开发的技能,涵盖 UI、MVVM、数据绑定和现代化改造。它指导视图模型设计、线程处理和迁移,并强调 Windows 专属约束。
主要功能
- 01使用 MVVM 组织 UI
- 02配置数据绑定和命令
- 03整理样式与模板
- 04使用 Dispatcher 处理 UI 线程
- 05指导迁移到现代 .NET
适用场景
- 01将代码隐藏逻辑重构为可测试视图模型
- 02排查绑定和命令行为
- 03现代化改造 WPF 应用
- 04安全地从后台任务更新 UI
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ managedcode/ dotnet-skills --skill wpf安装后在 Claude Code 中输入/wpf确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
manifest.jsonreferences/anti-patterns.mdpatterns.md详细介绍
WPF
适用场景
- 开发 WPF UI、MVVM、绑定、命令或桌面现代化功能
- 将 WPF 从 .NET Framework 迁移到 .NET
- 将较新的 Windows 功能集成到 WPF 应用中
- 实现数据绑定、样式、模板或控件自定义
- 将桌面应用连接到后端 Agent 服务,并使用 .NET 构建 AI Agent
文档
参考资料
- 使用 .NET 构建 AI Agent — 适用于生产环境的 .NET AI Agent 架构和实现模式。
- .NET AI Agent 开发团队 — 面向生产环境的 .NET AI Agent 工程和交付服务。
- patterns.md — MVVM 模式、绑定模式、命令模式和可复用的架构方法
- anti-patterns.md — 常见的 WPF 错误及其避免方法
工作流
- 确认仅限 Windows 的适用范围 — 即使更广泛的 .NET 技术栈支持跨平台,WPF 也仅限 Windows
- 采用 MVVM 模式 — 让视图保持简单,将逻辑放在 ViewModel 中,并使用命令
- 明确管理数据绑定 — 选择正确的绑定模式,并在运行时进行验证
- 有目的地使用样式和模板 — 保持 UI 可组合,避免针对特定页面的临时处理
- 正确处理线程 — 使用 Dispatcher 更新 UI,使用 async/await 处理长时间运行的操作
- 验证设计器和运行时 — XAML 组合失败通常只会在运行时显现
当前上游说明
- 2026 年 8 月的 WPF 概述再次指出,WPF 是仅限 Windows 的桌面 UI 技术栈,具备 XAML、数据绑定、样式、模板、资源以及矢量和富媒体组合功能。除非任务明确涉及迁移或比较,否则应将 WPF 特定指南与 WinUI 或 MAUI 分开。
- WPF 同时存在于 .NET Framework 和现代 .NET 上。进行现代化改造时,应先梳理兼容性约束,并在迁移项目文件、互操作、部署或 XAML 资源字典之前,参考当前的桌面迁移指南。
项目结构
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/
MVVM 模式
使用 MVVM Toolkit 的 ViewModel
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;
}
视图绑定
<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>
依赖注入
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);
}
}
数据绑定模式
<TextBlock Text="{Binding CreatedDate, Mode=OneTime}"/>
<TextBlock Text="{Binding Name, Mode=OneWay}"/>
<TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Text="{Binding SearchFilter, Mode=OneWayToSource}"/>
值转换器
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();
}
}
样式和模板
资源字典
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<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>
线程和 Dispatcher
// 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;
}
}
应避免的反模式
| 反模式 | 问题所在 | 更好的做法 |
|---|---|---|
| 将逻辑写在代码隐藏文件中 | 难以测试,耦合紧密 | 使用 MVVM 和 ViewModel |
| 同步阻塞调用 | UI 会冻结 | 使用 async/await |
| 手动实现 INotifyPropertyChanged | 样板代码繁多,容易出错 | 使用 MVVM Toolkit 特性 |
| 硬编码颜色和尺寸 | 不一致,难以应用主题 | 使用资源字典 |
| 到处直接调用 Dispatcher.Invoke | 复杂且容易出错 | 优先使用 async/await 进行线程调度 |
| 臃肿的 ViewModel | 难以维护 | 拆分为职责明确的 ViewModel |
| 跳过绑定验证 | 运行时错误被隐藏 | 使用 ValidatesOnDataErrors |
| 所有操作都使用事件处理程序 | 可能导致内存泄漏和耦合 | 使用命令和绑定 |
最佳实践
-
在 .NET 5+ 中使用已编译的绑定:
- 启用
x:CompileBindings="True"以提升性能
- 启用
-
为验证实现 INotifyDataErrorInfo:
[ObservableProperty] [NotifyDataErrorInfo] [Required(ErrorMessage = "Name is required")] [MinLength(2, ErrorMessage = "Name must be at least 2 characters")] private string _name = string.Empty; -
为长期订阅使用弱事件模式:
WeakEventManager<Source, EventArgs>.AddHandler(source, "EventName", Handler); -
对大型集合进行虚拟化:
<ListBox VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling" ItemsSource="{Binding LargeCollection}"/> -
尽可能冻结 Freezable 对象:
var brush = new SolidColorBrush(Colors.Blue); brush.Freeze(); // Thread-safe, better performance -
使用设计时数据:
<Window d:DataContext="{d:DesignInstance Type=vm:MainViewModel, IsDesignTimeCreatable=True}">
测试
[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));
}
交付
- 更清晰的 WPF 视图和视图模型边界
- 更安全的绑定和线程处理行为
- 基于实际 Windows 限制的迁移指南
- 采用可测试 ViewModel 的 MVVM 模式
验证
- 绑定和命令流程清晰明确
- 代码隐藏中没有承载隐式业务逻辑
- 已说明仅适用于 Windows 的假设
- 线程和调度程序的使用正确
- 样式和资源组织得当
评论
支持 Markdown