views:

989

answers:

2

In the course of my maintenance for an older application that badly violated the cross-thread update rules in winforms, I created the following extension method as a way to quickly fix illegal calls when I've discovered them:

/// <summary>
/// Execute a method on the control's owning thread.
/// </summary>
/// <param name="uiElement">The control that is being updated.</param>
/// <param name="updater">The method that updates uiElement.</param>
/// <param name="forceSynchronous">True to force synchronous execution of 
/// updater.  False to allow asynchronous execution if the call is marshalled
/// from a non-GUI thread.  If the method is called on the GUI thread,
/// execution is always synchronous.</param>
public static void SafeInvoke(this Control uiElement, Action updater, bool forceSynchronous)
{
    if (uiElement == null)
    {
        throw new ArgumentNullException("uiElement");
    }

    if (uiElement.InvokeRequired)
    {
        if (forceSynchronous)
        {
            uiElement.Invoke((Action)delegate { SafeInvoke(uiElement, updater, forceSynchronous); });
        }
        else
        {
            uiElement.BeginInvoke((Action)delegate { SafeInvoke(uiElement, updater, forceSynchronous); });
        }
    }
    else
    {
        if (!uiElement.IsHandleCreated)
        {
            // Do nothing if the handle isn't created already.  The user's responsible
            // for ensuring that the handle they give us exists.
            return;
        }

        if (uiElement.IsDisposed)
        {
            throw new ObjectDisposedException("Control is already disposed.");
        }

        updater();
    }
}

Sample usage:

this.lblTimeDisplay.SafeInvoke(() => this.lblTimeDisplay.Text = this.task.Duration.ToString(), false);

I like how I can leverage closures to read, also, though forceSynchronous needs to be true in that case:

string taskName = string.Empty;
this.txtTaskName.SafeInvoke(() => taskName = this.txtTaskName.Text, true);

I don't question the usefulness of this method for fixing up illegal calls in legacy code, but what about new code?

Is it good design to use this method to update UI in a piece of new software when you may not know what thread is attempting to update the ui, or should new Winforms code generally contain a specific, dedicated method with the appropriate Invoke()-related plumbing for all such UI updates? (I'll try to use the other appropriate background processing techniques first, of course, e.g. BackgroundWorker.)

Interestingly this won't work for ToolStripItems. I just recently discovered that they derive directly from Component instead of from Control. Instead, the containing ToolStrip's invoke should be used.

Followup to comments:

Some comments suggest that:

if (uiElement.InvokeRequired)

should be:

if (uiElement.InvokeRequired && uiElement.IsHandleCreated)

Consider the following msdn documentation:

This means that InvokeRequired can return false if Invoke is not required (the call occurs on the same thread), or if the control was created on a different thread but the control's handle has not yet been created.

In the case where the control's handle has not yet been created, you should not simply call properties, methods, or events on the control. This might cause the control's handle to be created on the background thread, isolating the control on a thread without a message pump and making the application unstable.

You can protect against this case by also checking the value of IsHandleCreated when InvokeRequired returns false on a background thread.

If the control was created on a different thread but the control's handle has not yet been created, InvokeRequired returns false. This means that if InvokeRequired returns true, IsHandleCreated will always be true. Testing it again is redundant and incorrect.

+4  A: 

I like the general idea, but I do see one problem. It is important to process EndInvokes, or you can have resource leaks. I know a lot of people don't believe this, but it really is true.

Here's one link talking about it. There are others as well.

But the main response I have is: Yes, I think you've got a nice idea here.

Charlie Flowers
Agreed about EndInvoke: shouldn't be that hard to use a waithandle to call it right there, though.
Joel Coehoorn
In fact, there's an SO question about it: http://stackoverflow.com/questions/532722/is-endinvoke-optional-sort-of-optional-or-definitely-not-optional
Charlie Flowers
FWIW, I recall reading on MSDN that, in this single, special, magical, particular case, it's documented to not be required. I don't have a link, though, and it isn't true until I do. :)
Greg D
I vaguely remember something about that too.
Charlie Flowers
@Greg: It's definitely not your imagination: http://weblogs.asp.net/cbrumme/archive/2003/05/06/51385.aspx
Jon Skeet
(I don't know if that link still works or not - it's taking a long time to load for me at the minute over 3G. It at least *used* to say that though :)
Jon Skeet
I've had timeouts with other MS blogs this morning already, so I suspect it's an issue with MS' system today.
Joel Coehoorn
Relevant portion from Jon's link: >> "I just got the official word from the WinForms team. It is not necessary to call Control.EndInvoke. You can call BeginInvoke in a "fire and forget" manner with impunity." << Note that was in the comments, and it's from 5/2003, but it's probably still true.
Joel Coehoorn
Yes, but I've seen later posts that seem to contradict that. It would be great to get the final, correct answer once and for all.
Charlie Flowers
Does anybody know a WinForms PM? :)
Greg D
You don't have to call Control.EndInvoke because the Control.BeginInvoke call resolves to a PostMessage onto the message loop of the control's thread. You can't easily tell if this message gets processed, and there's no return value.
Garo Yeriazarian
That makes sense, no doubt about it. I am inclined to believe all of you, because Control.BeginInvoke / EndInvoke really has very little in common with async delegates (other than the name). But I have heard and read contradictory claims. I'd feel better hearing it from some authority.
Charlie Flowers
+3  A: 

You should create Begin and End extension methods as well. And if you use generics, you can make the call look a little nicer.

public static class ControlExtensions
{
  public static void InvokeEx<T>(this T @this, Action<T> action)
    where T : Control
  {
    if (@this.InvokeRequired)
    {
      @this.Invoke(action, new object[] { @this });
    }
    else
    {
      if ([email protected])
        return;
      if (@this.IsDisposed)
        throw new ObjectDisposedException("@this is disposed.");

      action(@this);
    }
  }

  public static IAsyncResult BeginInvokeEx<T>(this T @this, Action<T> action)
    where T : Control
  {
    return @this.BeginInvoke((Action)delegate { @this.InvokeEx(action); });
  }

  public static void EndInvokeEx<T>(this T @this, IAsyncResult result)
    where T : Control
  {
    @this.EndInvoke(result);
  }
}

Now your calls get a little shorter and cleaner:

this.lblTimeDisplay.InvokeEx(l => l.Text = this.task.Duration.ToString());

var result = this.BeginInvokeEx(f => f.Text = "Different Title");
// ... wait
this.EndInvokeEx(result);

And with regards to Components, just invoke on the form or container itself.

this.InvokeEx(f => f.toolStripItem1.Text = "Hello World");
Samuel
Interesting. If we do this, we need to insert an appropriate call to EndInvoke(), I think, b/c we'll be stepping outside the perceived "safe zone" of ignoring Control.EndInvoke(). Also, it isn't safe to check IsHandleCreate or IsDisposed before the invoke.
Greg D
I've updated it, it should be fine now.
Samuel
I don't understand the benefit from the type parameter. Unless- is it for use from languages that don't support closures?
Greg D