在 System.Windows.Forms.Application 中启用 ActiveX 控件以在没有 运行 的情况下引发事件

Enable ActiveX control to raise events without running in a System.Windows.Forms.Application

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

我在尝试使用 ActiveX 控件时遇到过几次错误的开始,例如使用 C++/CLI 包装的本机 C++ (MFC),包装在 C# 中,我了解到我可以将控件拖放到 winforms 窗体中,并且会自动生成一些包装器代码。所以我把控件放在一个空的表单中,并将它的方法和事件存入表单,目的是让这个表单成为控件的 proxy/wrapper class。

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

中的 运行 时才会引发这些事件
Application.Run(new Form());

为了在控制台应用程序或单元测试中使用表单 class,我试过这样的方法:

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

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

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

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

更大的问题是线程不适合支持 ActiveX 控件。该线程必须标记为 STA(单线程单元),这是您做出的承诺,即您将为非线程安全的代码提供一个好客的家。与任何 ActiveX 控件一样。实施 STA 合约需要泵送消息循环 (Application.Run) 并且从不阻塞线程。 A Task 不会创建这样的线程,threadpool 线程不能被标记为 STA。你需要 custom TaskScheduler or just a plain Thread so you can call its 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 线程并负责与该线程的互锁,确保在线程启动和 运行ning 以及 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 控件的理想环境。他们 运行 在会话 0 中,该会话具有较小的桌面堆。这可能会导致您的服务失败并出现难以理解的 0xC0000142 异常。背景资料 is here