前言
工业4.0时代,工业监控系统开发需求激增,但传统WinForm开发中界面逻辑、业务逻辑与数据访问高度耦合,导致代码维护困难、测试覆盖率低。
MVP(Model-View-Presenter)架构模式通过分离职责、解耦组件,为C#开发提供更清晰的代码结构和更易维护的解决方案。
本文以工业设备监控系统为例,详细解析MVP架构的设计与实现,涵盖实时数据更新、异步操作及异常处理等核心问题。
问题分析
传统WinForm开发中,界面(UI)、业务逻辑与数据访问常混杂在同一代码文件中,导致三大问题:
1、代码耦合度高:修改需求需同时调整多个层面,易引入新Bug。
2、难以测试:UI控件与业务逻辑绑定,单元测试需模拟控件行为,成本高。
3、维护成本高:需求变更时,需重新梳理代码逻辑,效率低下。
MVP模式通过将应用拆分为Model(模型)、View(视图)、Presenter(展示器)三部分,有效解决上述问题。
项目效果
解决方案一
清晰的MVP架构
核心思想:
-
Model:封装数据与业务逻辑,独立于UI。
-
View:仅负责界面展示,通过接口与Presenter通信。
-
Presenter:协调Model与View,处理用户交互。
架构组件关系图:
实战代码
1、设备模型设计
public classEquipment : INotifyPropertyChanged
{
privatestring _id;
privatestring _name;
private EquipmentStatus _status;
privatedouble _temperature;
privatedouble _pressure;
privatedouble _speed;
private DateTime _lastUpdated;
publicstring Id
{
get => _id;
set
{
_id = value;
OnPropertyChanged(nameof(Id));
}
}
// 温度属性 - 支持数据绑定
publicdouble Temperature
{
get => _temperature;
set
{
_temperature = value;
OnPropertyChanged(nameof(Temperature));
}
}
publicevent PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
publicenum EquipmentStatus
{
Stopped, // 停止
Running, // 运行中
Warning, // 警告
Error, // 故障
Maintenance // 维护中
}
关键亮点
-
实现
INotifyPropertyChanged接口,支持数据绑定。 -
枚举定义设备状态(如
Running、Error),提升可读性。
2、视图接口设计
public interfaceIMainView
{
// 属性绑定
string SelectedEquipmentId { get; }
Equipment CurrentEquipment { get; set; }
List<Equipment> EquipmentList { set; }
// 用户交互事件
event EventHandler ViewLoaded;
event EventHandler<string> EquipmentSelected;
event EventHandler StartEquipmentClicked;
event EventHandler StopEquipmentClicked;
// 界面操作方法
void ShowMessage(string message, string title = "信息");
void ShowError(string message, string title = "错误");
void UpdateEquipmentStatus(Equipment equipment);
}
设计亮点
-
接口分离原则,便于Mock测试。
-
事件驱动模式,解耦用户操作与业务逻辑。
解决方案二:实现高效的Presenter层
Presenter层作为Model与View的桥梁,需处理异步操作与异常。
例如,设备启动逻辑:
namespaceAppMVPIndustrialMonitorSystem.Presenters
{
publicclassMainPresenter
{
privatereadonly IMainView _view;
privatereadonly IEquipmentService _equipmentService;
public MainPresenter(IMainView view, IEquipmentService equipmentService)
{
_view = view ?? thrownew ArgumentNullException(nameof(view));
_equipmentService = equipmentService ?? thrownew ArgumentNullException(nameof(equipmentService));
SubscribeToEvents();
}
private void SubscribeToEvents()
{
_view.ViewLoaded += OnViewLoaded;
_view.EquipmentSelected += OnEquipmentSelected;
_view.StartEquipmentClicked += OnStartEquipmentClicked;
_view.StopEquipmentClicked += OnStopEquipmentClicked;
_view.RefreshDataClicked += OnRefreshDataClicked;
_view.UpdateParametersClicked += OnUpdateParametersClicked;
_equipmentService.EquipmentStatusChanged += OnEquipmentStatusChanged;
}
private async void OnViewLoaded(object sender, EventArgs e)
{
try
{
await LoadEquipmentListAsync();
}
catch (Exception ex)
{
_view.ShowError($"加载设备列表失败:{ex.Message}");
}
}
private async void OnEquipmentSelected(object sender, string equipmentId)
{
try
{
if (string.IsNullOrEmpty(equipmentId))
{
_view.CurrentEquipment = null;
return;
}
var equipment = await _equipmentService.GetEquipmentByIdAsync(equipmentId);
_view.CurrentEquipment = equipment;
// 加载生产数据
await LoadProductionDataAsync(equipmentId);
}
catch (Exception ex)
{
_view.ShowError($"加载设备详情失败:{ex.Message}");
}
}
private async void OnStartEquipmentClicked(object sender, EventArgs e)
{
try
{
var equipmentId = _view.SelectedEquipmentId;
if (string.IsNullOrEmpty(equipmentId))
{
_view.ShowMessage("请先选择设备");
return;
}
var success = await _equipmentService.StartEquipmentAsync(equipmentId);
if (success)
{
_view.ShowMessage("设备启动成功");
}
else
{
_view.ShowError("设备启动失败");
}
}
catch (Exception ex)
{
_view.ShowError($"启动设备失败:{ex.Message}");
}
}
private async void OnStopEquipmentClicked(object sender, EventArgs e)
{
try
{
var equipmentId = _view.SelectedEquipmentId;
if (string.IsNullOrEmpty(equipmentId))
{
_view.ShowMessage("请先选择设备");
return;
}
var success = await _equipmentService.StopEquipmentAsync(equipmentId);
if (success)
{
_view.ShowMessage("设备停止成功");
}
else
{
_view.ShowError("设备停止失败");
}
}
catch (Exception ex)
{
_view.ShowError($"停止设备失败:{ex.Message}");
}
}
private async void OnRefreshDataClicked(object sender, EventArgs e)
{
try
{
await LoadEquipmentListAsync();
var equipmentId = _view.SelectedEquipmentId;
if (!string.IsNullOrEmpty(equipmentId))
{
await LoadProductionDataAsync(equipmentId);
}
_view.ShowMessage("数据刷新成功");
}
catch (Exception ex)
{
_view.ShowError($"刷新数据失败:{ex.Message}");
}
}
private async void OnUpdateParametersClicked(object sender, EventArgs e)
{
try
{
var currentEquipment = _view.CurrentEquipment;
if (currentEquipment == null)
{
_view.ShowMessage("请先选择设备");
return;
}
var success = await _equipmentService.UpdateEquipmentAsync(currentEquipment);
if (success)
{
_view.ShowMessage("参数更新成功");
}
else
{
_view.ShowError("参数更新失败");
}
}
catch (Exception ex)
{
_view.ShowError($"更新参数失败:{ex.Message}");
}
}
private void OnEquipmentStatusChanged(object sender, Equipment equipment)
{
_view.UpdateEquipmentStatus(equipment);
}
private async Task LoadEquipmentListAsync()
{
var equipments = await _equipmentService.GetAllEquipmentAsync();
_view.EquipmentList = equipments;
}
private async Task LoadProductionDataAsync(string equipmentId)
{
var endTime = DateTime.Now;
var startTime = endTime.AddHours(-1); // 获取最近1小时的数据
var productionData = await _equipmentService.GetProductionDataAsync(equipmentId, startTime, endTime);
_view.ProductionDataList = productionData;
}
}
}
核心优势
-
依赖注入(如
IEquipmentService)提升可测试性。 -
统一异常处理机制,避免UI阻塞。
解决方案三:服务层的实时数据处理
通过Timer模拟传感器数据更新,并通过事件通知UI:
public classEquipmentService : IEquipmentService
{
privatereadonly List<Equipment> _equipments;
privatereadonly Timer _statusUpdateTimer;
privatereadonly Random _random;
publicevent EventHandler<Equipment> EquipmentStatusChanged;
public EquipmentService()
{
_random = new Random();
_equipments = GenerateTestEquipment();
// 🔥 关键:使用定时器模拟实时数据更新
_statusUpdateTimer = new Timer(UpdateEquipmentStatus, null,
TimeSpan.Zero, TimeSpan.FromSeconds(5));
}
private void UpdateEquipmentStatus(object state)
{
foreach (var equipment in _equipments.Where(e => e.Status == EquipmentStatus.Running))
{
// 模拟传感器数据变化
var tempChange = (_random.NextDouble() - 0.5) * 2;
equipment.Temperature = Math.Max(0, Math.Min(200, equipment.Temperature + tempChange));
var pressureChange = (_random.NextDouble() - 0.5) * 0.5;
equipment.Pressure = Math.Max(0, Math.Min(20, equipment.Pressure + pressureChange));
equipment.LastUpdated = DateTime.Now;
// 🚀 触发状态变更事件
EquipmentStatusChanged?.Invoke(this, equipment);
}
}
public async Task<bool> StartEquipmentAsync(string id)
{
await Task.Delay(100); // 模拟网络延迟
var equipment = _equipments.FirstOrDefault(e => e.Id == id);
if (equipment != null)
{
equipment.Status = EquipmentStatus.Running;
equipment.LastUpdated = DateTime.Now;
EquipmentStatusChanged?.Invoke(this, equipment);
returntrue;
}
returnfalse;
}
}
实战技巧
-
边界值控制(如温度范围0-200℃)避免异常数据。
-
事件机制实现解耦,避免直接引用View。
解决方案四:UI层的优雅实现
跨线程更新UI时,需通过Invoke确保线程安全:
public partialclassFrmMain : Form, IMainView
{
public void UpdateEquipmentStatus(Equipment equipment)
{
// 关键:处理跨线程调用
if (InvokeRequired)
{
Invoke(new Action<Equipment>(UpdateEquipmentStatus), equipment);
return;
}
// 更新列表中的设备状态
for (int i = 0; i < lstEquipment.Items.Count; i++)
{
if (lstEquipment.Items[i] is EquipmentListItem item &&
item.Equipment.Id == equipment.Id)
{
item.Equipment = equipment;
// 刷新显示
var tempItem = lstEquipment.Items[i];
lstEquipment.Items.RemoveAt(i);
lstEquipment.Items.Insert(i, tempItem);
break;
}
}
// 更新状态栏
tslStatus.Text = $"设备 {equipment.Name} 状态已更新";
}
private void UpdateStatusDisplay(EquipmentStatus status)
{
switch (status)
{
case EquipmentStatus.Running:
lblStatusValue.Text = "运行中";
lblStatusValue.ForeColor = Color.Green;
break;
case EquipmentStatus.Warning:
lblStatusValue.Text = "警告";
lblStatusValue.ForeColor = Color.Orange;
break;
case EquipmentStatus.Error:
lblStatusValue.Text = "故障";
lblStatusValue.ForeColor = Color.Red;
break;
}
}
}
解决方案五:异常处理最佳实践
统一捕获异常并分类处理:
private async void OnRefreshDataClicked(object sender, EventArgs e)
{
try
{
//使用using确保资源释放
using (var loadingForm = new LoadingForm())
{
loadingForm.Show();
await LoadEquipmentListAsync();
var equipmentId = _view.SelectedEquipmentId;
if (!string.IsNullOrEmpty(equipmentId))
{
await LoadProductionDataAsync(equipmentId);
}
}
_view.ShowMessage("数据刷新成功");
}
catch (HttpRequestException httpEx)
{
_view.ShowError($"网络连接失败:{httpEx.Message}");
}
catch (TimeoutException timeoutEx)
{
_view.ShowError($"操作超时:{timeoutEx.Message}");
}
catch (Exception ex)
{
_view.ShowError($"刷新数据失败:{ex.Message}");
// 记录详细日志
Logger.LogError(ex, "RefreshData failed");
}
}
核心要点
1、架构设计:MVP模式通过职责分离降低耦合度,提升可维护性。
2、实时处理:Timer+事件机制实现高效数据更新,避免直接轮询。
3、异步编程:async/await简化异步操作,统一异常处理提升用户体验。
4、线程安全:跨线程更新UI时使用Invoke,防止界面卡死。
总结
MVP架构为工业监控系统提供清晰的代码结构与高效的维护方案。
-
如何设计解耦的MVP组件。
-
如何实现实时数据更新与异步操作。
-
如何统一处理异常与日志。
关键词
#MVP架构、C#、#工业监控、#实时数据、#异步编程、#异常处理、#WinForm、#解耦设计、#数据绑定、#异步处理、#实时更新
作者:技术老小子

.NET 9 可视化工作流引擎!纯血开源,支持数十种数据库,开箱即用
WinForm + STM32 打造稳定好用的工业设备远程升级工具
Visual Studio 2026 上手体验,AI 懂你、界面清爽、协作无缝
不用 GPU 也能跑的 WPF 视觉检测软件(Emgu CV + SQLite)
C# 工业级扫码难题破解,用微信实现精准扫码并自动填入任意应用
工业软件缺好 UI?这套 .NET 控件库从 IO 灯到圆角按钮全搞定
基于 .NET + Vue 3 的线路图绘制系统实战(含源码)
WinForm 下基于策略与工厂模式的 PLC 数据采集与监控系统
.NET 8 + Avalonia 跨平台简易校园信息管理系统的开发实战
C# + WPF + SuperSocket 开发面向工业自动化的 MES 系统
告别服务宕机,C# 看门狗守护你的 WinForm 与 Windows 服务
.NET 一款高效跨平台的自动更新工具(差异更新+热修复+自动升级)
面向工厂自动化的智能语音播报方案(基于.NET Windows服务)
工业自动化UI太难做?WPF 这套工业级控件方案真香(附源码)
工业自动化 WPF + Halcon 的模块化机器视觉解决方案
开源福利!八款 WPF + HandyControl 工业管理系统源码全公开
WinForm + Win32 API 自定义无边框窗口实战(工业软件必备)
基于 HslCommunication 的多端同步PLC远程监控系统
WinForm 数据采集实战:从串口通信到MES对接的轻量化解决方案
一个拒绝过度设计的 .NET 快速开发框架:开箱即用,专注"干活"
WinForm + SunnyUI 与 MQTTnet 实现智能可视化的火警联动大屏系统
.NET 9 + WPF + Halcon 构建工业视觉流程框架:从架构设计到落地实践
WinForm 高分屏适配难题?一款强大的控件自适应缩放工具
觉得有收获?不妨分享让更多人受益
关注「DotNet技术匠」,共同提升技术实力


