1. 为什么WPF开发者最终都逃不开MVVM?
我第一次接触MVVM模式时,内心是抗拒的。作为一个刚从WinForms转WPF的开发者,我习惯直接在按钮点击事件里写业务逻辑,简单粗暴又直接。直到接手一个复杂的数据仪表盘项目,代码量突破5000行后,我彻底理解了MVVM存在的意义——它让WPF开发从野蛮生长走向工程化。
MVVM(Model-View-ViewModel)不是WPF的附属品,而是专为数据绑定机制设计的架构模式。在传统事件驱动开发中,我们经常看到这样的代码:
csharp复制private void btnSubmit_Click(object sender, RoutedEventArgs e)
{
string name = txtName.Text;
int age = int.Parse(txtAge.Text);
// 业务逻辑直接写在事件处理中...
}
这种写法在小型项目中看似高效,但随着功能增加会导致:
- 界面逻辑与业务逻辑高度耦合
- 单元测试难以开展
- 状态管理混乱
- 代码复用率低
MVVM通过分层架构解决了这些问题。让我们看一个典型对比:
| 维度 | 传统事件驱动 | MVVM模式 |
|---|---|---|
| 代码组织 | 逻辑集中在事件处理程序 | 按职责分离到不同层 |
| 可测试性 | 需要模拟UI环境 | 可独立测试ViewModel |
| 数据同步 | 手动同步UI与数据 | 自动双向绑定 |
| 复杂度曲线 | 随功能增加急剧上升 | 保持相对平缓 |
经验之谈:MVVM的学习曲线在前20小时确实陡峭,但一旦突破临界点,开发效率会产生质的飞跃。我建议从300行以上的项目开始采用MVVM,小项目可以先用传统模式练手。
2. MVVM三剑客:解剖架构核心组件
2.1 Model层:数据实体的艺术
Model是MVVM中最"单纯"的组件,它只关心数据本身。一个好的Model设计应该像瑞士军刀——专注且自包含。以用户管理系统为例:
csharp复制public class User : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged();
}
}
// 实现INotifyPropertyChanged接口
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
注意几个关键点:
- 实现INotifyPropertyChanged接口是必须的,这是数据绑定的基础
- 使用CallerMemberName特性简化属性通知
- 避免在Model中加入业务逻辑,保持其纯粹性
2.2 ViewModel层:业务逻辑的中枢
ViewModel是MVVM的"大脑",它需要处理:
- 从Model获取数据
- 准备View需要的数据格式
- 处理用户交互逻辑
- 维护应用状态
csharp复制public class UserViewModel : INotifyPropertyChanged
{
private ObservableCollection<User> _users;
public ObservableCollection<User> Users
{
get => _users;
set
{
_users = value;
OnPropertyChanged();
}
}
private ICommand _addUserCommand;
public ICommand AddUserCommand => _addUserCommand ??= new RelayCommand(AddUser);
private void AddUser(object parameter)
{
// 添加新用户的业务逻辑
}
}
踩坑提醒:新手常犯的错误是在ViewModel中直接操作UI元素。记住,ViewModel不应该引用任何View相关的类型(如Window、Button等),否则就破坏了分层架构的初衷。
2.3 View层:专注展示的艺术
View应该尽可能"愚蠢",它的职责只有两个:
- 定义UI结构和外观
- 通过数据绑定与ViewModel交互
xml复制<Window x:Class="UserManagement.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:vm="clr-namespace:UserManagement.ViewModels"
Title="用户管理系统" Height="450" Width="800">
<Window.DataContext>
<vm:UserViewModel/>
</Window.DataContext>
<Grid>
<ListView ItemsSource="{Binding Users}">
<ListView.View>
<GridView>
<GridViewColumn Header="姓名" DisplayMemberBinding="{Binding Name}"/>
</GridView>
</ListView.View>
</ListView>
<Button Content="添加用户" Command="{Binding AddUserCommand}" HorizontalAlignment="Right"/>
</Grid>
</Window>
3. 数据绑定:MVVM的神经系统
3.1 基础绑定模式对比
WPF提供了多种绑定方式,每种都有其适用场景:
| 绑定模式 | 语法示例 | 适用场景 |
|---|---|---|
| 单向绑定(默认) | {Binding Path=UserName} |
显示数据,不更新源 |
| 双向绑定 | {Binding Path=Age, Mode=TwoWay} |
表单输入场景 |
| 一次性绑定 | {Binding Path=Version, Mode=OneTime} |
初始化后不变的数据 |
| 单向到源绑定 | {Binding Path=SearchText, Mode=OneWayToSource} |
只更新源,不显示变化 |
3.2 高级绑定技巧
**值转换器(Converter)**是处理数据格式显示的利器。比如将布尔值转换为可见性:
csharp复制[ValueConversion(typeof(bool), typeof(Visibility))]
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return (Visibility)value == Visibility.Visible;
}
}
在XAML中使用:
xml复制<Window.Resources>
<local:BoolToVisibilityConverter x:Key="BoolToVisibility"/>
</Window.Resources>
<Button Visibility="{Binding IsAdmin, Converter={StaticResource BoolToVisibility}}"/>
**多绑定(MultiBinding)**允许组合多个数据源:
xml复制<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} (年龄:{1})">
<Binding Path="Name"/>
<Binding Path="Age"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
4. 命令处理:告别事件处理程序
4.1 ICommand接口实现
WPF的命令系统是MVVM实现用户交互的核心。以下是典型的RelayCommand实现:
csharp复制public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(object parameter) => _execute(parameter);
}
使用示例:
csharp复制public ICommand SaveCommand => new RelayCommand(
_ => SaveData(),
_ => !string.IsNullOrEmpty(FileName));
4.2 事件转命令模式
对于没有Command属性的控件(如ListView的SelectionChanged),可以使用Interactivity库:
xml复制<xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
...>
<ListView ItemsSource="{Binding Items}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding SelectionChangedCommand}"
CommandParameter="{Binding SelectedItem, ElementName=listView}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ListView>
5. 实战:构建完整的用户管理系统
5.1 项目结构规划
规范的MVVM项目应该按职责分层:
code复制UserManagement/
├── Models/
│ ├── User.cs
│ └── Department.cs
├── ViewModels/
│ ├── UserViewModel.cs
│ └── MainViewModel.cs
├── Views/
│ ├── MainView.xaml
│ └── UserView.xaml
└── Converters/
├── BoolToVisibilityConverter.cs
└── DateTimeToStringConverter.cs
5.2 典型功能实现流程
- 数据加载:
csharp复制public class UserViewModel
{
private readonly IUserService _userService;
public UserViewModel(IUserService userService)
{
_userService = userService;
LoadUsers();
}
private async void LoadUsers()
{
Users = new ObservableCollection<User>(await _userService.GetAllUsersAsync());
}
}
- 表单验证:
csharp复制public class User : IDataErrorInfo
{
public string this[string columnName]
{
get
{
if (columnName == nameof(Name) && string.IsNullOrWhiteSpace(Name))
return "姓名不能为空";
if (columnName == nameof(Age) && Age < 18)
return "年龄必须大于18岁";
return null;
}
}
public string Error => null;
}
- 对话框交互:
csharp复制public interface IDialogService
{
bool? ShowDialog(object dialogViewModel);
}
public class MainViewModel
{
private readonly IDialogService _dialogService;
public ICommand ShowUserDialogCommand => new RelayCommand(_ =>
{
var vm = new UserDialogViewModel();
if (_dialogService.ShowDialog(vm) == true)
{
// 处理对话框返回结果
}
});
}
6. MVVM进阶:解决实际开发痛点
6.1 处理复杂的数据模板
当需要根据数据类型动态选择模板时,TemplateSelector是利器:
csharp复制public class UserTemplateSelector : DataTemplateSelector
{
public DataTemplate AdminTemplate { get; set; }
public DataTemplate NormalUserTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
return item is User { IsAdmin: true } ? AdminTemplate : NormalUserTemplate;
}
}
XAML中使用:
xml复制<local:UserTemplateSelector x:Key="UserTemplateSelector"
AdminTemplate="{StaticResource AdminTemplate}"
NormalUserTemplate="{StaticResource NormalTemplate}"/>
<ListView ItemTemplateSelector="{StaticResource UserTemplateSelector}"
ItemsSource="{Binding Users}"/>
6.2 实现多语言支持
MVVM下实现多语言的关键是将语言资源绑定到ViewModel:
csharp复制public class LocalizationService : INotifyPropertyChanged
{
private ResourceManager _resourceManager;
public string this[string key] => _resourceManager.GetString(key);
public CultureInfo CurrentCulture
{
get => CultureInfo.CurrentUICulture;
set
{
CultureInfo.CurrentUICulture = value;
OnPropertyChanged(nameof(CurrentCulture));
OnPropertyChanged("Item"); // 通知索引器变更
}
}
}
ViewModel中使用:
csharp复制public class MainViewModel
{
private readonly LocalizationService _localization;
public string WelcomeMessage => _localization["Welcome"];
public ICommand ChangeLanguageCommand => new RelayCommand(lang =>
{
_localization.CurrentCulture = new CultureInfo(lang.ToString());
});
}
6.3 处理异步操作
MVVM中处理异步操作需要特别注意线程安全:
csharp复制public class UserViewModel
{
private readonly IUserService _userService;
public ObservableCollection<User> Users { get; private set; }
public bool IsLoading { get; private set; }
public ICommand LoadUsersCommand => new RelayCommand(async _ =>
{
IsLoading = true;
try
{
var users = await _userService.GetAllUsersAsync();
Users = new ObservableCollection<User>(users);
}
finally
{
IsLoading = false;
}
});
}
使用Dispatcher确保UI线程安全:
csharp复制Application.Current.Dispatcher.Invoke(() =>
{
Users.Add(newUser);
});
7. 常见MVVM反模式与解决方案
7.1 ViewModel中直接操作View
反模式:
csharp复制// 错误示范!
public void ShowMessage()
{
MessageBox.Show("操作成功");
}
正确做法:
csharp复制public interface INotificationService
{
void ShowMessage(string message);
}
public class MainViewModel
{
private readonly INotificationService _notifier;
public ICommand ShowMessageCommand => new RelayCommand(_ =>
{
_notifier.ShowMessage("操作成功");
});
}
7.2 过度复杂的ViewModel
症状:
- 单个ViewModel超过1000行代码
- 包含多个不相关的功能
- 难以进行单元测试
解决方案:
- 使用Child ViewModel拆分职责
- 应用Composite模式管理复杂界面
- 引入Mediator模式协调多个ViewModel
7.3 滥用静态资源
问题代码:
xml复制<!-- 不推荐 -->
<Button Command="{x:Static local:Commands.SaveCommand}"/>
推荐方案:
xml复制<!-- 通过DataContext继承 -->
<Button Command="{Binding SaveCommand}"/>
8. 调试技巧:当绑定失效时
8.1 诊断绑定问题
- 开启WPF绑定日志:
xml复制<Window ...
xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase">
<TextBlock Text="{Binding UserName, diag:PresentationTraceSources.TraceLevel=High}"/>
</Window>
-
查看输出窗口的绑定错误信息
-
使用Snoop或WPF Inspector实时检查可视化树和绑定
8.2 常见绑定错误排查
-
绑定路径错误:
- 检查属性名称拼写
- 确认属性实现了INotifyPropertyChanged
- 验证DataContext是否正确设置
-
值转换器未注册:
- 确保在Resources中声明了转换器
- 检查Converter参数是否正确
-
线程访问问题:
- 非UI线程更新ObservableCollection会导致崩溃
- 使用Dispatcher.BeginInvoke解决
9. 性能优化:让MVVM应用飞起来
9.1 集合绑定的优化
问题场景:
- 大数据量时ObservableCollection添加/删除项卡顿
- 频繁更新导致UI冻结
解决方案:
csharp复制// 使用批量更新扩展
public static class ObservableCollectionExtensions
{
public static void AddRange<T>(this ObservableCollection<T> collection, IEnumerable<T> items)
{
foreach (var item in items)
{
collection.Add(item);
}
}
}
// 或者使用更高效的实现
public class RangeObservableCollection<T> : ObservableCollection<T>
{
public void AddRange(IEnumerable<T> items)
{
CheckReentrancy();
var startIndex = Count;
var changedItems = items.ToList();
foreach (var item in changedItems)
{
Items.Add(item);
}
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add, changedItems, startIndex));
}
}
9.2 虚拟化列表控件
确保使用支持UI虚拟化的控件:
xml复制<ListView VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling">
<!-- 内容 -->
</ListView>
9.3 绑定延迟策略
对于频繁更新的属性:
csharp复制private string _searchText;
public string SearchText
{
get => _searchText;
set
{
if (_searchText == value) return;
_searchText = value;
OnPropertyChanged();
// 触发搜索逻辑
DebounceSearch();
}
}
private CancellationTokenSource _searchTokenSource;
private async void DebounceSearch()
{
_searchTokenSource?.Cancel();
_searchTokenSource = new CancellationTokenSource();
try
{
await Task.Delay(300, _searchTokenSource.Token);
// 执行实际搜索
}
catch (TaskCanceledException) { /* 忽略取消 */ }
}
10. 从MVVM到框架:Prism实战入门
10.1 Prism核心功能概览
Prism为WPF MVVM开发提供了更强大的工具集:
| 功能模块 | 作用描述 |
|---|---|
| 依赖注入 | 统一管理组件生命周期 |
| 模块化 | 按功能拆分应用程序 |
| 导航服务 | 实现View之间的导航逻辑 |
| 事件聚合器 | 解耦组件间的通信 |
| 对话框服务 | 标准化对话框交互 |
10.2 快速集成Prism
- 安装NuGet包:
code复制Install-Package Prism.Unity -Version 8.1.97
- 修改App.xaml.cs:
csharp复制protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.Register<IDialogService, DialogService>();
containerRegistry.Register<IUserService, UserService>();
}
- ViewModel基类改造:
csharp复制public class ViewModelBase : BindableBase, INavigationAware
{
protected readonly INavigationService NavigationService;
public ViewModelBase(INavigationService navigationService)
{
NavigationService = navigationService;
}
public virtual void OnNavigatedTo(NavigationContext navigationContext) { }
public virtual bool IsNavigationTarget(NavigationContext navigationContext) => true;
public virtual void OnNavigatedFrom(NavigationContext navigationContext) { }
}
10.3 典型Prism功能实现
区域(Region)管理:
xml复制<ContentControl prism:RegionManager.RegionName="MainRegion"/>
csharp复制public class MainViewModel
{
private readonly IRegionManager _regionManager;
public MainViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
}
public void NavigateToUserView()
{
_regionManager.RequestNavigate("MainRegion", "UserView");
}
}
事件聚合器使用:
csharp复制// 定义事件
public class UserUpdatedEvent : PubSubEvent<User> { }
// 发布事件
_eventAggregator.GetEvent<UserUpdatedEvent>().Publish(updatedUser);
// 订阅事件
_eventAggregator.GetEvent<UserUpdatedEvent>().Subscribe(OnUserUpdated);
11. 测试驱动:MVVM的单元测试实践
11.1 ViewModel测试策略
- 命令测试:
csharp复制[TestMethod]
public void SaveCommand_WhenDataValid_ExecutesSuccessfully()
{
// 准备
var vm = new UserViewModel();
vm.Name = "测试用户";
vm.Age = 25;
// 执行
((RelayCommand)vm.SaveCommand).Execute(null);
// 断言
Assert.IsTrue(vm.IsSaved);
}
- 属性变更测试:
csharp复制[TestMethod]
public void WhenNameChanged_RaisesPropertyChanged()
{
var vm = new UserViewModel();
var propertyChangedRaised = false;
vm.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(vm.Name))
propertyChangedRaised = true;
};
vm.Name = "新名字";
Assert.IsTrue(propertyChangedRaised);
}
11.2 模拟依赖项
使用Moq框架模拟服务:
csharp复制[TestMethod]
public async Task LoadUsers_WhenServiceReturnsData_PopulatesCollection()
{
// 准备模拟服务
var mockService = new Mock<IUserService>();
mockService.Setup(x => x.GetAllUsersAsync())
.ReturnsAsync(new List<User> { new User { Name = "测试用户" } });
// 创建ViewModel
var vm = new UserViewModel(mockService.Object);
// 执行
await vm.LoadUsersAsync();
// 断言
Assert.AreEqual(1, vm.Users.Count);
Assert.AreEqual("测试用户", vm.Users[0].Name);
}
11.3 UI交互测试
使用XAML Test框架:
csharp复制[UITest]
public void ClickingAddButton_AddsNewItemToList()
{
var app = Application.Launch("UserManagement.exe");
var window = app.GetMainWindow();
var addButton = window.Get<Button>("AddButton");
addButton.Click();
var listView = window.Get<ListView>("UsersListView");
Assert.AreEqual(1, listView.Items.Count);
app.Close();
}
12. 项目实战:从零构建Markdown编辑器
12.1 需求分析
我们要构建的功能:
- 实时Markdown预览
- 文件打开/保存
- 导出HTML/PDF
- 主题切换
- 多标签编辑
12.2 核心ViewModel设计
csharp复制public class DocumentViewModel : BindableBase
{
private string _text;
public string Text
{
get => _text;
set => SetProperty(ref _text, value, () => UpdateHtml());
}
private string _html;
public string Html
{
get => _html;
private set => SetProperty(ref _html, value);
}
private readonly IMarkdownService _markdownService;
public DocumentViewModel(IMarkdownService markdownService)
{
_markdownService = markdownService;
}
private void UpdateHtml()
{
Html = _markdownService.ToHtml(Text);
}
}
12.3 实现双向绑定编辑器
使用AvalonEdit集成:
xml复制<avalonEdit:TextEditor
Text="{Binding Text, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
SyntaxHighlighting="Markdown"
FontFamily="Consolas"
FontSize="14"/>
12.4 实现预览面板
使用WebBrowser控件(注意:实际项目中建议使用CefSharp等现代浏览器组件):
xml复制<WebBrowser x:Name="webView"
DataContext="{Binding}"
NavigateToString="{Binding Html}"/>
13. 现代化改进:.NET Core下的WPF MVVM
13.1 .NET Core带来的变化
-
性能提升:
- 启动时间缩短30%
- 内存占用降低
- 跨平台支持
-
新工具链:
- 使用dotnet CLI管理项目
- 更好的Docker支持
- 更现代的NuGet体验
13.2 项目文件现代化
传统.csproj:
xml复制<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<!-- 大量繁琐配置 -->
</Project>
SDK风格.csproj:
xml复制<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
</PropertyGroup>
</Project>
13.3 依赖注入标准化
.NET Core内置DI容器集成:
csharp复制public static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.AddSingleton<IMarkdownService, MarkdownService>();
services.AddTransient<DocumentViewModel>();
services.AddSingleton<MainWindow>();
return services.BuildServiceProvider();
}
在App.xaml.cs中使用:
csharp复制protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var serviceProvider = ConfigureServices();
var mainWindow = serviceProvider.GetRequiredService<MainWindow>();
mainWindow.Show();
}
14. 架构演进:从MVVM到Clean Architecture
14.1 分层架构升级
传统MVVM:
code复制Presentation (View/ViewModel)
Business (Model)
Data Access
Clean Architecture:
code复制Presentation (View/ViewModel)
Application (Use Cases)
Domain (Entities)
Infrastructure (Persistence/External)
14.2 领域模型设计
csharp复制public class Document : Entity
{
public string Title { get; private set; }
public string Content { get; private set; }
public void UpdateContent(string newContent)
{
if (string.IsNullOrWhiteSpace(newContent))
throw new ArgumentException("内容不能为空");
Content = newContent;
AddDomainEvent(new DocumentUpdatedEvent(this));
}
}
14.3 应用层用例实现
csharp复制public class UpdateDocumentCommand : IRequest
{
public Guid DocumentId { get; set; }
public string Content { get; set; }
}
public class UpdateDocumentHandler : IRequestHandler<UpdateDocumentCommand>
{
private readonly IDocumentRepository _repository;
public UpdateDocumentHandler(IDocumentRepository repository)
{
_repository = repository;
}
public async Task<Unit> Handle(UpdateDocumentCommand request, CancellationToken cancellationToken)
{
var document = await _repository.GetByIdAsync(request.DocumentId);
document.UpdateContent(request.Content);
await _repository.UpdateAsync(document);
return Unit.Value;
}
}
14.4 ViewModel与Clean架构集成
csharp复制public class DocumentViewModel : BindableBase
{
private readonly IMediator _mediator;
public DocumentViewModel(IMediator mediator)
{
_mediator = mediator;
}
public async Task SaveAsync()
{
await _mediator.Send(new UpdateDocumentCommand
{
DocumentId = CurrentDocument.Id,
Content = CurrentDocument.Content
});
}
}
15. 性能监控与调优实战
15.1 WPF性能分析工具
-
Visual Studio诊断工具:
- CPU使用率
- 内存占用
- UI线程响应性
-
PerfView:
- 深入分析GC行为
- 线程竞争检测
- 热路径识别
-
WPF Performance Suite:
- 可视化树分析
- 渲染性能指标
- 绑定跟踪
15.2 常见性能瓶颈与优化
问题1:过度绘制
- 症状:UI卡顿,GPU使用率高
- 解决方案:
- 使用
CacheMode="BitmapCache" - 减少不必要的透明度和效果
- 启用容器回收:
xml复制<VirtualizingStackPanel VirtualizationMode="Recycling"/>
- 使用
问题2:绑定风暴
- 症状:属性变更导致级联更新
- 解决方案:
- 使用
UpdateSourceTrigger=Explicit - 批量更新时暂停通知:
csharp复制public void BulkUpdate() { _suppressNotifications = true; // 批量更新操作 _suppressNotifications = false; OnPropertyChanged(string.Empty); // 通知所有属性 }
- 使用
问题3:内存泄漏
- 症状:内存持续增长不释放
- 解决方案:
- 检查事件订阅,确保及时取消
- 使用WeakEventManager
- 避免静态引用ViewModel
15.3 性能优化检查清单
- [ ] 启用UI虚拟化
- [ ] 检查绑定表达式复杂度
- [ ] 分析可视化树深度
- [ ] 验证命令的CanExecute逻辑
- [ ] 监控GC行为
- [ ] 检查动画性能
- [ ] 评估位图缓存使用
- [ ] 测试高DPI场景
16. 跨平台之路:在Linux/macOS运行WPF MVVM
16.1 使用Avalonia重构
Avalonia是WPF理念的跨平台实现:
xml复制<!-- WPF -->
<Button Content="Click Me" Command="{Binding ClickCommand}"/>
<!-- Avalonia -->
<Button Content="Click Me" Command="{Binding ClickCommand}"/>
ViewModel几乎无需修改:
csharp复制public class MainViewModel : INotifyPropertyChanged
{
public ICommand ClickCommand { get; }
public MainViewModel()
{
ClickCommand = new RelayCommand(() => Console.WriteLine("Clicked!"));
}
}
16.2 条件编译处理平台差异
csharp复制public interface IFileService
{
string OpenFileDialog();
}
#if WINDOWS
public class WindowsFileService : IFileService
{
public string OpenFileDialog()
{
var dialog = new Microsoft.Win32.OpenFileDialog();
return dialog.ShowDialog() == true ? dialog.FileName : null;
}
}
#elif LINUX
public class LinuxFileService : IFileService
{
public string OpenFileDialog()
{
// 使用GTK文件对话框
}
}
#endif
16.3 构建跨平台CI/CD
示例GitHub Actions配置:
yaml复制name: Build
on: [push]
jobs:
build:
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.0.x'
- name: Build
run: dotnet build -c Release
- name: Test
run: dotnet test -c Release --no-build
17. 状态管理:复杂应用的数据流设计
17.1 全局状态管理方案
方案1:单例服务
csharp复制public interface IAppState
{
User CurrentUser { get; set; }
}
public class AppState : IAppState, INotifyPropertyChanged
{
private static readonly Lazy<AppState> _instance = new Lazy<AppState>(() => new AppState());
public static AppState Instance => _instance.Value;
private User _currentUser;
public User CurrentUser
{
get => _currentUser;
set => SetField(ref _currentUser, value);
}
}
方案2:Redux模式
csharp复制public class AppStore
{
private AppState _state;
public AppState State => _state;
private readonly List<Reducer> _reducers = new List<Reducer>();
public void Dispatch(IAction action)
{
foreach (var reducer in _reducers)
{
_state = reducer.Invoke(_state, action);
}
StateChanged?.Invoke(this, EventArgs.Empty);
}
}
17.2 状态持久化策略
- 本地存储:
csharp复制public class SettingsService
{
public T Get<T>(string key, T defaultValue = default)
{
if (ApplicationData.Current.LocalSettings.Values.TryGetValue(key, out var value))
{
return (T)value;
}
return defaultValue;
}
public void Set<T>(string key, T value)
{
ApplicationData.Current.LocalSettings.Values[key] = value;
}
}
- 状态序列化:
csharp复制var stateJson = JsonSerializer.Serialize(appState);
File.WriteAllText("state.json", stateJson);
// 恢复
var loadedState = JsonSerializer.Deserialize<AppState>(File.ReadAllText("state.json"));
18. 设计系统:创建可复用的UI组件库
18.1 自定义控件开发
创建支持MVVM的ColorPicker:
csharp复制[TemplatePart(Name = "PART_ColorSlider", Type = typeof(Slider))]
public class ColorPicker : Control
{
public static readonly DependencyProperty SelectedColorProperty =
DependencyProperty.Register("SelectedColor", typeof(Color), typeof(ColorPicker),
new FrameworkPropertyMetadata(Colors.Red, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public Color SelectedColor
{
get => (Color)GetValue(SelectedColorProperty);
set => SetValue(SelectedColorProperty, value);
}
static ColorPicker()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ColorPicker),
new FrameworkPropertyMetadata(typeof(ColorPicker)));
}
}
18.2 主题与样式管理
创建主题资源字典:
xml复制<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="Button">
<Setter Property="Background" Value="{DynamicResource PrimaryBrush}"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Padding" Value="12 4"/>
</Style>
<LinearGradientBrush x:Key="PrimaryBrush" StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#FF0080FF" Offset="0"/>
<GradientStop Color="#FF0040FF" Offset="1"/>
</LinearGradientBrush>
</ResourceDictionary>
动态切换主题:
csharp复制public void ApplyTheme(string themeName)
{
var themeUri = new Uri($"Themes/{themeName}.xaml", UriKind.Relative);
var themeDict = new ResourceDictionary { Source = themeUri };
// 移除旧主题
var oldTheme = Application.Current.Resources.MergedDictionaries
.FirstOrDefault(d => d.Source?.OriginalString.StartsWith("Themes/") == true);
if (oldTheme != null)
{
Application.Current.Resources.MergedDictionaries.Remove(oldTheme);
}
// 应用新主题
Application.Current.Resources.MergedDictionaries.Add(themeDict);
}
19. 部署策略:打包与分发WPF MVVM应用
19.1 ClickOnce部署
- 项目配置:
xml复制<PropertyGroup>
<PublishUrl>\\server\share\MyApp\</PublishUrl>
<InstallUrl>https://download.example.com/myapp/</InstallUrl>
<ProductName>My WPF App</ProductName>
<PublishProtocol>ClickOnce</PublishProtocol>
<Install>true</Install>
<UpdateEnabled>true</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
</PropertyGroup>
- 发布命令:
code复制msbuild /t:publish /p:Configuration=Release
19.2 MSIX打包
-
创建打包项目:
- 在Visual Studio中添加"Windows Application Packaging Project"
-
配置清单:
xml复制<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities">
