Files
DMS/软件设计文档/原始文档/08-WPF表现层-MVVM与响应式UI.md

286 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 08. WPF表现层 - MVVM与响应式UI
本文档详细设计了 `DMS.WPF` 层的MVVM架构并阐述了如何通过 `ItemViewModel` 和消息总线构建响应式UI确保数据变化能实时反映到界面。
## 1. 核心设计模式
### 1.1. MVVM (Model-View-ViewModel)
* **设计思路**MVVM 是一种UI架构模式旨在将UIView与业务逻辑和数据Model分离。ViewModel 作为View的抽象负责暴露数据和命令并处理View的交互逻辑。
* **优势**
* **职责分离**View只负责显示ViewModel负责逻辑Model负责数据职责清晰降低了复杂性。
* **可测试性**ViewModel可以独立于View进行单元测试无需UI框架的依赖提高了测试效率和覆盖率。
* **可维护性**UI和逻辑的修改互不影响降低了维护成本。
* **团队协作**UI设计师和开发者可以并行工作。
* **劣势/权衡**
* **学习曲线**对于初学者来说理解MVVM模式和数据绑定机制需要一定的学习成本。
* **样板代码**需要为每个View创建ViewModel并实现 `INotifyPropertyChanged` 等接口,增加了少量样板代码(但 `CommunityToolkit.Mvvm` 可以极大简化)。
### 1.2. ItemViewModel 模式
* **设计思路**当UI需要显示一个数据集合如设备列表集合中的每个数据项`DeviceDto`本身是“哑”的不具备通知UI更新的能力。`ItemViewModel` 模式为集合中的每个数据项创建一个专属的ViewModel`DeviceItemViewModel`这个ViewModel“包裹”原始数据并实现 `INotifyPropertyChanged`。当其内部属性变化时它会通知UI更新。
* **优势**
* **响应式UI**确保数据变化能实时、局部地反映到UI上提供流畅的用户体验。
* **解耦**将UI更新逻辑封装在 `ItemViewModel` 内部与原始数据DTO分离。
* **可重用性**`ItemViewModel` 可以在不同的列表或详细视图中复用。
* **劣势/权衡**
* **对象膨胀**:对于大型集合,每个数据项都创建一个 `ItemViewModel` 实例,可能会增加内存消耗。
* **映射开销**需要将原始数据DTO映射到 `ItemViewModel`,增加了少量代码和运行时开销。
### 1.3. 消息总线 (Messenger)
* **设计思路**:消息总线(或事件聚合器)是一种发布/订阅模式的实现用于在应用程序中解耦组件之间的通信。当后台服务如S7通信服务检测到设备状态变化时它不直接调用UI层的ViewModel而是向消息总线“发布”一条消息。任何“订阅”了该消息的ViewModel都会收到通知并做出响应。
* **优势**
* **高度解耦**:生产者(消息发布者)和消费者(消息订阅者)之间没有直接引用,降低了模块间的依赖性。
* **灵活性**:可以轻松添加新的消息订阅者,而无需修改消息发布者。
* **跨层通信**:提供了一种安全、标准的方式进行跨层(如基础设施层到表现层)通信。
* **劣势/权衡**
* **隐式性**:消息的发布和订阅是隐式的,可能导致代码流向难以追踪,增加了调试难度。
* **消息管理**:如果消息类型过多或命名不规范,可能导致混乱。
* **内存泄漏风险**:如果订阅者没有正确取消订阅,可能导致内存泄漏(`CommunityToolkit.Mvvm``WeakReferenceMessenger` 缓解了此问题)。
## 2. 目录结构
```
DMS.WPF/
├── Messages/ <-- 存放消息类
│ ├── DeviceStatusChangedMessage.cs
│ └── VariableValueUpdatedMessage.cs
├── ViewModels/
│ ├── Base/ <-- ViewModel基类
│ │ ├── BaseViewModel.cs
│ │ └── RelayCommand.cs
│ ├── Items/ <-- ItemViewModel
│ │ ├── DeviceItemViewModel.cs
│ │ └── VariableItemViewModel.cs
│ ├── DeviceListViewModel.cs
│ └── ...
└── ...
```
## 3. 响应式UI实现流程
### 3.1. 消息定义
```csharp
// 文件: DMS.WPF/Messages/VariableValueUpdatedMessage.cs
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace DMS.WPF.Messages;
/// <summary>
/// 当变量值在后台更新时通过IMessenger广播此消息。
/// </summary>
public class VariableValueUpdatedMessage : ValueChangedMessage<object>
{
public int VariableId { get; }
public VariableValueUpdatedMessage(int variableId, object value) : base(value)
{
VariableId = variableId;
}
}
```
### 3.2. ItemViewModel接收消息
`VariableItemViewModel` 实现了 `IRecipient<VariableValueUpdatedMessage>` 接口,当收到匹配的消息时,更新其 `Value` 属性从而触发UI刷新。
```csharp
// 文件: DMS.WPF/ViewModels/Items/VariableItemViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using DMS.Application.DTOs;
using DMS.WPF.Messages;
namespace DMS.WPF.ViewModels.Items;
/// <summary>
/// 代表变量列表中的单个变量项的ViewModel。
/// 实现了INotifyPropertyChanged其任何属性变化都会自动通知UI。
/// 同时订阅VariableValueUpdatedMessage以实时更新值。
/// </summary>
public partial class VariableItemViewModel : ObservableObject, IRecipient<VariableValueUpdatedMessage>
{
public int Id { get; }
[ObservableProperty]
private string _name;
[ObservableProperty]
private object _value; // 绑定到UI的值当此属性改变时UI会自动刷新
/// <summary>
/// 构造函数从DTO创建并注册消息接收器。
/// </summary>
public VariableItemViewModel(VariableDto dto, IMessenger messenger)
{
Id = dto.Id;
_name = dto.Name;
_value = dto.InitialValue; // 初始值
messenger.Register<VariableValueUpdatedMessage>(this); // 注册消息接收
}
/// <summary>
/// 实现IRecipient接口当接收到VariableValueUpdatedMessage消息时此方法被调用。
/// </summary>
public void Receive(VariableValueUpdatedMessage message)
{
if (message.VariableId == this.Id)
{
// 收到匹配的消息更新值UI会自动刷新
Value = message.Value; // ValueChangedMessage 的 Value 属性
}
}
}
```
### 3.3. 主ViewModel管理集合
`VariableListViewModel` 持有一个 `ObservableCollection<VariableItemViewModel>`当从应用层加载数据时将DTOs转换为 `VariableItemViewModel` 实例并添加到集合中。
```csharp
// 文件: DMS.WPF/ViewModels/VariableListViewModel.cs
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using DMS.Application.Interfaces;
using DMS.WPF.ViewModels.Items;
using CommunityToolkit.Mvvm.Messaging;
namespace DMS.WPF.ViewModels;
/// <summary>
/// 变量列表视图的ViewModel管理VariableItemViewModel集合。
/// </summary>
public class VariableListViewModel : BaseViewModel
{
private readonly IVariableAppService _variableAppService;
private readonly IMessenger _messenger;
/// <summary>
/// 绑定到UI的变量集合。
/// </summary>
public ObservableCollection<VariableItemViewModel> Variables { get; } = new();
/// <summary>
/// 构造函数。
/// </summary>
public VariableListViewModel(IVariableAppService variableAppService, IMessenger messenger)
{
_variableAppService = variableAppService;
_messenger = messenger;
}
/// <summary>
/// 加载变量数据。
/// </summary>
public override async Task LoadAsync()
{
IsBusy = true;
Variables.Clear();
try
{
var variableDtos = await _variableAppService.GetAllVariablesAsync(); // 假设有此方法
foreach (var dto in variableDtos)
{
Variables.Add(new VariableItemViewModel(dto, _messenger));
}
}
finally
{
IsBusy = false;
}
}
}
```
### 3.4. View绑定
XAML中的 `DataGrid``ItemsControl``ItemsSource` 绑定到 `ObservableCollection<VariableItemViewModel>`,列表项的属性直接绑定到 `VariableItemViewModel` 的属性。
```xml
<!-- 文件: DMS.WPF/Views/VariableListView.xaml -->
<DataGrid ItemsSource="{Binding Variables}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="名称" Binding="{Binding Name}" />
<!-- 这个绑定现在可以实时更新了 -->
<DataGridTextColumn Header="当前值" Binding="{Binding Value}" />
<!-- ... 其他列 -->
</DataGrid.Columns>
</DataGrid>
```
## 4. 依赖注入 (`App.xaml.cs`)
### 4.1. 设计思路与考量
* **标准DI**:使用 `Microsoft.Extensions.DependencyInjection` 作为标准的DI容器。
* **Messenger注册**`IMessenger` 必须注册为单例,确保所有组件共享同一个消息总线实例。
* **ViewModel生命周期**通常将主窗口的ViewModel注册为单例而其他子视图的ViewModel注册为 `Transient``Scoped`,以确保每次导航都获得新的实例。
### 4.2. 示例代码
```csharp
// 文件: DMS.WPF/App.xaml.cs
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Windows;
namespace DMS.WPF;
public partial class App : System.Windows.Application
{
private readonly IServiceProvider _serviceProvider;
public App()
{
var services = new ServiceCollection();
ConfigureServices(services);
_serviceProvider = services.BuildServiceProvider();
}
protected override void OnStartup(StartupEventArgs e)
{
// ... NLog初始化
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
mainWindow.Show();
// 启动后台服务
_serviceProvider.GetRequiredService<IHostedService>().StartAsync(CancellationToken.None);
}
private void ConfigureServices(IServiceCollection services)
{
// 消息总线 (关键新增)
// 使用弱引用信使,避免内存泄漏
services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);
// 应用层 & 基础设施层服务注册 (示例)
services.AddTransient<IRepositoryManager, RepositoryManager>();
services.AddTransient<IDeviceAppService, DeviceAppService>();
services.AddTransient<IVariableAppService, VariableAppService>();
services.AddTransient<IMqttPublishService, MqttPublishService>();
services.AddTransient<IChannelBus, ChannelBusService>();
services.AddTransient<ILoggerService, NLogService>();
// 注册后台服务为托管服务
services.AddHostedService<S7BackgroundService>();
// WPF UI 服务
services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<IDialogService, DialogService>(); // 假设有此服务
// ViewModels
services.AddSingleton<MainViewModel>(); // 主ViewModel通常是单例
services.AddTransient<DashboardViewModel>();
services.AddTransient<DeviceListViewModel>();
services.AddTransient<VariableListViewModel>();
// ... 其他子视图ViewModel注册为Transient
// Views
services.AddSingleton<MainWindow>();
}
}
```