.NET Forum / Languages / C# / March 2008
AsyncCallback method accessing WebBrowser control causes exception
|
|
Thread rating:  |
Artie - 05 Mar 2008 09:53 GMT Hi,
We have an app which uses an AsyncCallback method to handle the return from a COM call.
In this callback, if we try to do anything with a WebBrowser control, we get the following exception:
"InvalidCastException was unhandled Specified cast is not valid"
If we remove all code which tries to access the WebBrowser, the exception doesn't occur.
Has anyone got any idea why this would happen? We can access any other controls in our app and no exception is thrown.
Any help much appreciated.
Artie
Marc Gravell - 05 Mar 2008 10:48 GMT First-off; have you used Invoke/BeginInvoke to get back to the UI thread before touching the controls?
Otherwise, I'd guess that you simply have a bug - i.e. you are genuinely doing an invalid cast, perhaps picking up the wrong element from a DHTML query?
Marc
Artie - 05 Mar 2008 11:02 GMT > First-off; have you used Invoke/BeginInvoke to get back to the UI > thread before touching the controls? [quoted text clipped - 4 lines] > > Marc hi Marc,
Yes, we have BeginInvoke to start the process and then EndInvoke in the callback.
After EndInvoke, even doing something as trivial to our WebBrowser control as: webBrowser.Document.Title = "test";
throws the exception.
In this case, what would be causing the invalid cast?
Artie
Marc Gravell - 05 Mar 2008 11:20 GMT Ah - confusion due to the very confusing Delegate.[Begin]Invoke vs Control.[Begin]Invoke; I meant the latter, which pushes work back to the UI thread. Your EndInvoke usage sounds like Delegate.EndInvoke...
What I meant was something like:
void MyCallback(IAsyncResult result) { // (note: this bit runs on a worker thread) // TODO: usual stuff to get the answer from result
// now jump to UI thread before touching controls this.Invoke((MethodInvoker)delegate { // (note: this bit runs on the UI thread) // TODO: things that impact your WebBrowser, e.g. webBrowser1.Left += 5; }); }
Artie - 20 Mar 2008 10:29 GMT > Ah - confusion due to the very confusing Delegate.[Begin]Invoke vs > Control.[Begin]Invoke; I meant the latter, which pushes work back to [quoted text clipped - 15 lines] > }); > } Sorry for late reply Marc, been on holiday.
Anyway, thanks again for your help, this worked in the end, as you suggested:
webBrowser1.Invoke((MethodInvoker)delegate { Focus(); webBrowser1.Document.OpenNew(true); webBrowser1.Document.Write( "stuff" ); });
Willy Denoyette [MVP] - 05 Mar 2008 11:29 GMT >> First-off; have you used Invoke/BeginInvoke to get back to the UI >> thread before touching the controls? [quoted text clipped - 19 lines] > > Artie This is (IMO) not what Marc means with Invoke/BeginInvoke, you are not allowed to "touch" the webrowser control from your callback which runs on a thread pulled from the Thread Pool, you have to marshal the call to the thread owning the "webrowser" control using Control.Invoke/BeginInvoke .
Willy.
Artie - 05 Mar 2008 15:41 GMT On 5 Mar, 11:29, "Willy Denoyette [MVP]" <willy.denoye...@telenet.be> wrote:
> >> First-off; have you used Invoke/BeginInvoke to get back to the UI > >> thread before touching the controls? [quoted text clipped - 26 lines] > > Willy. public void manualMatchCallbackMethod(IAsyncResult ar) { // Retrieve the delegate. manualMatchAsyncMethodCaller caller = (manualMatchAsyncMethodCaller)ar.AsyncState;
Willy Denoyette [MVP] - 05 Mar 2008 17:27 GMT > On 5 Mar, 11:29, "Willy Denoyette [MVP]" <willy.denoye...@telenet.be> > wrote: [quoted text clipped - 36 lines] > manualMatchAsyncMethodCaller caller = > (manualMatchAsyncMethodCaller)ar.AsyncState; I'm not clear on what you are trying to tell us by this piece of code. Your problem is that you are accessing the RCW wrapping a "WebBrowser" COM control from a thread that is not the thread on which the control was created. The InvalidCastException is the result of a failing cross-Thread/Apartment QueryInterface call, all you can do about this is access the control from the creators thread (the UI thread) by calling Control.BeginInvoke or Control.Invoke as suggested by Marc in his last reply, or by something like this.
this.BeginInvoke((MethodInvoker)delegate { webBrowser.Document.Title = "test"; });
Willy.
Artie - 05 Mar 2008 17:01 GMT On 5 Mar, 11:29, "Willy Denoyette [MVP]" <willy.denoye...@telenet.be> wrote:
> >> First-off; have you used Invoke/BeginInvoke to get back to the UI > >> thread before touching the controls? [quoted text clipped - 26 lines] > > Willy. Hi,
Our code currently has the following in the callback:
public void callbackMethod(IAsyncResult ar) { // Retrieve the delegate. asyncMethodCaller caller = (asyncMethodCaller)ar.AsyncState;
val = caller.EndInvoke(ar);
...
// Added this next line after earlier post wbControl.Invoke(new _methodInvoker(wbControl.Focus));
wbControl.Document.Title = "blah"; // this is the line that throws the exception
Is this the right pattern to follow?
Artie
Marc Gravell - 06 Mar 2008 07:47 GMT > wbControl.Invoke(new _methodInvoker(wbControl.Focus)); > wbControl.Document.Title = "blah"; // this is the line that throws > Is this the right pattern to follow? No. In this example, only wbControl.Focus() is executed on the UI thread; the Document.Title still runs on the worker thread. This should be:
wbControl.Invoke((MethodInvoker)delegate { wbControl.Focus(); wbControl.Document.Title = "blah"; });
When you are on the wrong thread (i.e. in an async-callback), the *only* things you are really allowed to do is to query InvokeRequired, or execute Invoke or BeginInvoke. This is due to thread affinity.
Marc
Peter Duniho - 06 Mar 2008 07:57 GMT > [...] > When you are on the wrong thread (i.e. in an async-callback), the *only* > things you are really allowed to do is to query InvokeRequired, or > execute > Invoke or BeginInvoke. This is due to thread affinity. Is _that_ what it is? I've yet to see a clear document from Microsoft explaining why they require the use of Invoke() or BeginInvoke() (the native Win32 API has a similar requirement, but it's handle automatically by the OS). Is it really because the GUI thread has been assigned a specific CPU (which is what "thread affinity" means), and executing GUI class code on a different CPU would mess things up?
If that's true, where did you find that out, and why have you been keeping a secret all this time!? :)
Pete
Marc Gravell - 06 Mar 2008 08:05 GMT > assigned a specific CPU (which is what "thread affinity" means) Surely that would be processor affinity
I don't think there's any secret that most UI implementations exhibit thread affinity? Please correct me if I'm using the wrong term [but I'm not sure that I am...]
Marc
Peter Duniho - 06 Mar 2008 08:24 GMT >> assigned a specific CPU (which is what "thread affinity" means) > Surely that would be processor affinity Hmmm...well, I've always used the terms interchangeably. But I've been known to be sloppy in my use of nomenclature.
> I don't think there's any secret that most UI implementations exhibit > thread > affinity? Please correct me if I'm using the wrong term [but I'm not sure > that I am...] So if I understand your use of the term, all you're saying is that there's some affinity of the GUI data structures to the thread on which they are created?
Okay, well...if that's the case, I guess that doesn't actually provide more insight after all. I mean, that's basically a general description of the need to use Invoke(), but it doesn't explain why that affinity exists and, most importantly, why the application code needs to care.
After all, the same affinity exists in the unmanaged API, but Windows handles it automatically for you in at least some cases (anything that goes through the window proc is either just posted to the message queue and pumped in the correct thread, or is sent via SendMessage() which does the necessary thread marshaling if required).
What I've always been curious about is why .NET doesn't just always wrap this up and hide it from you. It knows what's allowed across threads and what's not (after all, it throws an exception when you do it wrong). It seems like it could have been written to just automatically marshal across the threads as necessary.
It's that latter question I thought your comment was answering, but I guess it wasn't. I simply misunderstood what you wrote (my fault).
Oh well...sorry for the fire drill. :) Nothing to see here. Please disperse.
Pete
Marc Gravell - 06 Mar 2008 09:11 GMT I don't pretend that it is an answer to "why", but perhaps one good thing about this approach is that it avoids accidental and overly inefficient code - i.e.
x.Foo = 123; // ... lots more, perhaps involving a loop x.Bar = "abc";
If each of those lines (executed on a worker thread) silently did an Invoke() behind your back, you could get some *seriously* poor performance. However, wrap that up a bit (for not much extra work) and it should perform OK:
x.Invoke((MethodInvoker) { x.Foo = 123; // ... lots more, perhaps involving a loop x.Bar = "abc"; });
I suspect that this is more by accident than by intent, however...
Marc
Willy Denoyette [MVP] - 06 Mar 2008 09:52 GMT Pete, see inline. Willy.
>>> assigned a specific CPU (which is what "thread affinity" means) >> Surely that would be processor affinity [quoted text clipped - 10 lines] > some affinity of the GUI data structures to the thread on which they are > created? Most "User"object handles (but not all) have thread affinity, "Window" handles (HWND's) have thread affinity, that means that they have to be accessed by the thread that owns the handle (the creator of the Window handle). Windows itself does not enforce this rule, accessing HWND's (all user interface elements are HWND based) from other thread's than the creator, may yield unexpected results.
> Okay, well...if that's the case, I guess that doesn't actually provide > more insight after all. I mean, that's basically a general description of [quoted text clipped - 6 lines] > and pumped in the correct thread, or is sent via SendMessage() which does > the necessary thread marshaling if required). Not true, Windows does not handle this automatically, when a thread other than the UI thread (the one pumping message and dispatching messages to it's WndProc) calls a function "directly", that is, without posting a message to the owning thread, you "may" have violated the affinity rule.
> What I've always been curious about is why .NET doesn't just always wrap > this up and hide it from you. It knows what's allowed across threads and > what's not (after all, it throws an exception when you do it wrong). It does not throw an exception in release builds, only debug builds have code injected (a 'costly' feature )that check whether the caller thread is the same a the owning HWND's thread. Note, that accessing the UI elements (the HWND's) for reading (say reading a property) is not really a 'violation' of the HWND's access rule, the "debug injected" code however treats all accesses, read and write, from another thread as "illegal" when not marshaled.
> seems like it could have been written to just automatically marshal across > the threads as necessary. Note that not all UI accesses are thread affinitized, some UI elements may be accessed cross thread without the need to marshal the call. Also, .NET doesn't have an idea whether you want to access the UI synchronously or asynchronously, so it cannot know what marshaling code it must inject.
> It's that latter question I thought your comment was answering, but I > guess it wasn't. I simply misunderstood what you wrote (my fault). [quoted text clipped - 3 lines] > > Pete Peter Duniho - 06 Mar 2008 17:19 GMT > Most "User"object handles (but not all) have thread affinity, "Window" > handles (HWND's) have thread affinity, that means that they have to be > accessed by the thread that owns the handle (the creator of the Window > handle). Windows itself does not enforce this rule, accessing HWND's > (all user interface elements are HWND based) from other thread's than > the creator, may yield unexpected results. I assume you mean for calls to functions list SetWindowLong(), etc.? The docs are clear that PostMessage() and SendMessage() both take care of threading issues (those were the examples I was talking about).
> Not true, Windows does not handle this automatically, when a thread > other than the UI thread (the one pumping message and dispatching > messages to it's WndProc) calls a function "directly", that is, without > posting a message to the owning thread, you "may" have violated the > affinity rule. I'm not entirely sure what you mean here. If I write code that calls my window proc literally directly, then I'm not necessarily using the HWND at all. Granted, it depends on what the window proc does for the given message ID and parameters.
Are you saying that for internal features of a window class, the window proc might be using thread-local storage or some other feature that causes the data to be tied to a specific thread?
Of course, I've always thought that calling SendMessage() was the only approved way of calling the window proc, but you're right...I guess there's nothing to stop you from calling it directly.
>> What I've always been curious about is why .NET doesn't just always >> wrap this up and hide it from you. It knows what's allowed across [quoted text clipped - 4 lines] > code injected (a 'costly' feature )that check whether the caller thread > is the same a the owning HWND's thread. [...] I'm not sure what you mean here. I get the exception whether I've compiled a debug or release build.
It's true that the exception only appears to be thrown if the debugger is present. Did you mean that only when the debugger is injected does the check get "injected"? If so, could you provide more details on that? I wasn't aware that the code being executed was different depending on the presence of the debugger (other than the "vshost" stuff, of course). Does Microsoft actually document this behavior? Or is this something you just "have to know"?
> Note that not all UI accesses are thread affinitized, some UI elements > may be accessed cross thread without the need to marshal the call. Also, > .NET doesn't have an idea whether you want to access the UI > synchronously or asynchronously, so it cannot know what marshaling code > it must inject. Well, I would expect it to assume synchronous, since that's what the code would imply if written without any explicit marshaling.
Anyway, thanks for the comments.
Pete
Willy Denoyette [MVP] - 06 Mar 2008 18:29 GMT >> Most "User"object handles (but not all) have thread affinity, "Window" >> handles (HWND's) have thread affinity, that means that they have to be [quoted text clipped - 6 lines] > docs are clear that PostMessage() and SendMessage() both take care of > threading issues (those were the examples I was talking about). No, I'm not talking about these. I'm talking about accessing UI elements that are backed by HWND from threads other than the thread that created the HWND (HWND handle = CreateWindow(..)). That means that "handle" should only be accessed from the thread that runs the message loop associated to the HWND, or more precisely from the WndProc that was registered when the Window class was registered. Note that there is no real *danger* in accessing HWND's from non-owning threads, the system/application will not die if you do, however, the application may not behave as expected and you may leak handles when you do. For instance a DestroyWindow(handle) call from another thread than the creator, will not close the handle, so here you leak, and finally your process may die because you have exhausted the available "user" handles.
>> Not true, Windows does not handle this automatically, when a thread >> other than the UI thread (the one pumping message and dispatching [quoted text clipped - 6 lines] > all. Granted, it depends on what the window proc does for the given > message ID and parameters. Not sure if you are talking about C# and .NET here, anyway, you don't call WndProc procedures directly from other threads than the thread that runs the message loop, don't you? Here I mean that if you have something like the following then you are in violation with the HWND affinity rules:
// C# code ahead // WndProc associated with the UI thread protected override void WndProc(ref Message m) { if(m.Msg == 0x1234) { label.Text = "Oh no, this is illegal"; // label is a static control backed by a HWND } base.WndProc(ref m); }
... // Other thread ... someForm.WndProc(ref m);
Here, the 'label' control is backed by an HWND and it's 'Text' property is accessed using it's HWND, HWND's have thread affinity and therefore should only be accessed from the same thread that owns the handle. So, here we are violating the "affinity rule" when setting the Text property.
Note that the same as above is invalid when the other thread directly calls "label.Text = ....";
> Are you saying that for internal features of a window class, the window > proc might be using thread-local storage or some other feature that causes [quoted text clipped - 19 lines] > present. Did you mean that only when the debugger is injected does the > check get "injected"? If so, could you provide more details on that? That's exactly what I meant, you don't need the "debug" build, whenever you run under the (VS) debugger, you are running in a context where the CLR injects some MDA's (Managed Debugging Assistant). Please search MSDN for details on all possible MDA's.
> I wasn't aware that the code being executed was different depending on the > presence of the debugger (other than the "vshost" stuff, of course). Does > Microsoft actually document this behavior? Or is this something you just > "have to know"? I'm not aware of such document, all I know is that the CLR knows that a managed debugger is attached, I know that V2 of the CLR has a number of MDA's (run-time probes) that may be active by default while others are activated when running under the debugger. I know that the message loop and WndProc are slightly modified when running under the debugger, for instance to "handle" "unhandled "exceptions (no pun intended :-)) I also know that all debuggers (whatever managed/unmanaged) may interfere with and change the behavior of a program when running under a debugger...
>> Note that not all UI accesses are thread affinitized, some UI elements >> may be accessed cross thread without the need to marshal the call. Also, [quoted text clipped - 4 lines] > Well, I would expect it to assume synchronous, since that's what the code > would imply if written without any explicit marshaling. And if you want an asynchronous dispatch? Anyway, inject cross-thread handling code is way to expensive, performance wise, to be done for each separate UI element you are touching. You would not write something similar like this I suppose:
Invoke((MethodInvoker) { label1.Text = "SomeText"; } Invoke((MethodInvoker) { label2.Text = ....; } Invoke((MethodInvoker) { label3.Text = ....; }
instead you will group all UI update stuff in a single code block that you will marshal to the UI thread.
>Anyway, thanks for the comments. You're welcome ;-)
Willy.
Willy Denoyette [MVP] - 07 Mar 2008 10:07 GMT >>> Most "User"object handles (but not all) have thread affinity, "Window" >>> handles (HWND's) have thread affinity, that means that they have to be [quoted text clipped - 6 lines] >> docs are clear that PostMessage() and SendMessage() both take care of >> threading issues (those were the examples I was talking about). Oh, now I see that you meant to say "calls to functions *like* SetWindowLong" instead of "calls to functions *list* SetWindowLong", so, I guess that below answer may be quite confusing. I'm indeed talking about the "Windows Functions" like SetWindowsText, GetWindowText, SetWindowLong, SetWindowPosition..., these are the API's wrapped by the control classes. So, a property setter like: "label.Text ="blabla", boils down to a 'SetWindowText(handleOfLabel, "blabla");' function call, where "handleOfLabel" is the HWND of "label". So, this function should only be called from the thread that owns this HWND, which is the thread that called CreateWindow(...) to create the label.
> No, I'm not talking about these. I'm talking about accessing UI elements > that are backed by HWND from threads other than the thread that created [quoted text clipped - 8 lines] > you leak, and finally your process may die because you have exhausted the > available "user" handles. Willy.
Peter Duniho - 07 Mar 2008 18:01 GMT >>> I assume you mean for calls to functions list SetWindowLong(), etc.? >>> The docs are clear that PostMessage() and SendMessage() both take care >>> of threading issues (those were the examples I was talking about). > > Oh, now I see that you meant to say "calls to functions *like* > SetWindowLong" instead of "calls to functions *list* SetWindowLong" Yes, sorry. I didn't even notice my typo when reading your original reply. While a bit of a tangent, the other information was informative anyway, so no big deal. :)
Anyway, I guess the bottom line here is that a) the unmanaged API doesn't actually handle cross-thread access except in a very specific case (SendMessage()), and b) in either case (managed and unmanaged) there's a potential and significant performance risk for handling cross-thread access transparently.
I think those are both strong enough reasons to justify this limitation existing in .NET, rather than it hiding the underlying architecture from you.
Thanks, Pete
Willy Denoyette [MVP] - 06 Mar 2008 09:23 GMT >> assigned a specific CPU (which is what "thread affinity" means) > Surely that would be processor affinity [quoted text clipped - 4 lines] > > Marc Note that the OP's issue is not related to a cross-thread UI access, the "InvalidCastException" is the result of a failing "QueryInterface" call (which really is an implicit cast). The reason for this is that the RCW (wrapping the Webbrowser AX object) is accessed from a thread (a thread pool thread) other than the RCW "creator's" thread, whatever that thread might be as long as it's an STA thread. Now, as both threads run in different COM apartments, the QI call must be marshaled from the TP's MTA thread to the object's STA thread, the CLR needs some marshaling support in the form of a typelib, which is not available for this wrapped Webbrowser AX control, hence the failing QI which finally results in an "InvalidCastException " thrown in .NET.
Willy.
Free MagazinesGet these publications absolutely FREE for up to 12 months. There are no hidden fees and no obligation. Simply choose a title, complete the application form and submit it. Read more ...
|
|
|