
简介本资源是一份面向C#与WPF初学者及中级开发者的MVVM模式实战项目源码包聚焦Windows桌面应用开发中视图与逻辑解耦的核心实践。资源完整呈现一个基于WPF构建的典型业务系统含申请人信息、教育背景等模块涵盖Model层数据建模、View层XAML界面定义15个xaml文件、ViewModel层命令绑定与属性通知44个cs文件以及运行所需的编译产物baml、exe、dll等和UI资源26个png图标与ico。压缩包共141个文件大小6.65MB结构清晰便于逐层理解MVVM各组件协作机制。已有926人学习下载读者可直接运行调试、对照源码掌握INotifyPropertyChanged实现、ICommand封装、DataTemplate定制及依赖属性绑定等关键技能并借鉴其分控件设计如ApplicantMessageControl、EducationMessageControl等提升模块化开发能力。1. 项目概述一个WPF MVVM项目实例的完整构建最近在社区里看到不少朋友在讨论WPF和MVVM特别是如何将这两者结合做出一个结构清晰、易于维护的桌面应用。很多人手头可能有一些零散的代码片段或者看过一些理论但一到自己动手从零开始搭建一个完整的项目时就感觉无从下手界面和逻辑搅在一起代码越写越乱。这其实非常普遍MVVM模式的理解和落地确实需要经过一个完整的项目来锤炼。今天我就以一个具体的“项目实例”为线索来拆解如何用C#和WPF从零构建一个符合MVVM模式的应用。这个实例的核心功能是管理一个虚拟的“文档”列表并支持将列表信息导出为PDF文件。这听起来简单但麻雀虽小五脏俱全它涵盖了WPF数据绑定、命令、通知属性、服务分离、依赖注入等MVVM的核心概念。通过这个例子你不仅能得到一个可运行的代码更能理解每一步背后的设计意图和最佳实践避免未来在开发更复杂的MES系统、上位机监控程序时踩坑。我会假设你已经有基础的C#和XAML知识我们的重点将放在“如何用MVVM的思维去组织代码”而不是语法本身。过程中我会穿插很多我实际项目中总结的经验比如如何优雅地处理异步操作、如何设计可测试的ViewModel、以及如何避免常见的性能陷阱。让我们开始吧。2. 架构设计与核心思路拆解在动手写代码之前花点时间想清楚架构是至关重要的。很多新手项目失败不是因为代码能力不行而是从一开始结构就歪了。2.1 为什么是MVVM从MVC和MVP说开去你可能听过MVC、MVP和MVVM。在WPF的世界里MVVM几乎是官配。简单来说Model代表你的数据和业务逻辑。它不关心界面只关心“是什么”和“怎么做”。在我们的例子里就是一个Document类包含标题、作者、创建时间等属性。View就是XAML文件负责定义用户界面的外观和布局。它应该尽可能“笨”只包含呈现逻辑如颜色、动画不包含任何业务逻辑。它的数据来自ViewModel。ViewModel这是MVVM的核心。它是View的抽象包含了View所需的数据状态和命令行为。它从Model获取数据处理成View容易绑定的格式并响应View的交互请求。MVVM最大的优势是解耦。View和ViewModel之间通过数据绑定和命令进行通信这意味着你可以独立修改界面设计师的工作或业务逻辑开发者的工作而不会互相影响。同时ViewModel不引用任何WPF特有的类如Button,Window这使得它可以被轻松地进行单元测试。注意很多初学者会把业务逻辑写在ViewModel里这没问题。但对于复杂的、可重用的业务规则我强烈建议抽象到独立的Model或领域服务中。ViewModel更应专注于“视图逻辑”比如格式化一个日期字符串以供显示或者控制某个控件的可见性。2.2 项目结构规划清晰分层是关键一个清晰的项目结构能极大提升代码的可读性和可维护性。对于这个WPF MVVM项目我推荐如下结构MyDocumentManager.WPF (主项目WPF应用程序) ├── Views/ (存放所有XAML视图如MainWindow.xaml) ├── ViewModels/ (存放所有ViewModel类) ├── Models/ (存放核心数据模型和业务逻辑) ├── Services/ (存放各种服务如PDF导出服务、数据持久化服务) ├── Converters/ (存放值转换器如BoolToVisibilityConverter) ├── Commands/ (存放自定义命令类如果不用RelayCommand库) └── App.xaml (应用程序入口通常在这里配置依赖注入)为什么这么分隔离变化当PDF生成库从iTextSharp换成QuestPDF时你只需要修改Services文件夹下的代码。便于测试你可以单独为Services和ViewModels创建单元测试项目因为它们不依赖WPF。团队协作前端开发者专注于Views后端或逻辑开发者专注于ViewModels和Models。2.3 工具与框架选型站在巨人的肩膀上纯手工实现MVVM的所有基础设施如INotifyPropertyChanged是很好的学习方式但在实际项目中我们更倾向于使用成熟的框架来提升效率。这里有几个主流选择CommunityToolkit.Mvvm (原 Microsoft.Toolkit.Mvvm)这是目前微软官方推荐的首选。它轻量、现代源代码生成器Source Generators用起来非常爽。你只需要给ViewModel类加上[ObservableObject]属性给属性加上[ObservableProperty]它就会在编译时自动生成属性变更通知的样板代码。这能让你的ViewModel代码变得极其简洁。Prism这是一个功能非常全面的企业级框架除了MVVM支持还提供了模块化、区域导航、事件聚合器等高级功能。如果你的应用非常庞大需要动态加载模块Prism是很好的选择。但对于我们这种中小型项目它可能显得有些重。MVVM Light Toolkit一个老牌、经典的框架非常轻量易懂。但现在社区更活跃的是CommunityToolkit.Mvvm。我的选择与理由 对于本次实例项目我选择CommunityToolkit.Mvvm。原因如下官方背书未来可期作为微软 .NET 社区工具包的一部分它与.NET生态的集成度最高更新及时。开发体验极佳源代码生成器避免了手写RaisePropertyChanged的繁琐和笔误代码干净得像POCOPlain Old CLR Object一样。学习曲线平缓它专注于MVVM核心不会引入太多复杂概念适合作为第一个MVVM框架来学习。同时为了生成PDF我们需要一个库。iTextSharp或其新版iText 7功能强大但许可协议对于某些场景可能需要注意。另一个不错的选择是QuestPDF它采用Fluent API设计声明式地描述PDF布局代码可读性很高且社区版许可友好。本例中为了聚焦MVVM我们暂时用一个简单的模拟服务来代表PDF生成但你完全可以替换为真实的QuestPDF代码。3. 核心实现从Model到View的完整链路理论说够了我们开始写代码。我会按照数据流动的方向从内到外Model - ViewModel - View来构建。3.1 定义核心数据模型ModelModel是业务的基石。我们创建一个Document类它只关注数据本身。// Models/Document.cs namespace MyDocumentManager.Models { public class Document { public string Id { get; set; } Guid.NewGuid().ToString(); public string Title { get; set; } string.Empty; public string Author { get; set; } string.Empty; public DateTime CreatedDate { get; set; } DateTime.Now; public string ContentPreview { get; set; } string.Empty; // 其他业务属性如文件大小、类型等 } }这个类很简单就是一些属性。在实际项目中你可能会在这里添加数据验证逻辑比如用System.ComponentModel.DataAnnotations或者一些纯计算属性的业务方法。3.2 构建ViewModel应用状态与逻辑的容器ViewModel是大脑。我们创建一个MainViewModel它需要维护一个Document的集合。提供“添加文档”、“删除文档”、“导出PDF”等命令。暴露当前选中的文档等状态。使用CommunityToolkit.Mvvm的实现首先通过NuGet安装CommunityToolkit.Mvvm包。// ViewModels/MainViewModel.cs using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using MyDocumentManager.Models; using MyDocumentManager.Services; using System.Collections.ObjectModel; using System.Windows.Input; namespace MyDocumentManager.ViewModels { // [ObservableObject] 属性让这个类自动实现 INotifyPropertyChanged public partial class MainViewModel : ObservableObject { private readonly IPdfExportService _pdfExportService; // [ObservableProperty] 属性会让编译器为 _documents 字段生成一个名为 Documents 的公共属性 // 并且该属性的 setter 会自动调用 OnPropertyChanged。 [ObservableProperty] private ObservableCollectionDocument _documents; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsDocumentSelected))] // 当SelectedDocument变化时也通知IsDocumentSelected属性 [NotifyCanExecuteChangedFor(nameof(DeleteSelectedDocumentCommand))] // 同时触发命令的CanExecute重新评估 private Document? _selectedDocument; // 这是一个计算属性依赖于 SelectedDocument public bool IsDocumentSelected SelectedDocument ! null; public MainViewModel(IPdfExportService pdfExportService) { _pdfExportService pdfExportService; // 初始化数据 Documents new ObservableCollectionDocument { new Document { Title 项目需求文档, Author 张三, ContentPreview 这是关于XX系统的需求描述... }, new Document { Title 设计评审记录, Author 李四, ContentPreview UI设计稿已通过评审... }, new Document { Title 周报-2024, Author 王五, ContentPreview 本周完成了模块A的开发... } }; // 命令的初始化在部分类中由源代码生成器完成 } // [RelayCommand] 属性会生成一个名为 AddNewDocumentCommand 的 ICommand 属性 // 并关联到下面这个 AddNewDocument 方法。 [RelayCommand] private void AddNewDocument() { var newDoc new Document { Title $新文档 {DateTime.Now:HHmmss}, Author 当前用户, ContentPreview 请输入内容... }; Documents.Add(newDoc); SelectedDocument newDoc; // 自动选中新添加的文档 } // CanExecute 条件当有文档被选中时命令才可用。 [RelayCommand(CanExecute nameof(IsDocumentSelected))] private void DeleteSelectedDocument() { if (SelectedDocument ! null) { Documents.Remove(SelectedDocument); // 删除后SelectedDocument 自动变为 null这会通过属性通知更新UI并禁用删除按钮。 } } // 这是一个异步命令的例子导出PDF通常是IO操作需要异步处理。 [RelayCommand] private async Task ExportToPdfAsync() { try { // 调用服务层传入需要导出的数据 await _pdfExportService.ExportDocumentsAsync(Documents.ToList()); // 在实际项目中这里可以弹出成功提示或者让服务层返回文件路径后自动打开。 } catch (Exception ex) { // 错误处理在实际项目中这里应该通过一个对话框服务或日志服务来处理而不是在VM中直接弹窗。 // 为了示例简单我们先用 Console.WriteLine。 Console.WriteLine($导出PDF失败: {ex.Message}); } } } }关键点解析ObservableCollectionT这是WPF数据绑定的“明星”集合。当集合中的项被添加、删除或整个列表被替换时它会自动通知UI更新。绝对不要用ListT来绑定需要动态更新的列表。依赖注入MainViewModel通过构造函数接收一个IPdfExportService。这是依赖注入DI的体现它使ViewModel易于测试你可以传入一个模拟服务并且与服务实现解耦。异步命令ExportToPdfAsync方法标记为async Task并用[RelayCommand]装饰CommunityToolkit.Mvvm会自动处理异步命令的执行状态如防止重复点击。这是处理IO密集型或耗时操作的推荐方式。NotifyPropertyChangedFor和NotifyCanExecuteChangedFor这些是源代码生成器的强大功能让你声明式地定义属性之间的依赖关系自动处理通知链避免了手动调用OnPropertyChanged的遗漏和错误。3.3 设计View声明式的界面绑定View就是XAML文件它的任务是“描述”界面并通过绑定与ViewModel连接。!-- Views/MainWindow.xaml -- Window x:ClassMyDocumentManager.Views.MainWindow xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml xmlns:dhttp://schemas.microsoft.com/expression/blend/2008 xmlns:mchttp://schemas.openxmlformats.org/markup-compatibility/2006 xmlns:vmclr-namespace:MyDocumentManager.ViewModels mc:Ignorabled Title文档管理器 Height450 Width800 Window.DataContext !-- 在真实应用中DataContext通常在App.xaml.cs或通过DI容器设置 -- vm:MainViewModel / /Window.DataContext Grid Grid.RowDefinitions RowDefinition HeightAuto/ RowDefinition Height*/ RowDefinition HeightAuto/ /Grid.RowDefinitions !-- 工具栏 -- StackPanel Grid.Row0 OrientationHorizontal Margin5 Button Content添加文档 Command{Binding AddNewDocumentCommand} Margin5 Padding10,5/ Button Content删除选中 Command{Binding DeleteSelectedDocumentCommand} Margin5 Padding10,5/ Button Content导出PDF Command{Binding ExportToPdfCommand} Margin5 Padding10,5/ !-- 可以添加一个进度条或忙碌指示器绑定到 ExportToPdfCommand.IsRunning -- /StackPanel !-- 文档列表 -- DataGrid Grid.Row1 ItemsSource{Binding Documents} SelectedItem{Binding SelectedDocument, ModeTwoWay} AutoGenerateColumnsFalse IsReadOnlyTrue Margin5 DataGrid.Columns DataGridTextColumn Header标题 Binding{Binding Title} Width*/ DataGridTextColumn Header作者 Binding{Binding Author} Width100/ DataGridTextColumn Header创建时间 Binding{Binding CreatedDate, StringFormatyyyy-MM-dd HH:mm} Width150/ DataGridTextColumn Header内容预览 Binding{Binding ContentPreview} Width2*/ /DataGrid.Columns /DataGrid !-- 状态栏或详细信息面板 -- Border Grid.Row2 Background#EEE Padding5 TextBlock Run Text已选择: / Run Text{Binding SelectedDocument.Title, FallbackValue无} FontWeightBold/ Run Text 总计 / Run Text{Binding Documents.Count}/ Run Text 个文档。/ /TextBlock /Border /Grid /Window绑定要点ItemsSource{Binding Documents}将DataGrid的列表源绑定到ViewModel的Documents集合。SelectedItem{Binding SelectedDocument, ModeTwoWay}TwoWay模式确保UI中的选择变化能更新到ViewModelViewModel中的选择变化也能反映到UI。Command{Binding AddNewDocumentCommand}将按钮的点击事件绑定到ViewModel的对应命令。这是MVVM中处理用户交互的标准方式。FallbackValue当绑定的路径为null时比如初始没选中文档显示一个默认值避免显示空白。3.4 实现服务层与依赖注入服务层负责具体的、可能变化的实现。我们定义一个接口和一个模拟实现。// Services/IPdfExportService.cs namespace MyDocumentManager.Services { public interface IPdfExportService { Task ExportDocumentsAsync(ListDocument documents); } } // Services/MockPdfExportService.cs namespace MyDocumentManager.Services { public class MockPdfExportService : IPdfExportService { public async Task ExportDocumentsAsync(ListDocument documents) { // 模拟一个耗时的操作 await Task.Delay(1000); // 在实际项目中这里会调用iTextSharp或QuestPDF来生成PDF文件 // 例如使用QuestPDF生成一个简单的列表PDF Console.WriteLine($模拟导出PDF成功共导出了 {documents.Count} 个文档。); // 可以在这里使用 SaveFileDialog 让用户选择保存位置注意对话框调用应在UI线程最好通过服务抽象 // 简单示例生成一个文本文件代替 string fileName $Documents_Export_{DateTime.Now:yyyyMMdd_HHmmss}.txt; var content string.Join(Environment.NewLine, documents.Select(d ${d.Title} - {d.Author})); await File.WriteAllTextAsync(fileName, content); Console.WriteLine($已生成文件: {Path.GetFullPath(fileName)}); } } }现在我们需要一个地方来组装这些部件。在WPF中通常是在App.xaml.cs中配置依赖注入容器如Microsoft.Extensions.DependencyInjection。// App.xaml.cs using Microsoft.Extensions.DependencyInjection; using MyDocumentManager.Services; using MyDocumentManager.ViewModels; using MyDocumentManager.Views; using System.Windows; namespace MyDocumentManager { public partial class App : Application { private readonly ServiceProvider _serviceProvider; public App() { var services new ServiceCollection(); ConfigureServices(services); _serviceProvider services.BuildServiceProvider(); } private void ConfigureServices(IServiceCollection services) { // 注册服务 services.AddSingletonIPdfExportService, MockPdfExportService(); // 注册ViewModel为Transient每次请求新实例或Scoped在WPF中通常用Singleton模拟一个“窗口范围” services.AddTransientMainViewModel(); // 注册主窗口 services.AddSingletonMainWindow(); } protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); // 从容器中获取主窗口并显示其DataContext会自动由容器解析如果Window的构造函数接收MainViewModel var mainWindow _serviceProvider.GetRequiredServiceMainWindow(); mainWindow.Show(); } } }同时需要修改MainWindow.xaml.cs使其从构造函数接收MainViewModel。// Views/MainWindow.xaml.cs using MyDocumentManager.ViewModels; using System.Windows; namespace MyDocumentManager.Views { public partial class MainWindow : Window { public MainWindow(MainViewModel viewModel) { InitializeComponent(); DataContext viewModel; // 设置DataContext } } }重要提示在App.xaml中需要删除StartupUriMainWindow.xaml这一行因为我们现在在代码中手动创建并显示窗口。4. 进阶技巧与深度优化一个能跑起来的MVVM应用只是开始。要让它在真实项目中健壮、可维护还需要考虑更多。4.1 消息通知与松耦合通信ViewModel之间、或Service与ViewModel之间如何通信而不直接引用事件聚合器Event Aggregator或消息机制是答案。CommunityToolkit.Mvvm提供了IMessenger接口。场景当PDF导出服务完成时我们希望在主界面显示一个通知而不是在服务里直接操作UI。定义消息// Messages/PdfExportCompletedMessage.cs using CommunityToolkit.Mvvm.Messaging.Messages; namespace MyDocumentManager.Messages { public class PdfExportCompletedMessage : ValueChangedMessagestring { public PdfExportCompletedMessage(string filePath) : base(filePath) { } } }发送消息在服务中// 在MockPdfExportService.ExportDocumentsAsync方法末尾添加 using CommunityToolkit.Mvvm.Messaging; ... var messenger WeakReferenceMessenger.Default; messenger.Send(new PdfExportCompletedMessage(fileName));接收消息在ViewModel中// 在MainViewModel的构造函数中注册接收消息 public MainViewModel(IPdfExportService pdfExportService) { ... WeakReferenceMessenger.Default.RegisterPdfExportCompletedMessage(this, (r, m) { // 这里可以更新一个状态属性View绑定这个属性来显示通知 // 例如ExportStatus $导出成功{m.Value}; Console.WriteLine($收到导出完成消息: {m.Value}); }); } // 记得在适当的时候取消注册通常在ViewModel实现IDisposable接口时做。4.2 数据验证与用户输入处理对于需要用户输入的表单比如编辑文档详情的对话框数据验证至关重要。CommunityToolkit.Mvvm与System.ComponentModel.DataAnnotations可以很好地结合。在Model或专门的DTO上添加数据注解using System.ComponentModel.DataAnnotations; public class DocumentEditorDto { [Required(ErrorMessage 标题不能为空)] [MaxLength(100, ErrorMessage 标题不能超过100个字符)] public string Title { get; set; } [Required] public string Author { get; set; } // ... }在ViewModel中利用验证 你可以使用ObservableValidatorCommunityToolkit.Mvvm的一部分作为ViewModel的基类它提供了属性和整体验证的支持。using CommunityToolkit.Mvvm.ComponentModel; public partial class DocumentEditorViewModel : ObservableValidator // 注意基类 { [ObservableProperty] [NotifyDataErrorInfo] // 这个属性会触发验证通知 [Required] [MaxLength(100)] private string _title; // 有一个 Errors 属性可以获取所有错误信息便于UI绑定显示。 }在View中绑定错误信息 WPF的Validation.ErrorTemplate和Binding的ValidatesOnDataErrors属性可以自动显示错误提示。4.3 性能考量与常见陷阱集合更新大量数据一次性添加到ObservableCollection会触发多次UI更新导致卡顿。解决方案是使用ObservableCollection的批量操作扩展方法或者先准备一个List然后一次性赋值给ObservableCollection会触发一次重置通知。对于超大数据集考虑使用VirtualizingStackPanelDataGrid默认启用和分页。绑定模式明确指定Binding的Mode。对于纯展示的数据使用OneWay对于需要回写的如TextBox.Text使用TwoWay对于命令使用OneWay。避免不必要的双向绑定。内存泄漏事件泄漏如果ViewModel订阅了某个生命周期更长的对象的事件记得在ViewModel销毁时取消订阅。WeakReferenceMessenger本身是弱引用通常没问题。绑定泄漏在用户控件中如果绑定了非依赖属性在控件卸载时可能不会自动清理。确保在控件的Unloaded事件中清理DataContext或手动清除绑定。异步与UI线程在ViewModel的异步命令或方法中如果需要更新绑定到UI的ObservableProperty代码会自动在UI线程上引发通知。但如果你在后台线程直接修改集合如Documents.Add(item)会抛出跨线程异常。务必使用Dispatcher.Invoke或更好的方式在ViewModel内部通过异步方法获取数据后在主线程上更新集合。5. 项目调试与问题排查实录即使遵循了最佳实践开发过程中还是会遇到各种问题。这里记录几个我踩过的坑和解决方法。5.1 绑定失败UI不更新这是最常见的问题。请按以下清单排查检查DataContext是否正确设置在View的构造函数中打断点查看this.DataContext是否为预期的ViewModel实例。检查属性名称拼写绑定路径{Binding Documents}ViewModel里必须是名为Documents的公共属性。注意大小写。检查属性是否实现通知确保绑定属性在set时触发了PropertyChanged事件。如果你用CommunityToolkit.Mvvm的[ObservableProperty]检查字段名是否正确如_documents生成Documents属性。检查输出窗口Visual Studio的输出窗口选择“调试”源会显示详细的绑定错误信息例如“找不到属性‘XXX’”。这是你的第一道防线。使用调试转换器写一个简单的值转换器在所有绑定中返回固定值或原始值并打上断点可以追踪绑定过程。public class DebugConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { Debugger.Break(); // 或者用 Debug.WriteLine return value; // 或者返回一个标记值 } //... ConvertBack }5.2 命令不执行或按钮一直禁用检查CanExecute命令的CanExecute方法返回false会导致按钮禁用。检查你的[RelayCommand(CanExecute nameof(...))]中指定的条件属性是否正确实现并且在其变化时调用了NotifyCanExecuteChanged。CommunityToolkit.Mvvm通过[NotifyCanExecuteChangedFor]属性自动处理了。检查异步命令async void方法作为命令处理程序是危险的可能引发未捕获的异常。务必使用async Task并用[RelayCommand]装饰框架会正确处理。检查命令绑定确认XAML中Command{Binding ...Command}的路径正确。命令属性必须是ICommand类型。5.3 依赖注入容器报错“无法解析服务...”检查服务注册在App.ConfigureServices中确认所有需要的服务包括ViewModel都已正确注册AddSingleton,AddTransient。检查构造函数参数确保MainWindow的构造函数参数类型与容器中注册的服务类型匹配。容器会尝试解析所有参数。查看异常详情这类异常通常包含内部异常会明确指出哪个类型无法解析。根据提示检查注册生命周期和作用域。5.4 界面设计技巧让DataGrid更好用冻结列DataGrid FrozenColumnCount2可以让前两列在水平滚动时保持不动。自定义单元格模板你可以用DataGridTemplateColumn来嵌入按钮、图片或其他复杂控件。行详情使用DataGrid.RowDetailsTemplate可以展开行显示更多信息。分组结合CollectionViewSource可以实现对DataGrid项的分组但这通常是在ViewModel中准备好分组后的数据源更清晰。性能确保EnableRowVirtualization和EnableColumnVirtualization为True默认是这对大量数据至关重要。5.5 关于PDF导出服务的扩展我们的模拟服务只是写了个文本文件。如果你想集成真实的PDF库以QuestPDF为例步骤大致如下安装NuGet包QuestPDF。创建一个真正的PdfExportService实现IPdfExportService。在ExportDocumentsAsync方法中使用QuestPDF的Fluent API描述文档结构并生成。using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; using System.Diagnostics; public class QuestPdfExportService : IPdfExportService { public async Task ExportDocumentsAsync(ListDocument documents) { // 让用户选择保存路径这通常通过一个界面服务抽象这里简化为固定路径 string filePath $Documents_Export_{DateTime.Now:yyyyMMdd_HHmmss}.pdf; Document.Create(container { container.Page(page { page.Size(PageSizes.A4); page.Margin(2, Unit.Centimetre); page.DefaultTextStyle(x x.FontSize(11)); page.Header() .Text(文档导出报告) .SemiBold().FontSize(24).FontColor(Colors.Blue.Medium); page.Content() .PaddingVertical(1, Unit.Centimetre) .Table(table { table.ColumnsDefinition(columns { columns.RelativeColumn(); // 标题 columns.ConstantColumn(100); // 作者 columns.ConstantColumn(150); // 日期 }); table.Header(header { header.Cell().Text(标题); header.Cell().Text(作者); header.Cell().Text(创建时间); }); foreach (var doc in documents) { table.Cell().Text(doc.Title); table.Cell().Text(doc.Author); table.Cell().Text(doc.CreatedDate.ToString(yyyy-MM-dd)); } }); page.Footer() .AlignCenter() .Text(x { x.Span(第 ); x.CurrentPageNumber(); x.Span( 页 / 共 ); x.TotalPages(); x.Span( 页); }); }); }) .GeneratePdf(filePath); // 同步生成对于大量数据可考虑异步写入 Process.Start(new ProcessStartInfo(filePath) { UseShellExecute true }); await Task.CompletedTask; } }最后别忘了在App.ConfigureServices中将注册的服务从MockPdfExportService改为QuestPdfExportService。构建一个WPF MVVM项目就像搭积木清晰的层次和约定能让工程稳固且易于扩展。从定义清晰的Model开始到用CommunityToolkit.Mvvm简化ViewModel的编写再到View中声明式的绑定每一步都有其设计哲学。记住MVVM的终极目标是可测试性和可维护性。当你发现某段代码很难写单元测试或者View的XAML.cs文件里有大量业务逻辑时就是时候回头审视你的架构了。这个实例项目提供了完整的骨架你可以在此基础上添加导航、对话框、本地化、主题切换等功能逐渐把它变成一个功能丰富的现代化桌面应用。本文还有配套的精品资源点击获取