What is the correct/best method for threading in a COM exposed assembly?
First let me say: I am still very inexperienced with VB.NET so there is probably something simple I am missing, or at least that's what I'm hoping.
I've been writing a COM exposed wrapper class to provide access to a web service in a legacy VB6 application. I got everything pretty much working as I wanted – all the COM properties, methods and events are showing up in the VB6 app – but for one minor detail: when I used a method which made a call to the ser开发者_高级运维vice, it was obviously only working asynchronously, so I couldn't update a progress bar or do anything else while waiting for a corresponding assembly event to fire.
So I concluded that I needed to use a separate thread to access the web service. I decided the cleanest method was to do this via a separate class, which raised events in the main wrapper class, which in turn raised the public COM events. Here is how I implemented it:
At the top of the wrapper class I put:
Private WithEvents oService As Service_Class
Private ServiceThread As Thread
Then, in one of the main wrapper methods (say a simple Status Request), I put the following:
oService = New Service_Class
ServiceThread = New Thread(AddressOf oService.RequestStatus)
ServiceThread.Start()
Once the service call has acquired the 'Status' value it raises the StatusReport event in the main wrapper, which in turn raises the COM exposed StatusReport event, thus:
Private Sub oService_StatusReport(ByVal Status As String) Handles oService.StatusReport
RaiseEvent StatusReport(Status)
This works, up to a point – it certainly doesn't tie up processing in the calling app any more – but there does still seem to be a problem: the calling app crashes if I try to access any controls within the wrapper object's events. I've tested this with a .NET form and it doesn't crash but it raises a "Cross-thread operation" error. Of course I could use a timer in the calling app, monitoring a variable or something, to get around this problem, but that seems awfully messy and made me think that I'm probably doing something fundamentally wrong in the way I'm implementing the threading. Can someone tell me what I am doing wrong and how to fix it, please?
VB6 completely does not support threading. It sorta worked in VB5 if you programmed very carefully but that got borked in VB6 runtime. It is okay to use threading in your .NET code, as long as the VB6 code never sees that. You have to marshal the event raising call to the STA thread on which VB6 created your class object.
That's done with Control.BeginInvoke() or Dispatcher.BeginInvoke(). If you don't have a form or dispatcher to make this call then create a hidden Winforms form so you can use it to marshal. Getting this right isn't terribly easy.
Yes. This is common issue in Windows Forms applications, and for that matter in native Windows applications (e.g. those written in C/C++ against the native Windows API). You have different pieces here, but it's the same problem.
Let me give you some background:
First, you need to know that .NET Events are nothing but a convenient wrap around delegates, intended to make it easy to define methods in your class which "others will implement, and your class will call". Note the difference between that and 'normal' methods, which are those "your class implements and others will call". The key observation is that Events don't know anything about threading.
If you think about it, you will realize that that is also true of VB6 events.
The second thing you need to know is that Windows controls, which is what VB6 uses, as well as controls in .NET's Windows.Forms world (and WPF for that matter) are bound by what's called thread affinity: they can ONLY be accessed from the thread that created them.
Note that Forms (both Windows/VB6 and .NET Forms) are controls themselves (albeit very complicated ones) so this is all true for Forms.
Put the two facts together... and you will immediately see what's going on. When your working thread code raises the event, it's just calling the event listener method directly, and it does so from the wrong thread. The moment the event listener method in VB6 (running from the working thread instead of the VB6 main thread) touches any Windows control in your VB6 form, "puff".
In the .NET Windows.Forms world, Microsoft did all the really hard work for you.
Every Control object (including the Form object) comes with a method called Invoke()
. If a working thread has to communicate something to a form/control, it does not manipulate the controls directly. Instead, the form that hosts the control must implement an appropriate delegate, and the working thread calls the form's Invoke()
method with that delegate as an argument and possibly some data parameters. Control.Invoke()
then takes care of all the threading messiness and makes sure the delegate is called from the thread that created the form and its controls. Finally, the delegate method implemented by the form takes care of manipulating the form's controls on behalf of the working thread.
Calling the delegate method directly from a working thread instead of using the Control.Invoke()
method is a sure-fire way of crashing the application.
Now, I confess: my immediate reaction when I read your question was "Yeah, I know exactly what's going on; I'll just explain..." and then I realized that I only had a faint idea of how the .NET framework actually handles this internally. And then I thought some more, and realized that this was not a simple task at all.
No problem! I just loaded System.Windows.Forms.DLL
in Reflector to quickly check out the implementation of Control.Invoke()
... and felt all the blood drained out of my face. There is some serious ninjitzu going on in there. Go ahead and take a look for yourself.
When you are done, you will understand the why of Hans's suggestion -- and that is my suggestion as well:
Create a hidden .NET Form. Make sure you new
the form from the thread used by VB6. Use the methodology I described above and have the working thread call a delegate in the hidden form via Invoke()
. Then the delegate can raise your Event. Everyhing will then work correctly.
I know. It sounds crazy overkill to create a hidden window just to change the thread used to call a function; but it's not. You should know that that's exactly how COM handles communications between VB6-style COM objects (called STA objects) and multi-threaded COM objects.
Whether or not the extra benefit is worth the hassle, only you can decide.
Good Luck.
References:
- The MSDN topic for
Control.Invoke()
has an example in VB.NET that can be quickly distilled down to what you need. Add a comment to this response if you have difficulties parsing the example and I'll try to help.
精彩评论