Wpf 子窗体,OnClosing 事件和等待
Wpf child form, OnClosing event and await
我有一个从父表单启动的子表单:
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 工作并且子表单被关闭...
有线索吗?
谢谢!
由于此方法是 window 的事件处理程序,它将在 UI 线程上调用,因此不需要异步显示消息框。
至于您看到的奇怪行为,这与事件处理程序中的 await
有关。当你 await
一个方法调用时,实际发生的是 await
之前的所有事情都正常执行,但是一旦 await
语句到达控制 returns呼叫者。一旦 await
编辑了 returns 的方法,就会执行原始方法的其余部分。
触发 OnClosing
事件的代码可能在设计时并未考虑异步事件处理程序,因此它假定如果事件处理程序 returns,它已完成需要完成的所有工作.由于您的事件处理程序在方法调用 await
之前将 CancelEventArgs.Cancel
设置为 true
,因此您的事件处理程序的调用者会看到它已设置为 true
,因此它不会'关闭表单。
这就是同步显示消息框的原因:整个方法在控制 returns 到调用者之前执行,因此 CancelEventArgs.Cancel
始终设置为其预期值。
Raymond Chen 最近发布了两篇关于 async
的文章,阅读起来可能很有趣:Crash course in async and await and The perils of async void。第二篇文章描述了为什么 async
事件处理程序往往无法按照您的预期工作。
在 OnClosing
中使用 async/await 的主要问题是,正如 Andy 所解释的那样,一旦执行 await 语句,控制权就会返回给调用者,并且关闭过程会继续。
我们可以通过在等待之后再次往返 OnClosing
来解决这个问题,这次用一个标志来指示是否实际关闭,但问题是调用 Close
虽然 Window 已经关闭,但不允许并抛出异常。
解决这个问题的方法是简单地将Close
的执行推迟到当前关闭过程之后,此时关闭window.[=17=再次生效。 ]
我想做这样的事情来允许用户处理 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 解决方案!
我有一个从父表单启动的子表单:
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 工作并且子表单被关闭...
有线索吗? 谢谢!
由于此方法是 window 的事件处理程序,它将在 UI 线程上调用,因此不需要异步显示消息框。
至于您看到的奇怪行为,这与事件处理程序中的 await
有关。当你 await
一个方法调用时,实际发生的是 await
之前的所有事情都正常执行,但是一旦 await
语句到达控制 returns呼叫者。一旦 await
编辑了 returns 的方法,就会执行原始方法的其余部分。
触发 OnClosing
事件的代码可能在设计时并未考虑异步事件处理程序,因此它假定如果事件处理程序 returns,它已完成需要完成的所有工作.由于您的事件处理程序在方法调用 await
之前将 CancelEventArgs.Cancel
设置为 true
,因此您的事件处理程序的调用者会看到它已设置为 true
,因此它不会'关闭表单。
这就是同步显示消息框的原因:整个方法在控制 returns 到调用者之前执行,因此 CancelEventArgs.Cancel
始终设置为其预期值。
Raymond Chen 最近发布了两篇关于 async
的文章,阅读起来可能很有趣:Crash course in async and await and The perils of async void。第二篇文章描述了为什么 async
事件处理程序往往无法按照您的预期工作。
在 OnClosing
中使用 async/await 的主要问题是,正如 Andy 所解释的那样,一旦执行 await 语句,控制权就会返回给调用者,并且关闭过程会继续。
我们可以通过在等待之后再次往返 OnClosing
来解决这个问题,这次用一个标志来指示是否实际关闭,但问题是调用 Close
虽然 Window 已经关闭,但不允许并抛出异常。
解决这个问题的方法是简单地将Close
的执行推迟到当前关闭过程之后,此时关闭window.[=17=再次生效。 ]
我想做这样的事情来允许用户处理 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 解决方案!