依赖注入 DI
注入 = 自己不 new 对象,由容器帮你实例化硬件/服务,通过构造函数传进来
杜绝VM内部 new EtherCATMaster()、new XYScanAxis()。
二、两种写法对比
❌ 非注入(坏写法,AxisEcatVM老代码)
csharp
//VM内部自己new硬件,重复实例、抢总线资源
public AxisEcatVM()
{
_master = new EtherCATMaster();
_axis = new XYScanAxis(_master);
}
问题:开两次页面=创建2套EtherCAT主站,总线冲突、资源泄漏。
✅ 构造注入(规范写法,AxisControlViewModel)
csharp
//容器提前建好全局唯一实例,直接传参进来
public AxisControlViewModel(IXYScanAxis scanAxis)
{
_axis = scanAxis;
}
EtherCATMaster、XYScanAxis 在程序启动时DI容器只创建1个单例,全项目共用。
三、DI三步落地流程
1、注册服务(Program.cs/App启动入口)
告诉容器:接口对应哪个实现、生命周期(硬件统一单例Singleton)
csharp
//硬件全局只实例一次
services.AddSingleton<EtherCATMaster>();
services.AddSingleton<IXYScanAxis,XYScanAxis>();
AddSingleton:单例,全项目唯一实例(EtherCAT、相机独占硬件必用)AddTransient:每次使用新建对象(普通工具类)
2、VM构造函数接收(注入接收)
所有VM不再new硬件,构造参数接收接口
csharp
public sealed partial class AxisEcatVM
{
private readonly IXYScanAxis _axis;
//容器自动把注册好的实例传入
public AxisEcatVM(IXYScanAxis axis)
{
_axis = axis;
}
}
3、页面绑定DataContext由容器创建
csharp
//从容器拿VM,自动完成依赖注入
DataContext = App.Current.Services.GetService<AxisEcatVM>();
四、4个核心好处
- 硬件唯一实例:全项目共用同一个EtherCAT主站,不会重复初始化网卡、总线不冲突
- 方便单元测试 :测试时替换仿真实现
MockXYScan,不用启动真实硬件 - 统一生命周期管理:容器统一释放资源,减少Dispose遗漏泄漏
- 代码统一规范:所有VM统一注入,不再一半new一半注入
启动注册→构造接收→容器实例化,全程不手写new硬件
补充:属性注入(少用)
极少场景用,优先构造注入(工业项目规范首选)。
结合你正在做的 WPF 上位机开发 + Dispatcher 分支 ,我用最直白、最贴合你代码的方式讲清楚:
什么是「非事件处理器」?
先搞懂:事件处理器
事件处理器 = 专门响应「事件」的方法
是被动触发的,只有事件发生才会跑。
✅ 你的项目里的事件处理器:
csharp
// 1. 按钮点击事件(UI事件)
private void BtnStartScan_Click(object sender, RoutedEventArgs e) { }
// 2. 窗口加载事件
private void MainWindow_Loaded(object sender, RoutedEventArgs e) { }
// 3. 消息订阅事件
private void OnAxisStatusMessageReceived(AxisStatusMessage msg) { }
非事件处理器 = 普通业务方法
不是为了响应事件而写的方法
是主动调用的,写核心逻辑、硬件控制、工具功能。
✅ 你的上位机里的非事件处理器(核心代码):
csharp
// 1. 初始化 EtherCAT 总线
private void InitEtherCATMaster() { }
// 2. 控制轴移动
public void MoveAxisToPosition(double x, double y) { }
// 3. 启动扫描逻辑
public void StartScanMode() { }
// 4. 更新 UI 状态(Dispatcher 封装)
private void UpdateUIData(AxisStatus status) { }
// 5. 校验限位
private bool CheckAxisLimit() { }
核心区别
| 类型 | 触发方式 | 线程 | 用途 |
|---|---|---|---|
| 事件处理器 | 被动(点击/消息/加载触发) | WPF 默认在 UI 线程 | 只做「触发」 |
| 非事件处理器 | 主动(代码里手动调用) | 可能在 后台线程(轴控制/视觉检测) | 写「核心业务/硬件逻辑」 |
- 事件处理器(UI线程)→ 直接改UI ✅
- 非事件处理器 (后台线程:EtherCAT/轴/扫描)→ 不能直接改UI ❌
→ 必须用Dispatcher调度到UI线程
示例
csharp
// 非事件处理器:后台线程执行轴运动
private void UpdateAxisStatus() // 非事件处理器
{
// 后台线程不能直接改UI,用Dispatcher
Application.Current.Dispatcher.Invoke(() =>
{
AxisPosX = _scanAxis.PosX; // UI更新
});
}
- 事件处理器:等着被触发(按钮点、消息来)
- 非事件处理器:主动跑业务(初始化、动轴、扫描、算数据)
- 你在
Dispatcher分支做的优化:
让「非事件处理器」能安全更新UI,这是 WPF 工控上位机的标准规范!
一、现有代码两处隐患(工控致命问题)
InitMaster() 属于非事件处理器(总线初始化业务方法)
csharp
public void InitMaster()
{
//_=StartBus(); 点火即弃写法
_ = StartBus();
}
-
_ = StartBus()隐患:吞异常StartBus()返回Task,用丢弃赋值_=属于fire-and-forget,总线启动内部报错(网卡断开、从站缺失、DC同步失败)不会抛出、不会被捕获、无任何日志 ,EtherCAT静默启动失败,上位机无提示、设备失控。.NET规则:未被await/未绑定异常回调的异常Task,后期会随机触发程序进程崩溃。
-
被注释
BusLog/BusState隐患:跨线程UI报错StartBus运行在后台异步线程 ,直接给VM绑定属性BusLog、BusState赋值 = 跨线程修改WPF依赖属性,触发跨线程异常(正好是你feature/Dispatcher分支优化目标)。
方案1【工控最优:改 async Task,规范写法,推荐落地】
修改方法签名,await等待总线,try-catch捕获所有异常,UI状态通过Dispatcher切UI线程赋值
csharp
/// <summary> EtherCAT总线初始化,返回Task便于外部等待与捕获异常 </summary>
public async Task InitMaster()
{
try
{
// 调度到UI线程刷新日志(你Dispatcher分支封装的方法)
DispatcherHelper.Invoke(() => BusLog = "EtherCAT总线初始化中...");
// 真正等待总线启动,异常直接进catch,不会吞报错
await StartBus();
DispatcherHelper.Invoke(() =>
{
BusState = ECATBusState.运行中;
BusLog = "EtherCAT总线运行中,正在进行DC时钟同步...";
});
}
catch (Exception ex)
{
// 总线启动全异常捕获:网卡故障、从站失联、DC同步失败全落日志
DispatcherHelper.Invoke(() => BusLog = $"总线初始化失败:{ex.Message}");
// 可额外写入日志服务LogService
}
}
调用方变更:
await _ecatMaster.InitMaster();,外部可捕获启动失败,工控标准。
方案2【无法修改调用处、必须保留void签名:改良fire-and-forget】
不动方法返回值,给丢弃Task绑定异常回调,杜绝静默吞错
csharp
public void InitMaster()
{
_ = StartBus()
.ContinueWith(t =>
{
if (t.Exception != null)
{
var ex = t.Exception.InnerException;
// Dispatcher切UI更新日志
DispatcherHelper.Invoke(() => BusLog = $"总线异常:{ex.Message}");
}
}, TaskScheduler.FromCurrentSynchronizationContext());
}
_ = Task= 抛弃异常,工控禁用在硬件初始化代码;- 总线初始化优先
async Task + await,异常全可控; - 后台任务改UI属性 → 强制Dispatcher调度。
git
csharp
# 1. 准备
cd /d/vsprogram/WpfApp6
git checkout master
git pull
# 2. 建分支
git checkout -b fix-axis
# 3. 改代码...
# 4. 提交
git add .
git commit -m "完成轴控VM优化"
git push -u origin fix-axis
# 5. 合并到master
git checkout master
git merge fix-axis
# 6. 同步云端master
git push
bash
11400@▒▒ MINGW64 ~
$ git checkout main
fatal: not a git repository (or any of the parent directories): .git
11400@ MINGW64 ~
$ cd /d/vsprogram/WpfApp6
11400@ MINGW64 /d/vsprogram/WpfApp6 (xaml)
$ git checkout master
Switched to branch 'master'
11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git pull
fatal: unable to access 'https://github.com/zhixincao1123/WpfApp6.git/': Recv failure: Connection was reset
11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git checkout -b disp
Switched to a new branch 'disp'
11400@ MINGW64 /d/vsprogram/WpfApp6 (disp)
$ git status
On branch disp
nothing to commit, working tree clean
11400@ MINGW64 /d/vsprogram/WpfApp6 (disp)
$ git checkout xaml
Switched to branch 'xaml'
11400@ MINGW64 /d/vsprogram/WpfApp6 (xaml)
$ git checkout -b feature/disp优化
Switched to a new branch 'feature/disp优化'
11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/disp优化)
$ git checkout -b feature/message
Switched to a new branch 'feature/message'
11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/message)
$ git add .
11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/message)
$ git commit -m "添加AxisEcatVM 在轴状态刷新和运行时发送总线状态与扫描进度消息,MainShellVM 订阅并更新 UI(BusStatus、Rate)"
[feature/message 9a6ed8c] 添加AxisEcatVM 在轴状态刷新和运行时发送总线状态与扫描进度消息,MainShellVM 订阅并更新 UI(BusStatus、Rate)
1 file changed, 3 insertions(+), 3 deletions(-)
11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/message)
$ git add .
11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/message)
$ git commit -m "MainShellVM:订阅新消息并在 UI 线程更新
BusStatus 与 Rate(界面上绑定这两个代理属性 。"
[feature/message 30ab948] MainShellVM:订阅新消息并在 UI
线程更新 BusStatus 与 Rate(界面上绑定这两个代理属性)。
3 files changed, 51 insertions(+), 6 deletions(-)
11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/message)
$ git checkout master
Switched to branch 'master'
11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git merge feature/message
Updating 5ef4fe5..30ab948
Fast-forward
.../Legacy}/AxisControlViewModel.cs | 0
WpfApp6/MainViewModel.cs | 598
---------------------
WpfApp6/MainWindow.xaml | 48
+-
WpfApp6/MainWindow.xaml.cs | 37
+-
WpfApp6/Messages/AppMessages.cs | 63
++-
WpfApp6/ViewModel/AxisEcatVM.cs | 67
++-
WpfApp6/ViewModel/MainShellVM.cs | 87
++-
WpfApp6/ViewModel/MainViewModel.cs | 4
+-
WpfApp6/ViewModel/XrayImageVM.cs | 9
+-
WpfApp6/WpfApp6.csproj | 1
-
10 files changed, 202 insertions(+), 712 deletions(-)
rename {WpfApp6/ViewModel => Archive/Legacy}/AxisControl
ViewModel.cs (100%)
delete mode 100644 WpfApp6/MainViewModel.cs
11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git checkout -b feature/Dispatcher
Switched to a new branch 'feature/Dispatcher'
11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/Dispatcher)
$ git add .
11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/Dispatcher)
$ git commit -m "使所有注册 Messenger 的 VM 在 Dispose 中对称释放资源(CTS、Timer、Halcon 对象、消息订阅),并避免阻塞或抛异常。"
[feature/Dispatcher 6f3b6d6] 使所有注册 Messenger 的 VM在 Dispose 中对称释放资源(CTS、Timer、Halcon 对象、消息订阅),并避免阻塞或抛异常。
1 file changed, 21 insertions(+), 5 deletions(-)
11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/Dispatcher)
$ git add .
11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/Dispatcher)
$ git commit -m "修改MainShellVM"
On branch feature/Dispatcher
nothing to commit, working tree clean
11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/Dispatcher)
$ git add .
11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/Dispatcher)
$ git commit -m "修改MainShellVM"
[feature/Dispatcher 7b1510b] 修改MainShellVM
3 files changed, 70 insertions(+), 51 deletions(-)
11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/Dispatcher)
$ git checkout master
Switched to branch 'master'
11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git merge feature/Dispatcher
Updating 30ab948..7b1510b
Fast-forward
WpfApp6/ViewModel/AxisEcatVM.cs | 89 +++++++++++++++++++++-------------------
WpfApp6/ViewModel/MainShellVM.cs | 6 +--
WpfApp6/ViewModel/XrayImageVM.cs | 52 ++++++++++++++++++-----
3 files changed, 91 insertions(+), 56 deletions(-)
11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ ^[[200~git grep -n -e "async void" -- "*.cs" || true
bash: $'\E[200~git': command not found
11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git grep -n -e "async void" -- "*.cs" || true
git grep -n -e "\.Wait(" -- "*.cs" || true
git grep -n -e "\.Result" -- "*.cs" || true
git grep -n -e "Task.Delay(.*)\.Wait" -- "*.cs" || true
git grep -n "Dispatcher.Invoke(" -- "*.cs" || true
git grep -n "Dispatcher.InvokeAsync(" -- "*.cs" || true
git grep -n "WeakReferenceMessenger.Default.Register<" -- "*.cs" || true
git grep -n "UnregisterAll(this)" -- "*.cs" || true
git grep -n "new EtherCATMaster" -- "*.cs" || true
git grep -n "new XYScanAxis" -- "*.cs" || true
git grep -n "_=StartBus\(" -- "*.cs" || true
git grep -n "new CancellationTokenSource" -- "*.cs" || true
WpfApp6/EtherCATMaster.cs:26: //Task.Delay(100
0).Wait();
WpfApp6/ViewModel/AxisEcatVM.cs:115: Task.
Delay(100).Wait(); // 等待任务响应取消
WpfApp6/EtherCATMaster.cs:26: //Task.Delay(1000).Wait();
WpfApp6/ViewModel/AxisEcatVM.cs:115: Task.
Delay(100).Wait(); // 等待任务响应取消
WpfApp6/Services/LogService.cs:19: Application
.Current.Dispatcher.Invoke(() =>
WpfApp6/ViewModel/AxisEcatVM.cs:62: Application.Current.Dispatcher.Invoke(() =>
WpfApp6/ViewModel/XrayImageVM.cs:41: A
pplication.Current.Dispatcher.Invoke(() =>
WpfApp6/ViewModel/XrayImageVM.cs:68: Appli
cation.Current.Dispatcher.Invoke(() =>
WpfApp6/ViewModel/XrayImageVM.cs:98: Application.Current.Dispatcher.Invoke(() => ClearWindow())
;
WpfApp6/ViewModel/AxisEcatVM.cs:51: WeakReferenceMessenger.Default.Register<StartScanMessage>(this, async (recipient, msg) =>
WpfApp6/ViewModel/AxisEcatVM.cs:53: WeakRefere
nceMessenger.Default.Register<StopScanMessage>(this, (recipient, msg) => Stop());
WpfApp6/ViewModel/MainShellVM.cs:79: WeakRefer
enceMessenger.Default.Register<LogMessage>(this, (recipient, msg) =>
WpfApp6/ViewModel/MainShellVM.cs:83: WeakRefer
enceMessenger.Default.Register<BusStatusMessage>(this, (recipient, msg) =>
WpfApp6/ViewModel/MainShellVM.cs:87: WeakRefer
enceMessenger.Default.Register<ScanProgressMessage>(this, (recipient, msg) => {
WpfApp6/ViewModel/XrayImageVM.cs:25: WeakRefer
enceMessenger.Default.Register<AxisPositionReadyMessage>(this, (r, m) =>
...skipping...
WpfApp6/ViewModel/AxisEcatVM.cs:53: WeakRefere
nceMessenger.Default.Register<StopScanMessage>(this, (recipient, msg) => Stop());
WpfApp6/ViewModel/MainShellVM.cs:79: WeakReferenceMessenger.Default.Register<LogMessage>(this, (recipient, msg) =>
WpfApp6/ViewModel/MainShellVM.cs:83: WeakReferenceMessenger.Default.Register<BusStatusMessage>(this, (r
ecipient, msg) =>
WpfApp6/ViewModel/MainShellVM.cs:87: WeakReferenceMessenger.Default.Register<ScanProgressMessage>(this, (recipient, msg) => {
WpfApp6/ViewModel/XrayImageVM.cs:25: WeakReferenceMessenger.Default.Register<AxisPositionReadyMessage>(this, (r, m) =>
WpfApp6/ViewModel/XrayImageVM.cs:29: WeakReferenceMessenger.Default.Register<StopScanMessage>(this, (r, m) =>
(END)
WpfApp6/ViewModel/AxisEcatVM.cs:53: WeakRefere
nceMessenger.Default.Register<StopScanMessage>(this, (recipient, msg) => Stop());
WpfApp6/ViewModel/MainShellVM.cs:79: WeakReferenceMessenger.Default.Register<LogMessage>(this, (recipient, msg) =>
WpfApp6/ViewModel/MainShellVM.cs:83: WeakReferenceMessenger.Default.Register<BusStatusMessage>(this, (recipient, msg) =>
WpfApp6/ViewModel/MainShellVM.cs:87: WeakReferenceMessenger.Default.Register<ScanProgressMessage>(this, (recipient, msg) => {
WpfApp6/ViewModel/XrayImageVM.cs:25: WeakReferenceMessenger.Default.Register<AxisPositionReadyMessage>(this, (r, m) =>
WpfApp6/ViewModel/XrayImageVM.cs:29: WeakReferenceMessenger.Default.Register<StopScanMessage>(this, (r, m) =>
(END)
WpfApp6/ViewModel/AxisEcatVM.cs:53: WeakRefere
nceMessenger.Default.Register<StopScanMessage>(this, (recipient, msg) => Stop());
WpfApp6/ViewModel/MainShellVM.cs:79: WeakReferenceMessenger.Default.Register<LogMessage>(this, (recipient, msg) =>
WpfApp6/ViewModel/MainShellVM.cs:83: WeakRefer
enceMessenger.Default.Register<BusStatusMessage>(this, (recipient, msg) =>
WpfApp6/ViewModel/MainShellVM.cs:87: WeakReferenceMessenger.Default.Register<ScanProgressMessage>(this, (recipient, msg) => {
WpfApp6/ViewModel/XrayImageVM.cs:25: WeakReferenceMessenger.Default.Register<AxisPositionReadyMessage>(this, (r, m) =>
WpfApp6/ViewModel/XrayImageVM.cs:29: WeakReferenceMessenger.Default.Register<StopScanMessage>(this, (r, m) =>
(END)
WpfApp6/ViewModel/AxisEcatVM.cs:53: WeakRefere
nceMessenger.Default.Register<StopScanMessage>(this, (rec
ipient, msg) => Stop());
WpfApp6/ViewModel/MainShellVM.cs:79: WeakReferenceMessenger.Default.Register<LogMessage>(this, (recipie
nt, msg) =>
WpfApp6/ViewModel/MainShellVM.cs:83: WeakReferenceMessenger.Default.Register<BusStatusMessage>(this, (r
ecipient, msg) =>
WpfApp6/ViewModel/MainShellVM.cs:87: WeakReferenceMessenger.Default.Register<ScanProgressMessage>(this,
(recipient, msg) => {
WpfApp6/ViewModel/XrayImageVM.cs:25: WeakRefer
enceMessenger.Default.Register<AxisPositionReadyMessage>(this, (r, m) =>
WpfApp6/ViewModel/XrayImageVM.cs:29: WeakReferenceMessenger.Default.Register<StopScanMessage>(this, (r, m) =>
WpfApp6/ViewModel/AxisEcatVM.cs:137: try { WeakReferenceMessenger.Default.UnregisterAll(this); }catch {
}
WpfApp6/ViewModel/MainShellVM.cs:117: try { WeakReferenceMessenger.Default.UnregisterAll(this); }catch{ }
WpfApp6/ViewModel/XrayImageVM.cs:107: try { WeakReferenceMessenger.Default.UnregisterAll(this); }catch{ }
WpfApp6/ViewModel/XrayImageVM.cs:109: //WeakRe
ferenceMessenger.Default.UnregisterAll(this);
WpfApp6/ViewModel/AxisEcatVM.cs:47: _ecatMaster = new EtherCATMaster();
WpfApp6/ViewModel/AxisEcatVM.cs:48: _scanAxis= new XYScanAxis(_ecatMaster);
fatal: command line, '_=StartBus\(': Unmatched ( or \(
Archive/Legacy/AxisControlViewModel.cs:64: _cts = new CancellationTokenSource();
WpfApp6/ViewModel/AxisEcatVM.cs:87: _cts =
new CancellationTokenSource();
11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git checkout -b feature/async
Switched to a new branch 'feature/async'
11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/async)
$ git add .
11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/async)
$ git commit -m "所有"需要改为 async Task / 存在 .Wait()/ Dispatcher.Invoke / async void / 未注销 Messenger"的位置"
On branch feature/async
nothing to commit, working tree clean
11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/async)
$ git add .
11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/async)
$ git commit -m "所有"需要改为 async Task / 存在 .Wait()/ Dispatcher.Invoke / async void / 未注销 Messenger"的位置"
[feature/async dacdac2] 所有"需要改为 async Task / 存在 .Wait() / Dispatcher.Invoke / async void / 未注销 Messenger"的位置
5 files changed, 30 insertions(+), 14 deletions(-)
11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/async)
$ git checkout master
Switched to branch 'master'
11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git merge feature/async
Updating 7b1510b..dacdac2
Fast-forward
WpfApp6/EtherCATMaster.cs | 28 ++++++++++++++++++++++------
WpfApp6/Services/LogService.cs | 2 +-
WpfApp6/ViewModel/AxisEcatVM.cs | 4 ++--
WpfApp6/ViewModel/MainShellVM.cs | 4 ++--
WpfApp6/ViewModel/XrayImageVM.cs | 6 +++---
5 files changed, 30 insertions(+), 14 deletions(-)
11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$