我有一个从父表单启动的子表单:
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 线程上调用,因此无需异步显示消息框。
至于您看到的奇怪行为,这与事件处理程序中的
await
有关。当您 await
调用方法时,实际发生的情况是,直到 await
之前的所有内容都正常执行,但一旦到达 await
语句,控制权就会返回给调用者。一旦返回后 await
的方法,就会执行原始方法的其余部分。
触发
OnClosing
事件的代码可能在设计时并未考虑异步事件处理程序,因此它假设如果事件处理程序返回,则它已完成需要执行的所有工作。由于您的事件处理程序在方法调用之前将 CancelEventArgs.Cancel
设置为 true
,因此事件处理程序的调用方会看到它设置为 await
,因此不会关闭表单。这就是同步显示消息框的原因:整个方法在控制权返回给调用者之前执行,因此 true
始终设置为其预期值。
Raymond Chen 最近发布了两篇关于CancelEventArgs.Cancel
的文章,读起来可能会很有趣:
async 和 wait 速成课程和 async void 的危险。第二篇文章描述了为什么
async
事件处理程序往往无法按照您期望的方式工作。async
中使用 async/await 的主要问题是,正如 Andy 解释的那样,一旦执行了 wait 语句,控制权就会返回给调用者,并且关闭过程会继续。
我们可以通过等待后再次返回OnClosing
来解决这个问题,这次用一个标志来指示是否实际关闭,但问题是在窗口已经关闭时调用
OnClosing
,是不允许并抛出异常。解决这个问题的方法就是简单地将Close
的执行推迟到当前关闭过程之后,此时关闭窗口再次有效。
我想做这样的事情来允许用户处理 ViewModel 中的异步关闭逻辑。我不知道是否还有其他边缘情况我没有涵盖,但到目前为止这段代码对我有用:
CoreWindow.cs
Close
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();
。