高级主题

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 自动化发布流程
  • 为不同平台创建不同的发布包
  • 提供安装程序和便携版两种选择
  • 实现自动更新功能
  • 签名可执行文件以避免安全警告
  • 提供详细的安装和使用文档