Note that there are some explanatory texts on larger screens.

plurals
  1. POCreating a way to safely access controls from another thread
    primarykey
    data
    text
    <p>I am trying to write a 'SafeInvoke' method that handles all cases/problems that can occur when trying to access a control from another thread. I've seen a lot of solutions and a lot of questions about this and while there are some that are good enough for most people, they all fail to take race conditions in account (meaning that it's still possible to get an unwanted exception).</p> <p>So this is what I have so far, I tried commenting as best as I could why I put some ifs and try catches. I also tried to catch only the relevant exceptions, InvalidOperationException is one that can occur for a wide range of reasons (including Collection was modified) and I didn't want to suppress those (because they have nothing to do with the safe invoking). To check that I based myself on the TargetSite.Name property of the exception, I also looked up the actual throw in reflector to see if there were any other locations that could cause an exception.</p> <pre><code>/// &lt;summary&gt; /// Safely invokes an action on the thread the control was created on (if accessed from a different thread) /// &lt;/summary&gt; /// &lt;typeparam name="T"&gt;The return type&lt;/typeparam&gt; /// &lt;param name="c"&gt;The control that needs to be invoked&lt;/param&gt; /// &lt;param name="a"&gt;The delegate to execute&lt;/param&gt; /// &lt;param name="spinwaitUntilHandleIsCreated"&gt;Waits (max 5sec) until the the control's handle is created&lt;/param&gt; /// &lt;returns&gt;The result of the given delegate if succeeded, default(T) if failed&lt;/returns&gt; public static T SafeInvoke&lt;T&gt;(this Control c, Func&lt;T&gt; a, bool spinwaitUntilHandleIsCreated = false) { if (c.Disposing || c.IsDisposed) // preliminary dispose check, not thread safe! return default(T); if (spinwaitUntilHandleIsCreated) // spin wait until c.IsHandleCreated is true { if (!c.SpinWaitUntilHandleIsCreated(5000)) // wait 5sec at most, to prevent deadlock return default(T); } if (c.InvokeRequired) // on different thread, need to invoke (can return false if handle is not created) { try { return (T)c.Invoke(new Func&lt;T&gt;(() =&gt; { // check again if the control is not dispoded and handle is created // this is executed on the thread the control was created on, so the control can't be disposed // while executing a() if (!c.Disposing &amp;&amp; !c.IsDisposed &amp;&amp; c.IsHandleCreated) return a(); else // the control has been disposed between the time the other thread has invoked this delegate return default(T); })); } catch (ObjectDisposedException ex) { // sadly the this entire method is not thread safe, so it's still possible to get objectdisposed exceptions because the thread // passed the disposing check, but got disposed afterwards. return default(T); } catch (InvalidOperationException ex) { if (ex.TargetSite.Name == "MarshaledInvoke") { // exception that the invoke failed because the handle was not created, surpress exception &amp; return default // this is the MarhsaledInvoke method body part that could cause this exception: // if (!this.IsHandleCreated) // { // throw new InvalidOperationException(SR.GetString("ErrorNoMarshalingThread")); // } // (disassembled with reflector) return default(T); } else // something else caused the invalid operation (like collection modified, etc.) throw; } } else { // no need to invoke (meaning this is *probably* the same thread, but it's also possible that the handle was not created) // InvokeRequired has the following code part: // Control wrapper = this.FindMarshalingControl(); // if (!wrapper.IsHandleCreated) // { // return false; // } // where findMarshalingControl goes up the parent tree to look for a parent where the parent's handle is created // if no parent found with IsHandleCreated, the control itself will return, meaning wrapper == this and thus returns false if (c.IsHandleCreated) { try { // this will still yield an exception when the IsHandleCreated becomes false after the if check (race condition) return a(); } catch (InvalidOperationException ex) { if (ex.TargetSite.Name == "get_Handle") { // it's possible to get a cross threadexception // "Cross-thread operation not valid: Control '...' accessed from a thread other than the thread it was created on." // because: // - InvokeRequired returned false because IsHandleCreated was false // - IsHandleCreated became true just after entering the else bloc // - InvokeRequired is now true (because this can be a different thread than the control was made on) // - Executing the action will now throw an InvalidOperation // this is the code part of Handle that will throw the exception // //if ((checkForIllegalCrossThreadCalls &amp;&amp; !inCrossThreadSafeCall) &amp;&amp; this.InvokeRequired) //{ // throw new InvalidOperationException(SR.GetString("IllegalCrossThreadCall", new object[] { this.Name })); //} // // (disassembled with reflector) return default(T); } else // something else caused the invalid operation (like collection modified, etc.) throw; } } else // the control's handle is not created, return default return default(T); } } </code></pre> <p>There is 1 thing I don't know for sure, which is if IsHandleCreated=true, will it ever become false again ?</p> <p>I added the spinwait for IsHandleCreated because I started Task&lt;>s in the OnLoad event of the control and it was possible that the task was finished before the control was completely finished with loading. If however it takes longer than 5sec to load a control I let the task finish anyway, without updating the GUI (otherwise I'd have a lot of threads spinwaiting for something that probably won't occur anymore)</p> <p>If you have any suggestions for optimizations or find any bugs or scenario's that might still pose a problem, please let me know :).</p>
    singulars
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload