6.5 注入

依赖注入 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个核心好处

  1. 硬件唯一实例:全项目共用同一个EtherCAT主站,不会重复初始化网卡、总线不冲突
  2. 方便单元测试 :测试时替换仿真实现MockXYScan,不用启动真实硬件
  3. 统一生命周期管理:容器统一释放资源,减少Dispose遗漏泄漏
  4. 代码统一规范:所有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 线程 只做「触发」
非事件处理器 主动(代码里手动调用) 可能在 后台线程(轴控制/视觉检测) 写「核心业务/硬件逻辑」

  1. 事件处理器(UI线程)→ 直接改UI ✅
  2. 非事件处理器 (后台线程:EtherCAT/轴/扫描)→ 不能直接改UI
    → 必须用 Dispatcher 调度到UI线程

示例

csharp 复制代码
// 非事件处理器:后台线程执行轴运动
private void UpdateAxisStatus() // 非事件处理器
{
    // 后台线程不能直接改UI,用Dispatcher
    Application.Current.Dispatcher.Invoke(() => 
    {
        AxisPosX = _scanAxis.PosX; // UI更新
    });
}

  1. 事件处理器:等着被触发(按钮点、消息来)
  2. 非事件处理器:主动跑业务(初始化、动轴、扫描、算数据)
  3. 你在 Dispatcher 分支做的优化:
    让「非事件处理器」能安全更新UI,这是 WPF 工控上位机的标准规范!

一、现有代码两处隐患(工控致命问题)

InitMaster() 属于非事件处理器(总线初始化业务方法)

csharp 复制代码
public void InitMaster()
{
    //_=StartBus(); 点火即弃写法
    _ = StartBus();
}
  1. _ = StartBus() 隐患:吞异常

    StartBus() 返回Task,用丢弃赋值_=属于fire-and-forget,总线启动内部报错(网卡断开、从站缺失、DC同步失败)不会抛出、不会被捕获、无任何日志 ,EtherCAT静默启动失败,上位机无提示、设备失控。

    .NET规则:未被await/未绑定异常回调的异常Task,后期会随机触发程序进程崩溃。

  2. 被注释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());
}


  1. _ = Task = 抛弃异常,工控禁用在硬件初始化代码;
  2. 总线初始化优先async Task + await,异常全可控;
  3. 后台任务改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)
$
相关推荐
小满Autumn1 天前
MVVM Light 架构笔记:定位器、命令、消息与 IoC 实践
笔记·学习·架构·c#·上位机·mvvm
小满Autumn1 天前
CommunityToolkit.Mvvm 架构笔记:现代 MVVM、源生成器与工程化实践
笔记·架构·c#·.net·wpf·mvvm
杊页2 天前
系列二:MVVM 深度实战与项目重构 | 第6篇 DataBinding & ViewBinding 实战落地:告别 findViewById 的“刀耕火种”
架构·mvvm
小满Autumn3 天前
依赖注入设计模式速查手册
开发语言·c#·wpf·mvvm·依赖注入
小满Autumn3 天前
WPF 依赖属性速查手册
笔记·c#·wpf·上位机·mvvm
czhc11400756635 天前
6.1EtherCAT工业架构:软主站,分布式时钟DC,PDO实时通信
视觉·运控
czhc11400756636 天前
531 扫描模式
mvvm·运控
czhc11400756637 天前
529: XYScanAxis()类
mvvm·视觉·运控
czhc11400756639 天前
528:Halcon图像控件 启动轴状态实时监控
mvvm·视觉·运控