c# - 启用 ActiveX 控件以在不在 System.Windows.Forms.Application 中运行的情况下引发事件

标签 c# winforms interop activex

我们的团队正在编写一些代码,要求我们与联网设备进行交互。该设备使用专有协议(protocol),制造商以OCX控件(即ActiveX控件)的形式为我们提供了接口(interface)库。

我在尝试使用 ActiveX 控件时遇到过几次错误的开始,例如使用包含在 C++/CLI 中的 native C++ (MFC),包含在 C# 中,我了解到我可以将控件拖放到 winforms 窗体中,还有一些包装器代码将自动生成。因此,我将控件放在一个空的表单中,并将其方法和事件存入表单中,目的是让此表单成为控件的代理/包装类。

我现在遇到的问题是设备通过每隔几秒发回一个数据包来报告其状态。这应该会导致 ActiveX 控件引发事件,但只有当表单在 Application 中运行时才会引发这些事件:

Application.Run(new Form());

为了在控制台应用程序或单元测试中使用表单类,我尝试过这样的事情:

Var proxy = new Form();
Task.Run(() => { Application.Run(proxy); };
proxy.SomeMethod(); 

但这会引发异常:跨线程操作无效:从创建它的线程以外的线程访问控件“Form1”

由于代理类最终将在 Windows 服务中运行,因此这是一个交易破坏者。如何启用 ActiveX 控件引发事件而不在应用程序内部托管其窗体?

最佳答案

该片段提供的指导很少,但它肯定在不止一个方面是错误的。它死得太早了,无法解决真正的问题。 “跨线程操作无效”异常是尝试在工作线程拥有的对象上调用 SomeMethod() 方法的简单结果。您必须使用 Begin/Invoke() 方法来避免这种情况发生。在尝试使用它之前,您还必须确保工作线程正在运行并且控件已正确初始化。

更大的问题是线程不适合支持 ActiveX 控件。该线程必须标记为 STA(单线程单元),这是您做出的 promise ,即您将为非线程安全的代码提供一个好客的家。与任何 ActiveX 控件一样。实现 STA 契约(Contract)需要泵送消息循环 (Application.Run) 并且从不阻塞线程。 Task 不会创建这样的线程,线程池线程不能被标记为 STA。你需要一个 custom TaskScheduler或者只是一个普通的线程,所以你可以调用它的 SetApartmentState() method .

一些示例代码可以帮助您开始:

using System;
using System.Threading;
using System.Windows.Forms;

class ActiveXHost : Form {
    public ActiveXHost(Control control, bool hidden = false) {
        if (control.IsHandleCreated) throw new InvalidOperationException("Control already committed to wrong thread");
        if (hidden) this.Opacity = 0;
        this.ShowInTaskbar = false;

        using (initDone = new ManualResetEvent(false)) {
            thread = new Thread((_) => {
                this.Controls.Add(control);
                Application.Run(this);
            });
            thread.IsBackground = true;
            thread.SetApartmentState(ApartmentState.STA);
            thread.Start();
            initDone.WaitOne();
        }
    }
    public void Execute(Action action) {
        this.BeginInvoke(action);
    }
    public TResult Execute<TResult>(Func<TResult> action) {
        return (TResult)this.Invoke(action);
    }

    protected override void OnLoad(EventArgs e) {
        base.OnLoad(e);
        initDone.Set();
    }
    protected override void Dispose(bool disposing) {
        if (disposing && thread != null) {
           this.Invoke(new Action(() => {
               base.Dispose(disposing);
               Application.ExitThread();
               thread = null;
           }));
        }
    }

    private Thread thread;
    private ManualResetEvent initDone;
}

构造函数负责创建合适的 STA 线程并负责与该线程的互锁,确保在线程启动并运行并且 ActiveX 控件准备好开始生成事件之前它不会完成。如果您收到 InvalidOperationException,那么您初始化控件的方式有问题,请通过订阅控件的 HandleCreated 事件来诊断。

我添加了 Execute() 方法,让您有机会正确调用 SomeMethod()。

使用 Dispose() 方法销毁控件并终止线程。

对于服务,您通常会像这样使用它:

ActiveXHost host;

protected override void OnStart(string[] args) {
    var ctl = SomeAxHostWrapper();
    host = new ActiveXHost(ctl);
    ctl.HasMessage += MessageReceived;
}

protected override void OnStop() {
    host.Dispose();
    host = null;
}

请记住,服务并不是 ActiveX 控件的理想环境。它们在 session 0 中运行,该 session 具有较小的桌面堆。这可能会导致您的服务失败并出现难以理解的 0xC0000142 异常。背景 is here

关于c# - 启用 ActiveX 控件以在不在 System.Windows.Forms.Application 中运行的情况下引发事件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36853716/

相关文章:

c#如何改变托盘图标

c# - 在对鼠标点击或按钮按下使用react时,是否可以使用代码模板/设计模式?

c# - PInvoke 创建桌面

c# - NHibernate.映射异常 : No persister for: XYZ

c# - 使用 haarcascade_profileface.xml 时出错

C# 7.0 模式匹配与新的输出参数混合

c# - WinForms:在其他应用程序的主窗口上显示表单模式

c# - Windows 窗体上的 log4net 不写入日志文件

java - 在 clojure 中调用需要类作为参数的 java 方法

c# - 如果/当 DllImport 被多次调用时会发生什么?