高级主题
WPF 高级开发技术与最佳实践
性能优化
性能优化是 WPF 开发中的重要主题,合理的优化可以显著提升应用的响应速度和用户体验。
虚拟化
<!-- 启用虚拟化 -->
<ListBox ItemsSource="{Binding LargeCollection}"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
<!-- DataGrid 虚拟化 -->
<DataGrid ItemsSource="{Binding LargeData}"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
EnableRowVirtualization="True"
EnableColumnVirtualization="True" />
冻结对象
// 冻结画刷
var brush = new SolidColorBrush(Colors.Blue);
brush.Freeze();
// 冻结几何图形
var geometry = new RectangleGeometry(new Rect(0, 0, 100, 100));
geometry.Freeze();
// 在 XAML 中使用 Freezable
<SolidColorBrush x:Key="FrozenBrush" Color="Blue"
PresentationOptions:Freeze="True" />
延迟加载
using System;
public class MainViewModel
{
private readonly Lazy<DataService> _dataService;
private readonly Lazy<ReportService> _reportService;
public MainViewModel()
{
_dataService = new Lazy<DataService>(() =>
new DataService());
_reportService = new Lazy<ReportService>(() =>
new ReportService());
}
public DataService DataService => _dataService.Value;
public ReportService ReportService => _reportService.Value;
}
减少布局更新
// 批量更新 UI
private void UpdateMultipleItems()
{
// 暂停布局
this.Dispatcher.Invoke(() =>
{
var items = new List<Item>();
// 批量添加
foreach (var item in sourceItems)
{
items.Add(item);
}
// 一次性更新
Items.Clear();
foreach (var item in items)
{
Items.Add(item);
}
}, DispatcherPriority.Background);
}
使用 Dispatcher
// 将耗时操作放到后台线程
private async Task LoadDataAsync()
{
// 后台线程
var data = await Task.Run(() =>
{
// 耗时操作
return LoadFromDatabase();
});
// UI 线程更新
await Application.Current.Dispatcher.InvokeAsync(() =>
{
Items.Clear();
foreach (var item in data)
{
Items.Add(item);
}
});
}
优化绑定
<!-- 使用 OneTime 代替 OneWay -->
<TextBlock Text="{Binding StaticData, Mode=OneTime}" />
<!-- 延迟更新源 -->
<TextBox Text="{Binding SearchText,
UpdateSourceTrigger=PropertyChanged,
Delay=300}" />
<!-- 使用 StringFormat 代替转换器 -->
<TextBlock Text="{Binding Price, StringFormat='¥{0:N2}'}" />
缓存资源
// 缓存样式和模板
public class ResourceCache
{
private static readonly Dictionary<string, Style> _styleCache =
new Dictionary<string, Style>();
public static Style GetStyle(string key)
{
if (!_styleCache.TryGetValue(key, out var style))
{
style = Application.Current.FindResource(key) as Style;
_styleCache[key] = style;
}
return style;
}
}
💡 性能优化建议
- 使用性能分析工具找出瓶颈
- 优先优化热路径代码
- 避免在循环中创建对象
- 使用对象池减少 GC 压力
- 合理使用虚拟化处理大量数据
- 冻结不变的 Freezable 对象
自定义控件
自定义控件是 WPF 的强大功能,允许你创建完全自定义的 UI 组件。
创建自定义控件
using System.Windows;
using System.Windows.Controls;
public class CustomButton : Button
{
static CustomButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomButton),
new FrameworkPropertyMetadata(typeof(CustomButton)));
}
// 依赖属性
public static readonly DependencyProperty CornerRadiusProperty =
DependencyProperty.Register(
"CornerRadius",
typeof(CornerRadius),
typeof(CustomButton),
new PropertyMetadata(new CornerRadius(5)));
public CornerRadius CornerRadius
{
get => (CornerRadius)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
public static readonly DependencyProperty IconProperty =
DependencyProperty.Register(
"Icon",
typeof(object),
typeof(CustomButton),
new PropertyMetadata(null));
public object Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
}
定义控件模板
<!-- Themes/Generic.xaml -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyControls">
<Style TargetType="{x:Type local:CustomButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomButton}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}">
<StackPanel Orientation="Horizontal">
<ContentPresenter Content="{TemplateBinding Icon}"
VerticalAlignment="Center"
Margin="0,0,8,0" />
<ContentPresenter Content="{TemplateBinding Content}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
使用 VisualStateManager
<ControlTemplate TargetType="{x:Type local:CustomButton}">
<Border x:Name="Border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="MouseOver">
<Storyboard>
<ColorAnimation Storyboard.TargetName="Border"
Storyboard.TargetProperty="Background.Color"
To="#1976D2" Duration="0:0:0.2" />
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ColorAnimation Storyboard.TargetName="Border"
Storyboard.TargetProperty="Background.Color"
To="#0D47A1" Duration="0:0:0.1" />
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="Border"
Storyboard.TargetProperty="Opacity"
To="0.5" Duration="0" />
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</ControlTemplate>
自定义路由事件
public class CustomButton : Button
{
public static readonly RoutedEvent CustomClickEvent =
EventManager.RegisterRoutedEvent(
"CustomClick",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(CustomButton));
public event RoutedEventHandler CustomClick
{
add => AddHandler(CustomClickEvent, value);
remove => RemoveHandler(CustomClickEvent, value);
}
protected override void OnClick()
{
base.OnClick();
RaiseEvent(new RoutedEventArgs(CustomClickEvent, this));
}
}
交互行为
行为(Behaviors)允许你将交互逻辑与 UI 分离,提高代码的可维护性。
使用 Microsoft.Xaml.Behaviors
# 安装 NuGet 包
dotnet add package Microsoft.Xaml.Behaviors.Wpf
EventTriggerBehavior
<Window xmlns:i="http://schemas.microsoft.com/xaml/behaviors">
<Button Content="点击我">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<i:InvokeCommandAction Command="{Binding ClickCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</Window>
自定义行为
using Microsoft.Xaml.Behaviors;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
public class TextBoxEnterKeyBehavior : Behavior<TextBox>
{
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register(
"Command",
typeof(ICommand),
typeof(TextBoxEnterKeyBehavior),
new PropertyMetadata(null));
public ICommand Command
{
get => (ICommand)GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.KeyDown += OnKeyDown;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.KeyDown -= OnKeyDown;
}
private void OnKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter && Command != null)
{
if (Command.CanExecute(AssociatedObject.Text))
{
Command.Execute(AssociatedObject.Text);
}
}
}
}
使用自定义行为
<Window xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:local="clr-namespace:MyApp.Behaviors">
<TextBox Text="{Binding SearchText}">
<i:Interaction.Behaviors>
<local:TextBoxEnterKeyBehavior Command="{Binding SearchCommand}" />
</i:Interaction.Behaviors>
</TextBox>
</Window>
数据触发行为
<TextBlock Text="{Binding Status}">
<i:Interaction.Triggers>
<i:DataTrigger Binding="{Binding Status}" Value="Loading">
<i:StartStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="0.5" To="1"
Duration="0:0:0.5"
RepeatBehavior="Forever"
AutoReverse="True" />
</Storyboard>
</i:StartStoryboard>
</i:DataTrigger>
</i:Interaction.Triggers>
</TextBlock>
多线程编程
WPF 提供了多种多线程编程模型,确保 UI 的响应性。
Dispatcher
// 同步调用 UI 线程
Application.Current.Dispatcher.Invoke(() =>
{
// 更新 UI
myTextBlock.Text = "更新完成";
});
// 异步调用 UI 线程
await Application.Current.Dispatcher.InvokeAsync(() =>
{
// 更新 UI
myTextBlock.Text = "更新完成";
});
// 指定优先级
Application.Current.Dispatcher.InvokeAsync(() =>
{
// 更新 UI
}, DispatcherPriority.Background);
async/await 模式
public class MainViewModel
{
private bool _isLoading;
public bool IsLoading
{
get => _isLoading;
set
{
_isLoading = value;
OnPropertyChanged(nameof(IsLoading));
}
}
public async Task LoadDataAsync()
{
IsLoading = true;
try
{
// 在后台线程执行
var data = await Task.Run(() =>
{
return LoadFromDatabase();
});
// 自动回到 UI 线程
ProcessData(data);
}
finally
{
IsLoading = false;
}
}
}
BackgroundWorker
public class MainViewModel
{
private readonly BackgroundWorker _worker;
public MainViewModel()
{
_worker = new BackgroundWorker();
_worker.WorkerReportsProgress = true;
_worker.WorkerSupportsCancellation = true;
_worker.DoWork += Worker_DoWork;
_worker.ProgressChanged += Worker_ProgressChanged;
_worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
}
public void StartWork()
{
if (!_worker.IsBusy)
{
_worker.RunWorkerAsync();
}
}
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
for (int i = 0; i <= 100; i++)
{
if (_worker.CancellationPending)
{
e.Cancel = true;
return;
}
// 执行工作
Thread.Sleep(50);
// 报告进度
_worker.ReportProgress(i);
}
}
private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
Progress = e.ProgressPercentage;
}
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
Status = "已取消";
}
else if (e.Error != null)
{
Status = $"错误: {e.Error.Message}";
}
else
{
Status = "完成";
}
}
}
Task Parallel Library
public async Task ProcessMultipleItemsAsync()
{
var tasks = new List<Task>();
foreach (var item in items)
{
tasks.Add(Task.Run(() => ProcessItem(item)));
}
await Task.WhenAll(tasks);
// 或使用 Parallel.ForEach
await Task.Run(() =>
{
Parallel.ForEach(items, item =>
{
ProcessItem(item);
});
});
}
⚠️ 线程安全注意事项
- 不要在后台线程直接访问 UI 元素
- 使用 Dispatcher 或 async/await 切换到 UI 线程
- 使用锁保护共享资源
- 避免死锁,不要在 UI 线程等待后台任务
- 使用 CancellationToken 支持取消操作
单元测试
单元测试是保证代码质量的重要手段,MVVM 架构使得测试变得更加容易。
设置测试项目
# 创建测试项目
dotnet new xunit -n WpfApp.Tests
# 添加测试框架
dotnet add package xunit
dotnet add package xunit.runner.visualstudio
dotnet add package Moq
dotnet add package FluentAssertions
测试 ViewModel
using Xunit;
using FluentAssertions;
using Moq;
public class MainViewModelTests
{
[Fact]
public void Constructor_InitializesProperties()
{
// Arrange
var service = new Mock<IDataService>().Object;
// Act
var viewModel = new MainViewModel(service);
// Assert
viewModel.Should().NotBeNull();
viewModel.Items.Should().BeEmpty();
}
[Fact]
public async Task LoadCommand_WhenExecuted_LoadsItems()
{
// Arrange
var mockService = new Mock<IDataService>();
var expectedItems = new List<Item>
{
new Item { Id = 1, Name = "Item 1" }
};
mockService.Setup(s => s.GetAllAsync())
.ReturnsAsync(expectedItems);
var viewModel = new MainViewModel(mockService.Object);
// Act
await viewModel.LoadCommand.ExecuteAsync(null);
// Assert
viewModel.Items.Should().HaveCount(1);
viewModel.Items.First().Name.Should().Be("Item 1");
}
[Fact]
public void SaveCommand_WhenNoItemSelected_CannotExecute()
{
// Arrange
var service = new Mock<IDataService>().Object;
var viewModel = new MainViewModel(service);
// Act & Assert
viewModel.SaveCommand.CanExecute(null).Should().BeFalse();
}
}
测试命令
[Fact]
public void IncrementCommand_WhenExecuted_IncreasesCount()
{
// Arrange
var viewModel = new MainViewModel();
var initialCount = viewModel.Count;
// Act
viewModel.IncrementCommand.Execute(null);
// Assert
viewModel.Count.Should().Be(initialCount + 1);
}
[Fact]
public void DeleteCommand_WhenNoItemSelected_CannotExecute()
{
// Arrange
var viewModel = new MainViewModel();
// Act & Assert
viewModel.DeleteCommand.CanExecute(null).Should().BeFalse();
}
UI 测试
# 安装 UI 测试框架
dotnet add package FlaUI.UIA3
dotnet add package FlaUI.Core
using FlaUI.Core;
using FlaUI.UIA3;
public class MainWindowTests
{
[Fact]
public void MainWindow_ShouldOpenAndClose()
{
// Arrange
using var app = Application.Launch("WpfApp.exe");
using var automation = new UIA3Automation();
var window = app.GetMainWindow(automation);
// Act & Assert
window.Should().NotBeNull();
window.Title.Should().Be("我的应用");
// Cleanup
window.Close();
}
}
部署与发布
了解如何将 WPF 应用程序打包和发布给用户。
框架依赖部署
# 发布为框架依赖
dotnet publish -c Release -r win-x64
# 生成的文件较小,但需要 .NET 运行时
独立部署
# 发布为独立应用
dotnet publish -c Release -r win-x64 --self-contained
# 包含 .NET 运行时,无需安装 .NET
单文件发布
# 发布为单文件
dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFile=true
# 所有文件打包到一个可执行文件中
创建安装程序
# 使用 WiX 工具集
dotnet add package WixToolset.Core
# 使用 Inno Setup
# 创建 .iss 脚本文件
; Script.iss
[Setup]
AppName=My WPF App
AppVersion=1.0.0
DefaultDirName={pf}\MyWpfApp
DefaultGroupName=My WPF App
OutputBaseFilename=MyWpfApp-Setup
Compression=lzma2
SolidCompression=yes
[Files]
Source: "bin\Release\net8.0-windows\win-x64\publish\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{group}\My WPF App"; Filename: "{app}\MyWpfApp.exe"
Name: "{commondesktop}\My WPF App"; Filename: "{app}\MyWpfApp.exe"
[Run]
Filename: "{app}\MyWpfApp.exe"; Description: "Launch application"; Flags: nowait postinstall skipifsilent
自动更新
# 使用 Squirrel.Windows
dotnet add package Squirrel.Windows
# 或使用 Clowd.Squirrel
dotnet add package Clowd.Squirrel
using Squirrel;
public class UpdateManager
{
public async Task CheckForUpdatesAsync()
{
using var mgr = new GithubUpdateManager(
"https://github.com/yourusername/yourrepo");
var updateInfo = await mgr.CheckForUpdate();
if (updateInfo.ReleasesToApply.Count > 0)
{
await mgr.UpdateApp();
UpdateManager.RestartApp();
}
}
}
💡 部署建议
- 使用 CI/CD 自动化发布流程
- 为不同平台创建不同的发布包
- 提供安装程序和便携版两种选择
- 实现自动更新功能
- 签名可执行文件以避免安全警告
- 提供详细的安装和使用文档