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

本文关键字:终止 等待 代码 窗口 WPF | 更新日期: 2023-09-27 18:04:44

我正在尝试实现一个系统,用于关闭WPF应用程序中的所有模式和非模式窗口(主应用程序窗口除外(。关闭这些窗口时,应放弃任何等待对话框结果的代码。

到目前为止,我已经考虑/尝试了两种策略:

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

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

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

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

一些伪代码 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
    }
}

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

使用 Window.ShowDialog 可以创建嵌套的Dispather消息循环。使用 await ,可以"跳转"到该内部循环并在那里继续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

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

通过查看您的伪代码,很难判断死锁可能在哪里。令人担忧的是,您仅使用 Task.Run 来等待在主 UI 线程上完成的任务,或者从池线程(通过 Dispatcher.Invoke(调用主 UI 线程上的同步回调。你当然不需要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取消最外层的模态窗口,这将自动关闭任何嵌套的模态窗口(用ShowDialogAsync打开的窗口(并退出其相应的消息循环。因此,您的逻辑执行流最终将位于正确的外部消息循环中。

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

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

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");
    }
}

这只是问题的第一部分(关闭窗口(。

如果您不需要窗口的任何结果,这里有一些简单的代码来关闭除主窗口之外的所有窗口。

这是从我的主窗口执行的,但如果从备用区域运行,您可以更改 if 语句以查找主窗口。

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

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