环境: VS2022+.Net Framework4.8
NuGet包:EasyModbusTCP 5.6.0
程序名:WPFA_EasyModbus
一、功能概述
- 这是一个基于 WPF (.NET) 的 Modbus TCP 工业设备点位状态监控与控制系统,主要用于与 PLC/工业控制器通过 Modbus TCP 协议通讯,实时监控和操控现场设备的数字量点位。
| 功能模块 | 说明 |
|---|---|
| 连接管理 输入 | IP 和端口,连接/断开 Modbus TCP 服务器(默认 192.168.0.7:8234) |
| 离散输入监控 | 轮询读取 8 路离散输入(DI0~DI7),如"开关灯/称重电源""拍照""测厚""称重""去皮"等按钮/信号状态 |
| 线圈状态监控 | 轮询读取 9 路线圈(CO1、CO3、CO4、CO5、CO8、CO9、CO11、CO13、CO14),如"拍照灯""按钮灯""伸缩杆抬起/压下""照明灯""测厚启动""称重启动"等 |
| 线圈写控制 | 每个线圈点位提供 ON/OFF 按钮,可远程写单个线圈 |
| LED 指示灯 | 绿色=ON,灰色=OFF,通过 BoolToBrushConverter 或 Color 属性绑定实现 |
| 运行日志 | 带时间戳的日志输出,支持清空 |
| 断线自动重连 | 轮询线程中发现连接断开时自动重连 |
| 资源释放 | 窗口关闭时停止轮询、断开连接、释放资源 |
![]() |
二、技术栈分析
| 技术 | 用途 |
|---|---|
| WPF (.NET Framework / .NET) | UI 框架,XAML 声明式界面 |
| C# | 主要开发语言 |
| MVVM 模式 | UI 与逻辑解耦的架构模式 |
| EasyModbus | 第三方 Modbus TCP 客户端库,负责 ModbusClient 的读写 |
| 数据绑定 (DataBinding) | {Binding} 实现 UI 与 ViewModel 的自动同步 |
| ICommand / Relay | Command 命令模式,替代事件处理 |
| INotifyPropertyChanged | 属性变更通知机制,驱动 UI 更新 |
| ObservableCollection | 集合变更通知,用于 ItemsControl 绑定 |
| IValueConverter | BoolToBrushConverter 布尔→画刷转换 |
| Task / CancellationToken | 后台轮询线程与取消机制 |
| Dispatcher | 跨线程更新 UI(后台线程 → UI 线程) |
| IDisposable | 资源释放模式 |
| StringFormat / RelativeSource | XAML 高级绑定技巧(如 {0}:True 参数拼接、AncestorType=Window 找上级 DataContext) |
三、MVVM 设计框架原理与应用详解
1、 MVVM 基本原理
- MVVM(Model-View-ViewModel)是 MVC 的 WPF 特化版本,核心思想是通过数据绑定把视图和逻辑分离,让 UI 开发者只关心 XAML,逻辑开发者只关心 ViewModel。
View:只负责界面呈现和绑定,几乎不含业务代码
ViewModel:持有状态(属性)和行为(命令),通过 INotifyPropertyChanged 通知 View 更新
Model:数据实体和业务服务(如 BitStatusItem、ModbusService)
关键机制:
- 数据绑定:View 用 {Binding Xxx} 连接到 ViewModel 的属性,属性变化自动刷新 UI。
- 命令绑定:View 用 Command="{Binding XxxCommand}" 调用 ViewModel 的方法。
- 双向绑定:TextBox Text="{Binding IpAddress, UpdateSourceTrigger=PropertyChanged}" 用户输入自动写回 ViewModel。
2、具体应用
(1)Model 层
| 类 | 角色 |
|---|---|
| BitStatusItem | 单个点位数据模型(Address / Name / Value / ValueText / Color),实现 INotifyPropertyChanged |
| ModbusService | 业务服务模型:封装 EasyModbus 客户端、连接、轮询、读写线圈,并持有两个 ObservableCollection |
-
BitStatusItem.Value 的 setter 是典型的模型自通知
public bool Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
OnPropertyChanged(); // 通知 Value 变化
OnPropertyChanged(nameof(ValueText)); // 联动通知 ValueText
OnPropertyChanged(nameof(Color)); // 联动通知 Color
}
}
}
这样 UI 上绑定的 {Binding ValueText}、{Binding Color} 会自动刷新,无需手动更新界面。
(2)ViewModel 层
MainViewModel 是核心,它:
-
持有 Model:_service = new ModbusService()
-
暴露集合给 View:
public ObservableCollection
DiscreteInputs => _service.DiscreteInputs;
public ObservableCollectionCoils => _service.Coils; -
暴露属性:IpAddress、Port、IsConnected、IsPolling、LogText,均通过 SetProperty 触发通知
-
暴露命令(ICommand):
- ConnectCommand / DisconnectCommand
- StartPollCommand / StopPollCommand
- WriteCoilCommand
- ClearLogCommand
MainViewModel 继承自 ViewModelBase,后者实现了 INotifyPropertyChanged 和泛型 SetProperty辅助方法:
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string name = null)
{
if (Equals(field, value)) return false;
field = value;
OnPropertyChanged(name);
return true;
}
CallerMemberName 让调用处写 SetProperty(ref _port, value) 即可自动传入属性名 "Port",减少硬编码。
(3)View 层
MainWindow.xaml 中几乎没有业务逻辑,全部是声明式绑定:
① 绑定 ViewModel 作为 DataContext
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
② 属性双向绑定
<TextBox Text="{Binding IpAddress, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Text="{Binding Port, UpdateSourceTrigger=PropertyChanged}"/>
③ 命令绑定按钮
<Button Content="连接" Command="{Binding ConnectCommand}"/>
<Button Content="开始轮询" Command="{Binding StartPollCommand}"/>
④ 集合绑定
<ItemsControl ItemsSource="{Binding DiscreteInputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding ValueText}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
⑤ 跨层级命令绑定
-
(ItemsControl 内每个 DataTemplate 的 DataContext 是 BitStatusItem,要访问 MainViewModel 的 WriteCoilCommand 必须向上找):
这里 RelativeSource AncestorType=Window 沿可视化树向上找 Window,取其 DataContext(即 MainViewModel)的 WriteCoilCommand,并把 Address:True 作为参数传过去。
⑥ 值转换器(布尔→颜色):
<Border Background="{Binding IsConnected, Converter={StaticResource BoolToBrush}}"/>
(4)命令模式 RelayCommand
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Predicate<object> _canExecute;
public bool CanExecute(object p) => _canExecute == null || _canExecute(p);
public void Execute(object p) => _execute(p);
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
RelayCommand 让 ViewModel 用一个 Lambda 即可定义按钮行为,不必为每个按钮写 Click 事件处理函数。
例:
WriteCoilCommand = new RelayCommand(param =>
{
if (param is string s && s.Split(':').Length == 2)
{
if (int.TryParse(s.Split(':')[0], out int addr) &&
bool.TryParse(s.Split(':')[1], out bool val))
{
_service.WriteCoil(addr, val);
}
}
});
XAML 传参 CommandParameter="{Binding Address, StringFormat={}{0}:True}",产生如 "1:True" 字符串,ViewModel 解析后调用服务。
3 线程与 MVVM 的配合
Modbus 轮询是后台线程,而 UI 只能在 UI 线程更新,因此:
System.Windows.Application.Current?.Dispatcher.Invoke(() => item.Value = v);
Dispatcher.Invoke 把"设置属性"的操作切到 UI 线程执行。由于 BitStatusItem 实现了 INotifyPropertyChanged,一旦 UI 线程设值,WPF 绑定系统会自动刷新对应控件。
日志同理:
private void OnLog(string msg)
{
var dispatcher = System.Windows.Application.Current?.Dispatcher;
dispatcher.Invoke(() => LogText += msg + Environment.NewLine);
}
4 资源释放的 MVVM 处理
View 不直接调业务方法,而是通过 ViewModel 暴露的 DisposeService() 释放,避免 View 依赖 ModbusService:
// MainWindow.xaml.cs
protected override void OnClosed(EventArgs e)
{
(DataContext as ViewModels.MainViewModel)?.DisposeService();
base.OnClosed(e);
}
// MainViewModel
public void DisposeService() => _service.Dispose();
这样 View 只知道"要在关闭时通知 ViewModel",不知道具体怎么释放,符合 MVVM 解耦原则。
5 MVVM 在本程序中的优势总结
| 优势 | 体现 |
|---|---|
| UI 与逻辑彻底解耦 | XAML 中无一行 C# 业务代码,ViewModel 不引用任何 WPF 控件 |
| 可测试性 | 可对 MainViewModel、ModbusService 写单元测试,无需启动 UI |
| 可维护性 | 修改界面不影响逻辑,修改逻辑不影响界面 |
| 数据驱动 | 属性改变自动刷新 UI,避免手动 textBox.Text = ... |
| 命令驱动 | 用 ICommand 替代事件,便于统一管理启用/禁用状态 |
| 可扩展性 | 增加新点位只需在 ModbusService 初始化列表增加一行;增加新功能只需在 ViewModel 加命令,XAML 加绑定 |
四、源码
https://download.csdn.net/download/dalong10/93471541
