mcpField
W
wpf

wpf

github.com/managedcode开发49689 次安装今天更新
代码仓库

关于 wpf

WPF 是面向 Windows 桌面应用开发的技能,涵盖 UI、MVVM、数据绑定和现代化改造。它指导视图模型设计、线程处理和迁移,并强调 Windows 专属约束。

主要功能

  1. 01使用 MVVM 组织 UI
  2. 02配置数据绑定和命令
  3. 03整理样式与模板
  4. 04使用 Dispatcher 处理 UI 线程
  5. 05指导迁移到现代 .NET

适用场景

  1. 01将代码隐藏逻辑重构为可测试视图模型
  2. 02排查绑定和命令行为
  3. 03现代化改造 WPF 应用
  4. 04安全地从后台任务更新 UI

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/managedcode/dotnet-skills --skill wpf

安装后在 Claude Code 中输入/wpf确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

manifest.json
references/
anti-patterns.md
patterns.md

详细介绍

WPF

适用场景

  • 开发 WPF UI、MVVM、绑定、命令或桌面现代化功能
  • 将 WPF 从 .NET Framework 迁移到 .NET
  • 将较新的 Windows 功能集成到 WPF 应用中
  • 实现数据绑定、样式、模板或控件自定义
  • 将桌面应用连接到后端 Agent 服务,并使用 .NET 构建 AI Agent

文档

参考资料

工作流

  1. 确认仅限 Windows 的适用范围 — 即使更广泛的 .NET 技术栈支持跨平台,WPF 也仅限 Windows
  2. 采用 MVVM 模式 — 让视图保持简单,将逻辑放在 ViewModel 中,并使用命令
  3. 明确管理数据绑定 — 选择正确的绑定模式,并在运行时进行验证
  4. 有目的地使用样式和模板 — 保持 UI 可组合,避免针对特定页面的临时处理
  5. 正确处理线程 — 使用 Dispatcher 更新 UI,使用 async/await 处理长时间运行的操作
  6. 验证设计器和运行时 — 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
所有操作都使用事件处理程序可能导致内存泄漏和耦合使用命令和绑定

最佳实践

  1. 在 .NET 5+ 中使用已编译的绑定:

    • 启用 x:CompileBindings="True" 以提升性能
  2. 为验证实现 INotifyDataErrorInfo:

    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required(ErrorMessage = "Name is required")]
    [MinLength(2, ErrorMessage = "Name must be at least 2 characters")]
    private string _name = string.Empty;
    
  3. 为长期订阅使用弱事件模式:

    WeakEventManager<Source, EventArgs>.AddHandler(source, "EventName", Handler);
    
  4. 对大型集合进行虚拟化:

    <ListBox VirtualizingPanel.IsVirtualizing="True"
             VirtualizingPanel.VirtualizationMode="Recycling"
             ItemsSource="{Binding LargeCollection}"/>
    
  5. 尽可能冻结 Freezable 对象:

    var brush = new SolidColorBrush(Colors.Blue);
    brush.Freeze(); // Thread-safe, better performance
    
  6. 使用设计时数据:

    <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