217 lines
6.4 KiB
Markdown
217 lines
6.4 KiB
Markdown
# 软件开发文档 - 06. 实现步骤与示例
|
||
|
||
本文档提供关键功能的具体实现步骤和代码示例,以指导开发过程。
|
||
|
||
## 1. 添加新设备 (End-to-End)
|
||
|
||
这是一个从UI到数据库的完整流程示例。
|
||
|
||
**步骤 1: View (`DeviceListView.xaml`)**
|
||
|
||
用户点击“添加”按钮,该按钮绑定到ViewModel的 `AddDeviceCommand`。
|
||
|
||
```xml
|
||
<Button Content="添加设备" Command="{Binding AddDeviceCommand}" />
|
||
```
|
||
|
||
**步骤 2: ViewModel (`DeviceListViewModel.cs`)**
|
||
|
||
`AddDeviceCommand` 执行,调用对话框服务来显示一个用于输入新设备信息的窗口。
|
||
|
||
```csharp
|
||
public ICommand AddDeviceCommand { get; }
|
||
|
||
public DeviceListViewModel(IDeviceAppService deviceAppService, IDialogService dialogService)
|
||
{
|
||
_deviceAppService = deviceAppService;
|
||
_dialogService = dialogService;
|
||
AddDeviceCommand = new RelayCommand(async () => await ExecuteAddDevice());
|
||
}
|
||
|
||
private async Task ExecuteAddDevice()
|
||
{
|
||
var vm = new AddDeviceDialogViewModel(); // 一个专门用于对话框的ViewModel
|
||
var result = await _dialogService.ShowDialogAsync(vm);
|
||
|
||
if (result != null) // 假设对话框返回创建的DTO
|
||
{
|
||
await _deviceAppService.CreateDeviceAsync(result);
|
||
await LoadDevicesAsync(); // 刷新列表
|
||
}
|
||
}
|
||
```
|
||
|
||
**步骤 3: Application Service (`DeviceAppService.cs`)**
|
||
|
||
应用服务接收DTO,将其映射为领域模型,并调用仓储来保存。
|
||
|
||
```csharp
|
||
public async Task CreateDeviceAsync(CreateDeviceDto deviceDto)
|
||
{
|
||
// 参数校验
|
||
if (string.IsNullOrWhiteSpace(deviceDto.Name) || string.IsNullOrWhiteSpace(deviceDto.IpAddress))
|
||
{
|
||
throw new ArgumentException("设备名称和IP地址不能为空");
|
||
}
|
||
|
||
var device = _mapper.Map<Device>(deviceDto);
|
||
device.IsActive = true; // 设置默认值
|
||
|
||
await _deviceRepository.AddAsync(device);
|
||
}
|
||
```
|
||
|
||
**步骤 4: Repository (`DeviceRepository.cs`)**
|
||
|
||
仓储将领域模型映射为数据库实体,并使用 `SqlSugar` 将其插入数据库。
|
||
|
||
```csharp
|
||
public async Task AddAsync(Device entity)
|
||
{
|
||
var dbEntity = _mapper.Map<DbDevice>(entity);
|
||
await _db.Insertable(dbEntity).ExecuteCommandAsync();
|
||
}
|
||
```
|
||
|
||
## 2. S7数据采集后台服务
|
||
|
||
后台服务将轮询所有激活的S7设备,读取变量值,并启动数据处理链。
|
||
|
||
**步骤 1: 创建后台服务 (`S7BackgroundService.cs`)**
|
||
|
||
这个服务应该是一个长时间运行的服务,可以使用 `IHostedService` (如果项目是.NET Core) 或一个简单的 `Task.Run` 循环。
|
||
|
||
```csharp
|
||
// 文件: DMS.WPF/Services/S7BackgroundService.cs
|
||
public class S7BackgroundService : IDisposable
|
||
{
|
||
private readonly IDeviceRepository _deviceRepo;
|
||
private readonly IS7CommunicationService _s7Service;
|
||
private readonly DataAcquisitionService _acquisitionService; // 包含处理链逻辑的服务
|
||
private CancellationTokenSource _cts;
|
||
|
||
public S7BackgroundService(IDeviceRepository deviceRepo, IS7CommunicationService s7Service, DataAcquisitionService acquisitionService)
|
||
{
|
||
// ... 注入依赖
|
||
}
|
||
|
||
public void Start()
|
||
{
|
||
_cts = new CancellationTokenSource();
|
||
Task.Run(async () => await ExecuteAsync(_cts.Token));
|
||
}
|
||
|
||
protected async Task ExecuteAsync(CancellationToken stoppingToken)
|
||
{
|
||
while (!stoppingToken.IsCancellationRequested)
|
||
{
|
||
var activeDevices = await _deviceRepo.GetActiveDevicesWithDetailsAsync();
|
||
|
||
foreach (var device in activeDevices.Where(d => d.Protocol == ProtocolType.S7))
|
||
{
|
||
await _s7Service.ConnectAsync(device); // 确保连接
|
||
|
||
foreach (var table in device.VariableTables.Where(t => t.IsActive))
|
||
{
|
||
foreach (var variable in table.Variables.Where(v => v.IsActive))
|
||
{
|
||
try
|
||
{
|
||
var value = await _s7Service.ReadVariableAsync(device, variable);
|
||
// 触发数据处理链
|
||
await _acquisitionService.OnDataReceived(variable, value);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// Log error
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
await Task.Delay(1000, stoppingToken); // 轮询间隔
|
||
}
|
||
}
|
||
|
||
public void Stop()
|
||
{
|
||
_cts?.Cancel();
|
||
}
|
||
}
|
||
```
|
||
|
||
**步骤 2: 在 `App.xaml.cs` 中启动服务**
|
||
|
||
在应用程序启动时,从DI容器获取服务实例并启动它。
|
||
|
||
```csharp
|
||
// 在 App.xaml.cs OnStartup
|
||
var s7Service = _serviceProvider.GetRequiredService<S7BackgroundService>();
|
||
s7Service.Start();
|
||
```
|
||
|
||
## 3. 菜单导航实现
|
||
|
||
**步骤 1: `MainViewModel`**
|
||
|
||
```csharp
|
||
public class MainViewModel : BaseViewModel
|
||
{
|
||
private readonly INavigationService _navigationService;
|
||
public ICommand NavigateCommand { get; }
|
||
|
||
public MainViewModel(INavigationService navigationService)
|
||
{
|
||
_navigationService = navigationService;
|
||
NavigateCommand = new RelayCommand<Type>(OnNavigate);
|
||
}
|
||
|
||
private void OnNavigate(Type viewModelType)
|
||
{
|
||
_navigationService.NavigateTo(viewModelType);
|
||
}
|
||
}
|
||
```
|
||
|
||
**步骤 2: `NavigationService`**
|
||
|
||
这个服务负责实际的ViewModel切换,它会更新 `MainViewModel` 的 `CurrentViewModel` 属性。
|
||
|
||
```csharp
|
||
public class NavigationService : INavigationService
|
||
{
|
||
private readonly IServiceProvider _serviceProvider;
|
||
private readonly MainViewModel _mainViewModel; // 持有MainViewModel的引用
|
||
|
||
public NavigationService(IServiceProvider serviceProvider, MainViewModel mainViewModel)
|
||
{
|
||
_serviceProvider = serviceProvider;
|
||
_mainViewModel = mainViewModel;
|
||
}
|
||
|
||
public void NavigateTo(Type viewModelType)
|
||
{
|
||
var viewModel = (BaseViewModel)_serviceProvider.GetRequiredService(viewModelType);
|
||
_mainViewModel.CurrentViewModel = viewModel;
|
||
}
|
||
}
|
||
```
|
||
|
||
**步骤 3: View (`MainWindow.xaml`)**
|
||
|
||
`ContentControl` 绑定到 `CurrentViewModel`,并使用 `DataTemplate` 来根据ViewModel的类型选择对应的View。
|
||
|
||
```xml
|
||
<ContentControl Content="{Binding CurrentViewModel}">
|
||
<ContentControl.Resources>
|
||
<DataTemplate DataType="{x:Type vm:DashboardViewModel}">
|
||
<views:DashboardView />
|
||
</DataTemplate>
|
||
<DataTemplate DataType="{x:Type vm:DeviceListViewModel}">
|
||
<views:DeviceListView />
|
||
</DataTemplate>
|
||
<!-- 其他模板 -->
|
||
</ContentControl.Resources>
|
||
</ContentControl>
|
||
```
|