技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
desktop-wpf【数据与研究】Use when the user asks about WPF (.NET Framework or .NET Core/5+/6+), XAML desktop apps, MVVM with WPF, XAML data binding, or WPF custom controls. Do NOT use for: WinForms (desktop-winforms), WinUI 3 (desktop-winui3), or UWP (desktop-uwp).
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Build Windows desktop applications using Windows Presentation Foundation (WPF) — .NET's XAML-based UI framework with powerful data binding, templating, styling, and hardware-accelerated rendering via DirectX. WPF excels at complex LOB applications with rich UI requirements.
Exact user phrases: "WPF", "WPF app", "XAML", "MVVM WPF", "WPF data binding", "WPF custom control", "WPF styles", "WPF template", "WPF binding", "ICommand", "INotifyPropertyChanged", "WPF UserControl".
WPF application architecture with XAML view tree, ViewModel layer, data access, and styling strategy.
250 lines.
What is the WPF app complexity?
├── Simple CRUD (single form, few fields)
│ → Window with data-bound controls
│ → RelCommand, INotifyPropertyChanged in code-behind
├── Standard LOB (master-detail, search, reports)
│ → MVVM with dedicated ViewModels
│ → Navigation via ContentControl + DataTemplate
│ → Entity Framework for data access
├── Complex dashboard (real-time, charts, drag-drop)
│ → MVVM with messaging (Mediator/Messenger)
│ → VirtualizingCollection for large data
│ → 3rd party charts or custom DrawingVisual rendering
└── Enterprise (modular, plugins, multiple teams)
→ Prism/MAUI for modularity, region navigation
→ MEF/DI for plugin loading
→ Separate assemblies per module
View (XAML) — Data Binding, Commands, Triggers
↑ ↓
ViewModel — Observable Properties, ICommand, INotifyDataErrorInfo
↑
Model — Business Logic, Data Access, Validation Rules
// App.xaml
<Application x:Class="MyApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:local="clr-namespace:MyApp"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles.xaml" />
<ResourceDictionary Source="Resources/Converters.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
// App.xaml.cs - Configure DI
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
var services = new ServiceCollection();
services.AddSingleton<IDataService, DataService>();
services.AddTransient<MainViewModel>();
services.AddTransient<MainWindow>();
var provider = services.BuildServiceProvider();
var window = provider.GetRequiredService<MainWindow>();
window.DataContext = provider.GetRequiredService<MainViewModel>();
window.Show();
}
}
// Infrastructure/RelayCommand.cs
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;
_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);
}
// Infrastructure/BaseViewModel.cs
public abstract class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
}
// ViewModels/MainViewModel.cs
public class MainViewModel : BaseViewModel
{
private readonly IDataService _dataService;
private string _searchText = string.Empty;
private Item? _selectedItem;
public MainViewModel(IDataService dataService)
{
_dataService = dataService;
LoadItemsCommand = new RelayCommand(async _ => await LoadItemsAsync());
DeleteItemCommand = new RelayCommand(async item => await DeleteItemAsync(item),
item => item is Item);
SaveCommand = new RelayCommand(async _ => await SaveAsync(), _ => CanSave);
}
public ObservableCollection<Item> Items { get; } = new();
public string SearchText
{
get => _searchText;
set
{
SetProperty(ref _searchText, value);
_ = FilterItemsAsync(); // Fire-and-forget with error handling
}
}
public Item? SelectedItem
{
get => _selectedItem;
set
{
SetProperty(ref _selectedItem, value);
OnPropertyChanged(nameof(CanSave));
}
}
public bool CanSave => SelectedItem != null;
public ICommand LoadItemsCommand { get; }
public ICommand DeleteItemCommand { get; }
public ICommand SaveCommand { get; }
private async Task LoadItemsAsync()
{
IsBusy = true;
try
{
var items = await _dataService.GetItemsAsync();
Items.Clear();
foreach (var item in items) Items.Add(item);
}
catch (Exception ex)
{
ErrorMessage = quot;Failed to load: {ex.Message}";
}
finally
{
IsBusy = false;
}
}
private async Task DeleteItemAsync(object? param)
{
if (param is Item item)
{
Items.Remove(item);
await _dataService.DeleteItemAsync(item.Id);
}
}
private async Task SaveAsync()
{
// Save logic
}
}
<Window x:Class="MyApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:MyApp.Converters"
Title="My WPF App" Height="600" Width="900">
<Window.Resources>
<conv:BoolToVisibilityConverter x:Key="BoolToVis" />
</Window.Resources>
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Search bar -->
<TextBox Grid.Row="0" Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
Margin="0,0,0,8" Watermark="Search items..." />
<!-- Busy indicator -->
<ProgressBar Grid.Row="1" IsIndeterminate="{Binding IsBusy}"
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVis}}"
Height="4" Margin="0,0,0,8" />
<!-- Master-detail -->
<Grid Grid.Row="2" ColumnDefinitions="2*,*">
<!-- List -->
<ListBox ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="4">
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" />
<TextBlock Text="{Binding Subtitle}" Foreground="Gray"
FontSize="11" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- Detail -->
<ContentControl Grid.Column="1" Margin="16,0,0,0"
Content="{Binding SelectedItem}">
<ContentControl.ContentTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}" FontSize="18" FontWeight="Bold" />
<TextBlock Text="{Binding Description}" TextWrapping="Wrap"
Margin="0,8,0,0" />
<CheckBox IsChecked="{Binding IsActive}" Content="Active"
Margin="0,8,0,0" />
</StackPanel>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</Grid>
<!-- Status bar -->
<StatusBar Grid.Row="3">
<StatusBarItem>
<TextBlock Text="{Binding StatusMessage}" />
</StatusBarItem>
</StatusBar>
</Grid>
</Window>
// Model with validation
public class Item : BaseViewModel, IDataErrorInfo
{
private string _name = string.Empty;
private string _email = string.Empty;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
public string Email
{
get => _email;
set => SetProperty(ref _email, value);
}
// IDataErrorInfo implementation
public string this[string columnName] => columnName switch
{
nameof(Name) => string.IsNullOrWhiteSpace(Name)
? "Name is required"
: Name.Length > 100 ? "Name too long (max 100)" : string.Empty,
nameof(Email) => !Regex.IsMatch(Email ?? "", @"^[^@\s]+@[^@\s]+\.[^@\s]+quot;)
? "Invalid email format"
: string.Empty,
_ => string.Empty
};
public string Error => string.Empty;
}
<!-- Resources/Styles.xaml -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<!-- Colors -->
<Color x:Key="PrimaryColor">#2563EB</Color>
<Color x:Key="PrimaryHover">#1D4ED8</Color>
<Color x:Key="SurfaceColor">#F8FAFC</Color>
<Color x:Key="TextPrimary">#1E293B</Color>
<!-- Brushes -->
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource PrimaryColor}" />
<SolidColorBrush x:Key="SurfaceBrush" Color="{StaticResource SurfaceColor}" />
<!-- Base button style -->
<Style TargetType="Button">
<Setter Property="Background" Value="{StaticResource PrimaryBrush}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Padding" Value="12,6" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Cursor" Value="Hand" />
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#1D4ED8" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="#94A3B8" />
</Trigger>
</Style.Triggers>
</Style>
<!-- ListBox item style -->
<Style TargetType="ListBoxItem">
<Setter Property="Padding" Value="8,4" />
<Setter Property="Margin" Value="0,2" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="#DBEAFE" />
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
// Controls/WatermarkTextBox.cs - Custom control with additional property
public class WatermarkTextBox : TextBox
{
static WatermarkTextBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(WatermarkTextBox),
new FrameworkPropertyMetadata(typeof(WatermarkTextBox)));
}
public string Watermark
{
get => (string)GetValue(WatermarkProperty);
set => SetValue(WatermarkProperty, value);
}
public static readonly DependencyProperty WatermarkProperty =
DependencyProperty.Register(nameof(Watermark), typeof(string),
typeof(WatermarkTextBox), new PropertyMetadata(string.Empty));
}
| Pitfall | Description | Prevention |
|---|---|---|
| UI thread blocking | Heavy computation freezes app | async/await, BackgroundWorker, Task.Run with dispatcher |
| Memory leaks from event handlers | Subscribing without unsubscribing | WeakEvent pattern, -= in Dispose |
| Data Virtualization missing | Loading 10K+ rows freezes UI | VirtualizingCollection, async loading, paging |
| INotifyPropertyChanged overhead | Every property change triggers binding | Batch updates, delay signals, use SetProperty |
| No async commands | Button blocks while operation runs | Use AsyncRelayCommand or async void with error handling |
| Deep XAML nesting | Performance degradation from complexity | UserControl extraction, virtualization |
| Missing fallback values | Binding errors show nothing | TargetNullValue, FallbackValue on bindings |
| Ignoring Freezable objects | Shared resources modified at runtime | Freeze resources, use Frozen flag |
| No dispatcher for background updates | Cross-thread UI access exception | Check Dispatcher.CheckAccess, use InvokeAsync |
| Large resource dictionaries | Slow startup loading all resources | Split by theme/feature, merge lazily |
| Practice | Rationale |
|---|---|
| MVVM strictly enforced | Testability, separation of concerns |
| CommunityToolkit.Mvvm for source generators | Less boilerplate, compile-time verified |
| async/await for all I/O | Non-blocking UI, Task-based commands |
| VirtualizingPanel for lists | Efficient rendering of large collections |
| StaticResource over DynamicResource | Better performance, compile-time check |
| DataTemplates for type-based rendering | Automatic view resolution for ViewModel types |
| Attached behaviors for complex interactions | Reusable, declarative interaction patterns |
| Prism for modular enterprise apps | Region navigation, modularity, DI integration |
| WPF Performance Suite for profiling | Identify rendering, layout, binding bottlenecks |
| .NET 6+ for new WPF projects | .NET 6+, not .NET Framework |
public interface IDialogService
{
bool? ShowDialog(string title, object viewModel);
}
public class DialogService : IDialogService
{
public bool? ShowDialog(string title, object viewModel)
{
var window = new Window
{
Title = title,
Content = new ContentControl { Content = viewModel },
DataContext = viewModel,
Width = 400, Height = 300,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Owner = Application.Current.MainWindow
};
return window.ShowDialog();
}
}
// WeakReference-based messaging between ViewModels
public class Mediator
{
private readonly Dictionary<Type, List<WeakReference>> _subscribers = new();
public void Subscribe<TMessage>(object subscriber, Action<TMessage> handler) { ... }
public void Unsubscribe<TMessage>(object subscriber) { ... }
public void Send<TMessage>(TMessage message) { ... }
}
Hand off to desktop-winui3 for Windows App SDK migration. Hand off to design-accessibility for UIA accessibility testing.