I am still encountering a background thread in the WinForm user interface. What for? Here are some of the problems:
- Obviously the most important problem, I cannot change the control if I do not execute the same thread that created it.
- As you know, Invoke, BeginInvoke, etc. unavailable until a control is created.
- Even after RequiresInvoke returns true, BeginInvoke can still drop ObjectDisposed, and even if it does not throw, it can never execute code if the control is destroyed.
- Even after RequiresInvoke returns true, Invoke can hang indefinitely pending execution by using a control that was removed at the same time as Invoke was called.
I am looking for an elegant solution to this problem, but before I find out what I am looking for, I thought that I would clarify the problem. This should take a general problem and set a more concrete example for it. In this example, let's say that we transfer large amounts of data over the Internet. The user interface should be able to display a progress dialog for an already started transfer. The progress dialog should be updated constantly and quickly (updated from 5 to 20 times per second). The user can reject the progress dialog at any time and call it again if necessary. And then, let's pretend to be the argument that if the dialogue is visible, it should handle every progress event. The user can click "Cancel" in the execution dialog box and by changing the event arguments, cancel the operation.
Now I need a solution that will correspond to the following restriction field:
- Allow the workflow to call the method in the control / form and lock / wait for the execution to complete.
- Allow the dialog itself to call the same method on initialization or the like (and therefore not use invoke).
- Do not set the burden on the implementation of the processing method or the triggering event, the solution should only change the event subscription itself.
- Accordingly, process blocking calls to a dialogue, which may be in the process of disposal. Unfortunately, this is not as simple as checking for IsDisposed.
- It should be possible to use any type of event (suppose a delegate of type EventHandler)
- Do not throw exceptions into a TargetInvocationException.
- The solution should work with .Net 2.0 and higher
So, can this be solved taking into account the limitations above? I searched and dug countless blogs and discussions, and, alas, I'm still empty-handed.
Update: I understand that this question does not have a simple answer. I was on this site for only a couple of days, and I saw some people with great experience answering questions. I hope that one of these people has solved it enough enough so that I do not spend a week or so to make a reasonable decision.
Update No. 2: Well, I will try to describe the problem in more detail and see what (if anything) shakes out. The following properties, which allow us to determine his condition, have a couple of things that cause concern ...
Control.InvokeRequired = Documented to return false if executed in the current thread, or if IsHandleCreated returns false for all parents. I am confused by the implementation of InvokeRequired, which has the ability to either throw an ObjectDisposedException, or potentially even recreate an object handle. And since InvokeRequired can return true when we cannot call (Dispose in progress), and it can return false, even if we may need to use invoke (Create in progress), this simply cannot be trusted in all cases. The only time I can see where we can trust InvokeRequired return false is when IsHandleCreated returns true both before and after the call (BTW, MSDN documents for InvokeRequired indicate a check for IsHandleCreated).
Control.IsHandleCreated = Returns true if a handle has been assigned to the control; otherwise false. Although IsHandleCreated is a safe call, it can break if the control is in the process of recreating its handle. This potential problem, apparently, can be solved by performing blocking (control) when accessing IsHandleCreated and InvokeRequired.
Control.Disposing = Returns true if the control is in the process of disposal.
- Control.IsDisposed = Returns true if the control is deleted. I am considering subscribing to the Disposed event and checking the IsDisposed property to determine if BeginInvoke will ever end. The big problem here is the lack of synchronization lock, which prevents the transfer of Disposing โ Disposed. Perhaps if you are subscribed to the Disposed event and after that make sure that Disposing == false && IsDisposed == false you can still never see the fire of the Disposed event. This is because the implementation of Dispose sets Disposing = false, and then sets Disposed = true. This gives you the opportunity (albeit a small one) to read both Disposing and IsDisposed as false on the remote control.
... my head hurts :( I hope that the above information sheds a little more light on the problems for those who have these problems. I appreciate your spare thought cycles on this.
Closing the problem ... Below is half of the Control.DestroyHandle () method:
if (!this.RecreatingHandle && (this.threadCallbackList != null)) { lock (this.threadCallbackList) { Exception exception = new ObjectDisposedException(base.GetType().Name); while (this.threadCallbackList.Count > 0) { ThreadMethodEntry entry = (ThreadMethodEntry) this.threadCallbackList.Dequeue(); entry.exception = exception; entry.Complete(); } } } if ((0x40 & ((int) ((long) UnsafeNativeMethods.GetWindowLong(new HandleRef(this.window, this.InternalHandle), -20)))) != 0) { UnsafeNativeMethods.DefMDIChildProc(this.InternalHandle, 0x10, IntPtr.Zero, IntPtr.Zero); } else { this.window.DestroyHandle(); }
You will notice that an ObjectDisposedException is thrown in all calls with cross-threads. Shortly after this, this.window.DestroyHandle () is called, which in turn destroys the window and sets its IntPtr.Zero reference handle, thereby preventing further calls in the BeginInvoke method (more precisely, MarshaledInvoke, which process both BeginInvoke and Invoke). The problem here is that after the lock is freed from threadCallbackList, a new record can be inserted before the control thread closes the window handle. It seems like I see, though infrequently, often enough to stop the release.
Update # 4:
Sorry to keep dragging it; however, I thought it was worth documenting here. I managed to solve most of the problems above, and I'm narrowing down the solution that works. I hit another issue that I was worried about, but still hadn't seen in-the-wild.
This problem is due to the genius who wrote the Control.Handle property:
public IntPtr get_Handle() { if ((checkForIllegalCrossThreadCalls && !inCrossThreadSafeCall) && this.InvokeRequired) { throw new InvalidOperationException(SR.GetString("IllegalCrossThreadCall", new object[] { this.Name })); } if (!this.IsHandleCreated) { this.CreateHandle(); } return this.HandleInternal; }
This in itself is not so bad (regardless of my opinion on getting {} modifications); however, in combination with the InvokeRequired property or the Invoke / BeginInvoke method, this is bad. Here is the main thread of Invoke:
if( !this.IsHandleCreated ) throw; ... do more stuff PostMessage( this.Handle, ... );
The problem here is that from another thread I can successfully pass the first if statement, after which the handle is destroyed by the control thread, which leads to get from the Handle property re-creating the window handle in my thread. This may cause an exception in the original control flow. In fact, I'm really confused because there is no way to protect myself from this. If they used the InternalHandle property and tested for the IntPtr.Zero result, this was not a problem.