关闭所有子 WPF windows 并终止等待代码

Close all child WPF windows and terminate awaiting code

我正在尝试实现一个系统来关闭 WPF 应用程序中的所有模态和非模态 windows(主应用程序除外 window。)当这些 windows 被关闭,任何等待对话框结果的代码都应该被放弃。

到目前为止,我有 considered/attempted 两种策略:

  1. 关闭并重新启动应用程序。
  2. 关闭所有 windows 并依靠任务取消异常来放弃所有等待对话结果的代码。 (它冒泡到 App 级别,然后被处理。)

第一个解决方案确实让应用程序关闭并且足以自动注销,但我对在等待的对话框关闭后继续执行的代码感到非常不舒服。有什么好的方法可以停止执行该代码?

第二种解决方案工作得相对较好(调用代码被中止)但有一个严重缺陷:偶尔,模态和非模态的某种组合windows 快速连续关闭将导致应用程序锁定 ShowDialog 调用。 (至少,当您暂停执行时,这就是它结束的地方。)这很奇怪,因为断点清楚地表明 Closed 事件正在我打算关闭的所有 windows 上引发。最终用户看到的结果是登录屏幕,无法单击但可以跳转。这么奇怪!尝试以不同的优先级调度调用没有成功,但是 100 毫秒的 Task.Delay 可能已经成功了。 (不过,这不是真正的解决方案。)

如果每个打开的弹出窗口都在后台等待 TaskCompletionSource,并且在 TCS 完成后尝试使用调度程序对其自身调用 Close,为什么一个(或多个) ) 的对话框仍然在 ShowDialog 上阻塞,即使在看到 Closed 事件被引发之后?有没有办法将这些调用正确地分派给 Close 以便它们成功完成?我需要特别注意 windows 关闭的顺序吗?

一些伪代码-C#-混合示例:

class PopupService
{
    async Task<bool> ShowModalAsync(...)
    {
        create TaskCompletionSource, publish event with TCS in payload
        await and return the TCS result
    }

    void ShowModal(...)
    {
        // method exists for historical purposes. code calling this should
        // probably be made async-aware rather than relying on the blocking
        // behavior of Window.ShowDialog
        create TaskCompletionSource, publish event with TCS in payload
        rethrow exceptions that are set on the Task after completion but do not await
    }

    void CloseAllWindows(...)
    {
        for every known TaskCompletionSource driving a popup interaction
            tcs.TrySetCanceled()
    }
}

class MainWindow : Window
{
    void ShowModalEventHandler(...)
    {
        create a new PopupWindow and set the owner, content, etc.
        var window = new PopupWindow(...) { ... };
        ...
        window.ShowDialog();
    }
}

class PopupWindow : Window
{
    void LoadedEventHandler(...)
    {
        ...
        Task.Run(async () =>
        {
            try
                await the task completion source
            finally
                Dispatcher.Invoke(Close, DispatcherPriority.Send);
        });

        register closing event handlers
        ...
    }

    void ClosedEventHandler(...)
    {
        if(we should do something with the TCS)
            try set the TCS result so the popup service caller can continue
    }
}

我不确定这是否会解决您的问题,但就我而言,我创建了扩展方法来帮助混合异步代码和 window 生命周期管理。例如,您可以创建一个 returns 任务将在 window 实际关闭时完成的 ShowDialogAsync()。如果您请求取消,还可以提供 CancellationToken 以自动关闭对话框。

public static class WindowExtension
{
    public static Task<bool?> ShowDialogAsync(this Window window, CancellationToken cancellationToken = new CancellationToken())
    {
        var completionSource = new TaskCompletionSource<bool?>();

        window.Dispatcher.BeginInvoke(new Action(() =>
        {
            var result = window.ShowDialog();
            // When dialog is closed, set the result to complete the returned task. If the task is already cancelled, it will be discarded.
            completionSource.TrySetResult(result);
        }));

        if (cancellationToken.CanBeCanceled)
        {
            // Gets notified when cancellation is requested so that we can close window and cancel the returned task 
            cancellationToken.Register(() => window.Dispatcher.BeginInvoke(new Action(() =>
            {
                completionSource.TrySetCanceled();
                window.Close();
            })));
        }

        return completionSource.Task;
    }
}

在您的 UI 代码中,您将使用 ShowDialogAsync() 方法,如下所示。如您所见,当任务被取消时,对话框将关闭并抛出 OperationCanceledException 异常以停止您的代码流。

