如何阻止来自另一个线程的UI线程或强制表单在UI线程中运行

时间:2022-12-17 21:02:08

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.

我通过在应用程序启动时启动DeviceMonitor类的实例来实现此目的。这个类创建一个System.Threading.Timer和每个Tick(通常是1秒)以及它试图从数据库中绘制数据的一些其他东西。如果绘制失败并且确定原因是由于缺乏连接性,则通过弹出上述对话框来处理异常。同样,如果数据获取成功并且对话框当前处于up,那么它将被强制关闭。

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.

问题是尽管一切正常,但ConnectionLost对话框不会阻止用户与UI交互。这是有道理的,因为Timer.Elapsed事件是在它自己的线程中引发的,并且在回调中调用noConnectionDialog.ShowDialog()它会阻塞它所在的线程而不是UI线程。

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.

根据我的理解,我需要强制noConnectionDialog.ShowDialog()在UI线程内运行或阻止UI线程,直到调用noConnectionDialog.Hide()但我不知道如何做。

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

编辑:更多信息 - 这是一个程式化的对话框,而不仅仅是一个消息框。当我的应用程序由Castle Windsor启动并注入到一个被传递的DialogFactory类时,它就被创建了。因此,访问该对话框

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.

我已经尝试将此代码放在计时器过去的回调之外 - 例如,当单击UI界面上的按钮时 - 它会阻止UI从那里开始,所以这不是表单创建的位置。

3 个解决方案

#1


2  

I'm pretty sure what Marc suggested should work. This is how I would write it to use your dialog instead of MessageBox:

我很确定Marc建议的应该是什么。这是我写它来使用你的对话而不是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:

如果那真的不起作用我以前在我的表单上使用Timer控件(System.Windows.Forms.Timer)和带有Tick函数的Actions队列成功,如下所示:

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:

当您的DeviceMonitor类需要显示UI时,它会执行以下操作:

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.

话虽这么说,我真的想重申一下,我认为Marc的方法应该正常工作,如果你绝对确定Control.Invoke对你不起作用,我只会使用我的Timer +队列方法。

#2


1  

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

如果您可以访问UI元素,则可以使用以下内容来推送到UI线程:

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)

(MessageBox.Show中的someControl帮助父消息框)

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

如果您无权访问UI控件,还可以使用sync-context:

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

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

但是保持控制更容易;-p

#3


0  

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.)

如果你的后台线程有一个调用UI线程所拥有的对象的委托,那么该方法(在委托的被调用端)只需要将自己编组回到拥有其对象的线程(这将是UI线程)然后阻止。这是非常简单的代码(IsInvokeRequired),你只需要了解如何解决问题。 (这基本上重述了Marc所说的,但是来自更高层次。)

#1


2  

I'm pretty sure what Marc suggested should work. This is how I would write it to use your dialog instead of MessageBox:

我很确定Marc建议的应该是什么。这是我写它来使用你的对话而不是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:

如果那真的不起作用我以前在我的表单上使用Timer控件(System.Windows.Forms.Timer)和带有Tick函数的Actions队列成功,如下所示:

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:

当您的DeviceMonitor类需要显示UI时,它会执行以下操作:

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.

话虽这么说,我真的想重申一下,我认为Marc的方法应该正常工作,如果你绝对确定Control.Invoke对你不起作用,我只会使用我的Timer +队列方法。

#2


1  

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

如果您可以访问UI元素,则可以使用以下内容来推送到UI线程:

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)

(MessageBox.Show中的someControl帮助父消息框)

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

如果您无权访问UI控件,还可以使用sync-context:

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

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

但是保持控制更容易;-p

#3


0  

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.)

如果你的后台线程有一个调用UI线程所拥有的对象的委托,那么该方法(在委托的被调用端)只需要将自己编组回到拥有其对象的线程(这将是UI线程)然后阻止。这是非常简单的代码(IsInvokeRequired),你只需要了解如何解决问题。 (这基本上重述了Marc所说的,但是来自更高层次。)