问题
Our team is writing some code that requires us to interface with a networked device. The device uses a proprietary protocol, and the manufacturer has provided us with an interface library in the form of an OCX control (i.e., an ActiveX control).
I had several false starts trying to use the ActiveX control, for example using native C++ (MFC) wrapped in C++/CLI, wrapped in see C#, I learned that I could drag and drop the control into winforms form, and some wrapper code would be auto-generated. So I put the control in an otherwise empty form and stubbed its methods and events out on the form, with the intention that this form will be the proxy/wrapper class for the control.
The problem I'm having now is that the device reports its status by sending a packet back every few seconds. This is supposed to cause the ActiveX control to raise an event, but these events only get raised if the form is running inside an Application
:
Application.Run(new Form());
For the purpose of using the form class in a console app or unit test, I’ve tried something like this:
Var proxy = new Form();
Task.Run(() => { Application.Run(proxy); };
proxy.SomeMethod();
But this throws an exception: Cross-thread operation not valid: Control 'Form1' accessed from a thread other than the thread it was created on
Since the proxy class will eventually be running in a Windows service this is a deal breaker. how do I enable the ActiveX control to raise events without hosting its form inside of an application?
回答1:
The snippet gives very little guidance but it is certainly wrong in more than one way. It died too early to get to the real problem. The "Cross-thread operation not valid" exception is a simple consequence of trying to call SomeMethod() method on an object that is owned by a worker thread. You must use the Begin/Invoke() method to avoid this from happening. You also must ensure that the worker thread is running and the control properly initialized before you try to use it.
The much bigger issue is that the thread is not suitable to support an ActiveX control. The thread must be marked as STA (single threaded apartment), a promise you make that you'll provide a hospitable home to code that is not thread-safe. Like any ActiveX control. Implementing the STA contract requires pumping a message loop (Application.Run) and never blocking the thread. A Task
does not create such a thread, threadpool threads cannot be marked STA. You'll need a custom TaskScheduler or just a plain Thread so you can call its SetApartmentState() method.
Some sample code to help you get going:
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;
}
The constructor takes care of creating a suitable STA thread and takes care of interlocking with that thread, ensuring that it will not complete until the thread is up and running and the ActiveX control is ready to start generating events. If you get the InvalidOperationException then there is something amiss with the way you initialized the control, diagnose that by subscribing the control's HandleCreated event.
I added the Execute() methods to give you a shot at calling SomeMethod() correctly.
Use the Dispose() method to get the control destroyed and the thread terminated.
From a service, you'd typically use it something like this:
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;
}
Do keep in mind that a service is not exactly a hospitable environment for ActiveX controls. They run in session 0, a session that has a small desktop heap. This may fail your service with an inscrutable 0xC0000142 exception. Backgrounder is here
来源:https://stackoverflow.com/questions/36853716/enable-activex-control-to-raise-events-without-running-in-a-system-windows-forms