【问题标题】:Wpf child form, OnClosing event and awaitWpf 子窗体,OnClosing 事件和等待
【发布时间】:2017-12-31 18:41:33
【问题描述】:

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

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 有效并且子窗体被关闭...

有什么线索吗? 谢谢!

【问题讨论】:

  • 为什么需要异步显示消息?既然您在窗口的事件处理程序中,那不应该已经在 UI 线程上吗?
  • @Andy 你是对的......如果我将 ShowMessageAsync 更改为 ShowModalMessageExternal (不是异步方法),问题就解决了。尽管这并不能解释这种奇怪的行为(我想知道到底发生了什么,你知道......)如果你发表你的评论作为答案,我会非常乐意将其标记为好的。谢谢!

标签: wpf async-await formclosing


【解决方案1】:

由于这个方法是一个窗口的事件处理函数,它已经在UI线程上被调用,所以不需要异步显示消息框。

至于您看到的奇怪行为,这与事件处理程序中的await 有关。当您await 进行方法调用时,实际发生的情况是直到await 之前的所有内容都正常执行,但是一旦到达await 语句,控制权就会返回给调用者。一旦awaited 的方法返回,那么原始方法的其余部分就会执行。

触发OnClosing 事件的代码可能在设计时并未考虑到异步事件处理程序,因此它假定如果事件处理程序返回,它已经完成了它需要做的任何工作。由于您的事件处理程序在方法调用上将true 设置为awaits 之前的true,因此事件处理程序的调用者会看到它被设置为true,因此它不会关闭表单。

这就是显示消息框同步工作的原因:整个方法在控制权返回给调用者之前执行,因此CancelEventArgs.Cancel 始终设置为其预期值。

Raymond Chen 最近发表了两篇关于async 的文章,可能会引起人们的兴趣:Crash course in async and awaitThe perils of async void。第二篇文章描述了为什么async 事件处理程序往往无法按照您的预期工作。

【讨论】:

  • 非常感谢您的解释!
  • 我明白为什么会这样。这并没有为如何继续提供任何提示。
  • @Arrow_Raider 在这个问题的情况下,解决方案是不异步显示消息框。该方法已经在 UI 线程上调用,所以消息框应该只是同步显示。
【解决方案2】:

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

我们可以通过在等待后再次往返于OnClosing 来解决此问题,这一次带有一个标志来指示是否实际关闭,但问题是在窗口已经关闭时调用Close , 是不允许的,会抛出异常。

解决此问题的方法是简单地将Close的执行推迟到当前关闭过程之后,此时再次生效以关闭窗口。

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

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

CoreWindow.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);
    }
}

BaseDialogViewModel.cs

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

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

非常感谢 Rick Strahl 的 Dispatcher 解决方案!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-13
    • 1970-01-01
    • 2023-03-14
    • 2018-04-18
    • 2022-01-19
    相关资源
    最近更新 更多