Sunday, March 6, 2011

How to block the UI thread from another thread or force a form to run within the UI thread

A requirement for my application is if it looses database connectivity then it must pop up a big modal "No Connection. Try again later" dialog blocking all user interaction until such time that connectivity is regained.

I achieve this by at the start of the application starting an instance of a DeviceMonitor class. This class creates a System.Threading.Timer and every Tick (usually 1 second) along with a few other things it tries to draw data from the database. If the draw fails and the cause is determined to be due to a lack of connectivity, the exception is handled by popping up the aforementioned dialog. Likewise, if the data fetch succeeds and the dialog is currently up hen it is forced closed.

The problem is that although this all works fine, the ConnectionLost dialog does not block the user from interacting with the UI. This makes sense, since the Timer.Elapsed event is raised within its own thread and noConnectionDialog.ShowDialog() is called from within the callback it blocks the thread it is on but not the UI Thread.

To my understanding I need to either force the noConnectionDialog.ShowDialog() to run within the UI thread or to block the UI thread until noConnectionDialog.Hide() is called but I don't know how to do either.

Perhaps there is some other remedy or I am missing something here. Any advice is appreciated.

EDIT: Further information - this is a stylized dialog, not just a messagebox. It is being created when my application starts by Castle Windsor and injected into a DialogFactory class which gets passed around. The dialog is therefore accessed by

var d = _dialogFactory.GetNoConnectionDialog();
d.ShowDialog();

I have experimented with putting this code outside of the timer elapsed callback - when a button on UI interface is clicked for example - and it blocks the UI just fine from there so this is not a matter of where the form is created.

From stackoverflow
  • If you have access to a UI element, you can push to the UI thread by using things like:

    someControl.Invoke((Action)delegate {
        MessageBox.Show(someControl, "Foo");
        // could also show a form here, etc
    });
    

    (the someControl in MessageBox.Show helps parent the message-box)

    If you don't have access to a UI control, you can also use sync-context:

    SynchronizationContext.Current.Post(delegate {
        MessageBox.Show("Foo");
    }, null);
    

    But it is easier to keep hold of a control ;-p

    George Mauer : This is not a messagebox however. Or rather its a stylized message box so it has to be a full form. I do have a decorator wrapping the dialog that calls Invoke() if necessary so I thought it would be running on the UI thread, but apparently it is not.
    Marc Gravell : The form will want to run on the thread that creates it. So create the form within the Control.Invoke
    George Mauer : The form is created by Castle Windsor and is being DIed into a dialog factory class so I don't think this is a problem. When I have it called from a place other than the device monitor it blocks the UI thread just fine
    George Mauer : please see my edit
    George Mauer : System.Threading.SynchronizationContext.Current == null !!!! What the heck could that mean?
    Marc Gravell : A null sync-context means you don't have a UI-pumping thread, i.e. a primary winform.
    RichardOD : +1 for SynchronizationContext. This still remains a little used feature.
  • I'm pretty sure what Marc suggested should work. This is how I would write it to use your dialog instead of MessageBox:

    someControl.Invoke((Action)delegate {
        var d = _dialogFactory.GetNoConnectionDialog();
        d.ShowDialog();
    }, null);
    

    If that really isn't working I've had success in the past using a Timer control (System.Windows.Forms.Timer) on my form and a queue of Actions with an Tick function that looks like this:

    void timer_Tick(object sender, System.EventArgs e)
    {
        lock(queue)
        {
            while(queue.Count > 0)
            {
                Action a = queue.Dequeue();
                a();
            }
        }
    }
    

    And when your DeviceMonitor class needs to show the UI it would do this:

    lock(queue)
    {
        queue.Enqueue((Action)delegate 
        {
            var d = _dialogFactory.GetNoConnectionDialog();
            d.ShowDialog();
        });
    }
    

    That being said, I really want to reiterate that I think Marc's method should work correctly and I would only use my Timer + queue method if you're absolutely certain that Control.Invoke won't work for you.

  • This is called marshaling and is a very simple concept once you read some good material on it (Google is your friend).

    If your background thread has a delegate that calls into an object that is owned by the UI thread, then that method (on the called end of the delegate) simply has to marshal itself back onto the thread that owns its object (which will be the UI thread) and then block. It's very simple code (IsInvokeRequired), you just have to understand how to lay things out. (This is basically restating what Marc said, but from a higher level.)

0 comments:

Post a Comment