Wpf 子表单,OnClosing 事件和等待

问题描述 投票:0回答:2

我有一个从父表单启动的子表单:

ConfigForm cfg = new ConfigForm();
cfg.ShowDialog();

该子表单用于配置一些应用程序参数。 我想检查是否有一些更改未保存,如果是,则警告用户。 所以我的 On OnClosing 事件是这样声明的:

private async void ChildFormClosing(object sender, System.ComponentModel.CancelEventArgs e)
{
    // Here i call a function that compare the current config with the saved config
    bool isUptated = CheckUnsavedChanges();

    // If updated is false, it means that there are unsaved changes...
    if (!isUpdated)
    {
         e.Cancel = true;

        // At this point i create a MessageDialog (Mahapps) to warn the user about unsaved changes...
        MessageDialogStyle style = MessageDialogStyle.AffirmativeAndNegative;

        var metroDialogSettings = new MetroDialogSettings()
        {
            AffirmativeButtonText = "Close",
            NegativeButtonText = "Cancel"
        };

        var result = await this.ShowMessageAsync("Config", "There are unsaved changes, do you want to exit?", style, metroDialogSettings);

        // If we press Close, we want to close child form and go back to parent...
        if (result == MessageDialogResult.Affirmative)
        {
            e.Cancel = false;
        }
    }
}

我的逻辑是,如果我将 e.cancel 声明为 false,它将继续关闭表单,但它不会发生,子表单保持打开状态。

我的猜测是异步调用正在做一些我不明白的事情,因为如果我以这种方式声明 ChildFormClosing :

private async void ChildFormClosing(object sender, System.ComponentModel.CancelEventArgs e)
{
    bool isUptated = CheckUnsavedChanges();

    e.Cancel = true;

    if (!isUpdated)
    {
        MessageDialogStyle style = MessageDialogStyle.AffirmativeAndNegative;

        var metroDialogSettings = new MetroDialogSettings()
        {
            AffirmativeButtonText = "Close",
            NegativeButtonText = "Cancel"
        };

        var result = await this.ShowMessageAsync("Config", "There are unsaved changes, do you want to exit?", style, metroDialogSettings);

        if (result == MessageDialogResult.Affirmative)
        {
            e.Cancel = false;
        }
    }
    else
    {
        e.Cancel = false;
    }
}

最后的 else e.Cancel = false 有效,子窗体关闭...

有什么线索吗? 谢谢!

wpf async-await formclosing
2个回答
6
投票

由于该方法是窗口的事件处理程序,因此它已经在 UI 线程上调用,因此无需异步显示消息框。

至于您看到的奇怪行为,这与事件处理程序中的

await
有关。当您
await
调用方法时,实际发生的情况是,直到
await
之前的所有内容都正常执行,但一旦到达
await
语句,控制权就会返回给调用者。一旦返回后
await
的方法,就会执行原始方法的其余部分。

触发

OnClosing
事件的代码可能在设计时并未考虑异步事件处理程序,因此它假设如果事件处理程序返回,则它已完成需要执行的所有工作。由于您的事件处理程序在方法调用之前将
CancelEventArgs.Cancel
设置为
true
,因此事件处理程序的调用方会看到它设置为
await
,因此不会关闭表单。
这就是同步显示消息框的原因:整个方法在控制权返回给调用者之前执行,因此 

true

始终设置为其预期值。

Raymond Chen 最近发布了两篇关于 

CancelEventArgs.Cancel

的文章,读起来可能会很有趣:

async 和 wait 速成课程
async void 的危险。第二篇文章描述了为什么 async 事件处理程序往往无法按照您期望的方式工作。
    


1
投票
async

中使用 async/await 的主要问题是,正如 Andy 解释的那样,一旦执行了 wait 语句,控制权就会返回给调用者,并且关闭过程会继续。

我们可以通过等待后再次返回

OnClosing

来解决这个问题,这次用一个标志来指示是否实际关闭,但问题是在窗口已经关闭时调用

OnClosing
,是不允许并抛出异常。
解决这个问题的方法就是简单地将

Close

的执行推迟到当前关闭过程之后,此时关闭窗口再次有效。

我想做这样的事情来允许用户处理 ViewModel 中的异步关闭逻辑。

我不知道是否还有其他边缘情况我没有涵盖,但到目前为止这段代码对我有用:

CoreWindow.cs

Close

BaseDialogViewModel.cs

public class CoreWindow : Window { private bool _isClosing; private bool _canClose; private BaseDialogViewModel ViewModel => (BaseDialogViewModel) DataContext; public CoreWindow() { DataContextChanged += OnDataContextChanged; } private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) { if (e.OldValue is BaseDialogViewModel oldDataContext) { oldDataContext.Closed -= OnViewModelClosed; } if (e.NewValue is BaseDialogViewModel newDataContext) { newDataContext.Closed += OnViewModelClosed; } } private void OnViewModelClosed(object sender, EventArgs e) { if (!_isClosing) { _isClosing = true; Close(); } } protected override async void OnClosing(CancelEventArgs e) { if (ViewModel == null) { base.OnClosing(e); return; } if (!_canClose) { // Immediately cancel closing, because the decision // to cancel is made in the ViewModel and not here e.Cancel = true; base.OnClosing(e); try { // Ask ViewModel if allowed to close bool closed = await ViewModel.OnClosing(); if (closed) { // Set _canClose to true, so that when we call Close again // and return to this method, we proceed to close as usual _canClose = true; // Close cannot be called while Window is in closing state, so use // InvokeAsync to defer execution of Close after OnClosing returns _ = Dispatcher.InvokeAsync(Close, DispatcherPriority.Normal); } } catch (Exception ex) { // TODO: Log exception } finally { _isClosing = false; } } base.OnClosing(e); } }

BaseViewModel 仅包含一些验证和属性通知内容,与此处显示无关。

非常感谢

Rick Strahl

提供调度程序解决方案!

更新:

可以使用 public class BaseDialogViewModel : BaseViewModel { public event EventHandler Closed; public bool? DialogResult { get; set; } public void Close() { Closed?.Invoke(this, EventArgs.Empty); } /// <summary> /// Override to add custom logic while dialog is closing /// </summary> /// <returns>True if should close dialog, otherwise false</returns> public virtual Task<bool> OnClosing() { return Task.FromResult(true); } } 代替

await Task.Yield();
    

© www.soinside.com 2019 - 2024. All rights reserved.