private async void Button_Click(object sender, RoutedEventArgs e)
{
    try
    {
        YourDialog dialog = new YourDialog();
        CancellationTokenSource source = new CancellationTokenSource(TimeSpan.FromSeconds(3));
        await dialog.ShowDialogAsync(source.Token);
    }
    catch (OperationCanceledException ex)
    {
        MessageBox.Show("Operation was cancelled");
    }
}

使用 Window.ShowDialog 可以创建嵌套的 Dispather 消息循环。使用 await,可以在该内部循环上 "jump" 并在那里继续逻辑执行 async 方法,例如:

var dialogTask = window.ShowDialogAsync();
// on the main message loop
await Task.Delay(1000);
// on the nested message loop
// ...
await dialogTask;
// expecting to be back on the main message loop

现在,如果 dialogTask 通过 TaskCompletionSource 相应的 Window.ShowDialog() 调用 returns 之前完成,则上面的代码可能仍然会在嵌套的消息循环中结束,而不是在主要的核心消息循环中。例如,如果在对话框的 Window.Closed 事件处理程序中调用 TaskCompletionSource.SetResult/TrySetCanceled 或右 before/after Window.Close() 调用,则可能会发生这种情况。这可能会产生不希望的重入副作用,包括死锁。

通过查看您的伪代码,很难判断死锁可能在哪里。令人担忧的是,您使用 Task.Run 只是为了等待在主 UI 线程上完成的任务,或者从池线程在主 UI 线程上调用同步回调(通过 Dispatcher.Invoke)。你当然不应该在这里需要 Task.Run

出于类似目的,我使用以下版本的 ShowDialogAsync。它确保由嵌套 ShowDialogAsync 调用启动的任何内部消息循环 此特定 ShowDialogAsync 任务完成之前退出:

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;

namespace WpfApplication
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.Loaded += MainWindow_Loaded;
        }

        // testing ShowDialogAsync
        async void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            var modal1 = new Window { Title = "Modal 1" };
            modal1.Loaded += async delegate
            {
                await Task.Delay(1000);

                var modal2 = new Window { Title = "Modal 2" };
                try
                {
                    await modal2.ShowDialogAsync();
                }
                catch (OperationCanceledException)
                {
                    Debug.WriteLine("Cancelled: " + modal2.Title);
                }
            };

            await Task.Delay(1000);
            // close modal1 in 5s
            // this would automatically close modal2
            var cts = new CancellationTokenSource(5000); 
            try
            {
                await modal1.ShowDialogAsync(cts.Token);
            }
            catch (OperationCanceledException)
            {
                Debug.WriteLine("Cancelled: " + modal1.Title);
            }
        }
    }

    /// <summary>
    /// WindowExt
    /// </summary>
    public static class WindowExt
    {
        [ThreadStatic]
        static CancellationToken s_currentToken = default(CancellationToken);

        public static async Task<bool?> ShowDialogAsync(
            this Window @this, 
            CancellationToken token = default(CancellationToken))
        {
            token.ThrowIfCancellationRequested();
            var previousToken = s_currentToken;
            using (var cts = CancellationTokenSource.CreateLinkedTokenSource(previousToken, token))
            {
                var currentToken = s_currentToken = cts.Token;
                try
                {
                    return await @this.Dispatcher.InvokeAsync(() =>
                    {
                        using (currentToken.Register(() => 
                            @this.Close(), 
                            useSynchronizationContext: true))
                        {
                            try
                            {
                                var result = @this.ShowDialog();
                                currentToken.ThrowIfCancellationRequested();
                                return result;
                            }
                            finally
                            {
                                @this.Close();
                            }
                        }
                    }, DispatcherPriority.Normal, currentToken);
                }
                finally
                {
                    s_currentToken = previousToken;
                }
            }
        }
    }
}

这允许您通过关联的 CancelationToken 取消 最外层 模态 window,这将自动关闭任何嵌套模态 windows(那些以 ShowDialogAsync 打开的)并退出相应的消息循环。因此,您的逻辑执行流将在正确的 outer 消息循环中结束。

请注意,它仍然不能保证关闭多模式 windows 的正确逻辑顺序,如果这很重要的话。但它保证由多个嵌套 ShowDialogAsync 调用返回的任务将以正确的顺序完成。

这只是您问题的第一部分(关闭 windows)。

如果您不需要 windows 的任何结果,这里有一些简单的代码只是为了关闭除主要 window 之外的所有结果。

这是从我的主 window 执行的,但您可以更改 if 语句以从备用区域查找您的 mainwindow 而不是 if 运行。

foreach(Window item in App.Current.Windows)
            {
                if(item!=this)
                    item.Close();
            }

至于其他线程,我不确定,尽管如上所述,如果您有线程的句柄列表,那么您也应该能够遍历并杀死它们。