首页 > 解决方案 > 如何等待所有模式对话框异步关闭?

问题描述

背景

作为客户端上不同应用程序之间的状态事务1(WCF 进程间消息传递)的一部分,在此期间用户不得修改任何应用程序状态,这一点很重要。今天,这种“阻塞”是通过模态对话框分两步完成的。首先,一个“不可见”的2模态对话框打开了几百毫秒。这会阻止用户交互并停止执行该方法,直到对话框关闭。如果超时后事务仍未完成,我们将显示一个可见的模态进度对话框。

这个不可见的模态对话框给我们带来了问题,我认为我可以通过简单地将其设置为await Task.Delay(timoutBeforeProgressDialog)3 + 阻止用户输入和消息过滤器来解决。我认为这会产生与在短时间内显示不可见模式对话框相同的效果。然而,情况似乎并非如此。如果在事务回调期间我们向用户显示一个消息框,要求保存他们的更改,await Task.Delay(timoutBeforeProgressDialog)则将在超时后继续并在消息框上方弹出进度对话框,阻止用户输入。隐形模态不会发生这种情况。当不可见的模态被 timout 关闭时,它不会继续执行,直到回调模态对话框关闭。

您可能会争辩说我们不应该这样做,并且应该重新设计事务逻辑。然而,这种逻辑在整个应用程序中非常根深蒂固和广泛,因此是一个代价高昂的重构。我希望只是重构这个不可见的模态对话框逻辑。

我还可以补充一点,从历史上看,我们没有在这种情况下显示不可见的模式,而是在循环中调用 Application.DoEvents() 直到准备好继续(这会产生相同的效果)。所有这些都是在 async await 出现在 .NET 之前很久就实现的。

1事务包括询问是否可以切换上下文,然后处理上下文切换回调。

2不可见的模态:只是一个模态对话框,不会窃取焦点,没有宽度或高度,并且在屏幕外打开。

3如果交易在之前完成,还有一个取消令牌可以停止延迟。


问题

我想停止执行一个方法,直到应用程序中的所有模态对话框都关闭(或改写:直到消息循环正在抽水)。我想要实现的示例:

public partial class Form1 :Form
{
    public Form1() {
        InitializeComponent();
    }

    private async void button_Click(object sender, EventArgs e) {

        this.BeginInvoke(new MethodInvoker(() => {
            MessageBox.Show(this, "A modal dialog", "Dialog", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }));

        await Task.Delay(1000);

        // wait for all modals to be closed / the main message loop is running. 
        await AllModalsClosed();

        Console.WriteLine("This should be logged after user presses OK in the dialog shown above.");
    }

    private async Task AllModalsClosed() {
        var allModalsClosedSource = new TaskCompletionSource<bool>();
        if (!this.TopLevelControl.CanFocus) {
            // Modals are open
            Application.LeaveThreadModal += (object s, EventArgs args) => {
                allModalsClosedSource.SetResult(true);
            };
            await allModalsClosedSource.Task;
        }
    }
}

但我不确定这是否在所有情况下都是正确的,或者它是否是最好的方法。我还必须使这个实现独立于实际的对话框,因为它可能在应用程序的任何地方。请注意,我不想阻塞主线程。

我还尝试研究是否有任何方法可以使用 BeginInvoke 发送到主消息循环,如果可能的话,我可以重写AllModalsClosed为:

private async Task AllModalsClosed() {
    var allModalsClosedSource = new TaskCompletionSource<bool>();

    // Made up BeginInvoke variant.
    // Is anything like this possible?
    this.BeginInvokeToMainMessageLoop(new MethodInvoker(() => {
        allModalsClosedSource.SetResult(true);
    }));
    await allModalsClosedSource.Task;
}

或者,是否有某种方法可以将任务配置为仅在完成后继续主消息循环?

标签: c#.netwinforms

解决方案


我不确定这背后的真正要求是什么,但是您在示例中尝试做的事情可以通过这种方式轻松完成:

private async void Form1_Load(object sender, EventArgs e)
{
    await Task.Run(() => MessageBox.Show("Hi"));
    MessageBox.Show("All dialog closed!");
}

它会将“Hi”显示为非模态,您可以访问主窗口。它也不会阻塞 UI 线程,而是等到“Hi”对话框关闭,然后运行下一行。

如果上面的代码是你要找的,你可以忽略这篇文章的其余部分;但是,如果您想出于学习目的阅读它,它会显示如何检测当前应用程序中的所有模态对话框以及如何等待所有对话框关闭

您可以创建一个函数来计算模态窗口的数量。我看到以下模式窗口:

  • 那些窗口喜欢MessageBoxColorDialog#32770作为窗口类
  • Form您使用 显示的那些s ShowDialog,其Modal属性为 true。

要枚举它们,您可以获取当前进程的所有线程,然后对于每个线程,然后使用EnumThreadWindows获取所有窗口并使用GetClassName检查类是否为#32770.

然后使用Application.OpenForms获取其Modal属性为的表单列表true

delegate bool EnumThreadDelegate(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")]
static extern bool EnumThreadWindows(int dwThreadId, EnumThreadDelegate lpfn, IntPtr lParam);
[DllImport("user32.dll")]
static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

static IEnumerable<IntPtr> GetModalWindowsHandles(int processId)
{
    var handles = new List<IntPtr>();
    foreach (ProcessThread thread in Process.GetProcessById(processId).Threads)
        EnumThreadWindows(thread.Id,
            (hWnd, lParam) =>
            {
                var className = new StringBuilder(256);
                GetClassName(hWnd, className, 256);
                if (className.ToString() == "#32770")
                {
                    handles.Add(hWnd);
                }
                return true;
            }, IntPtr.Zero);
    foreach (Form form in Application.OpenForms)
        form.Invoke(new Action(() =>
        {
            if (form.Modal)
                handles.Add(form.Handle);
        }));
    return handles;
}

例子

以下示例打开多个模态并等待它们全部关闭:

private async void Form1_Load(object sender, EventArgs e)
{
    Task.Run(() => MessageBox.Show("Hi"));
    Task.Run(() => new ColorDialog().ShowDialog());
    Task.Run(() => new Form().ShowDialog());
    await WaitUntil(() => GetModalWindowsHandles(Process.GetCurrentProcess().Id).Count() == 0);
    MessageBox.Show("All dialog closed!");
}
public async Task WaitUntil(Func<bool> condition, int frequency = 25, int timeout = -1)
{
    var waitTask = Task.Run(async () =>
    {
        while (!condition()) await Task.Delay(frequency);
    });
    if (waitTask != await Task.WhenAny(waitTask,
            Task.Delay(timeout)))
        throw new TimeoutException();
}

WaitUntil 取自这篇文章以在此示例中使用。


推荐阅读