取消窗口关闭后再次尝试关闭引发InvalidOperationException异常
问题场景
定义了如下关闭接口:
public interface ICloseable { event EventHandler Closing; event EventHandler Closed; void Close(); }
ViewModel实现该接口:
public class TestViewModel : ICloseable { public event EventHandler Closing; public event EventHandler Closed; public void Close() { RaiseEvent(Closing); // do some cleaning... RaiseEvent(Closed); } protected virtual void RaiseEvent(EventHandler handler) { handler?.Invoke(this, null); } }
View中绑定ViewModel并处理关闭逻辑:
public partial class TestView : Window { private bool _contextClosed = false; // true if DataContext has been closed public TestView() { InitializeComponent(); var context = new TestViewModel(); Closing += TestView_Closing; DataContext = context; context.Closed += TestViewModel_Closed; } private void TestView_Closing(object sender, System.ComponentModel.CancelEventArgs e) { var context = DataContext as ICloseable; if (!_contextClosed) { e.Cancel = true; context.Close(); return; } } private void TestViewModel_Closed(object sender, EventArgs e) { _contextClosed = true; Application.Current.Dispatcher.Invoke(() => Close()); } private void TestButton_Click(object sender, RoutedEventArgs e) { Close(); } }
点击窗口标题栏的“X”或TestButton触发关闭时,会在TestViewModel_Closed的Application.Current.Dispatcher.Invoke(() => Close());行抛出异常:
System.InvalidOperationException: 'Cannot set Visibility to Visible or call Show, ShowDialog, Close, or WindowInteropHelper.EnsureHandle while a Window is closing.'
问题原因
虽然在TestView_Closing中设置了e.Cancel = true,但当前的Closing事件处理流程还未完全结束,窗口仍处于“正在关闭”的状态周期内。此时同步调用context.Close()会立即触发Closed事件,进而同步执行Dispatcher.Invoke里的Close()——这相当于在窗口Closing的过程中再次调用Close方法,WPF不允许这种操作,因此抛出异常。
简单来说:第一次Close触发的Closing事件还在处理中,就立刻发起了第二次Close调用,窗口状态未完成重置,导致冲突。
解决方案
核心思路是让第二次Close调用延迟到当前Closing事件处理完成之后执行,可通过以下两种方式实现:
方式1:异步触发ViewModel的Closed事件
修改TestViewModel的Close方法,用Dispatcher.BeginInvoke异步触发Closed事件,避免同步执行导致的状态冲突:
public void Close() { RaiseEvent(Closing); // do some cleaning... // 异步触发Closed事件,等待当前Closing流程结束后再执行后续逻辑 Application.Current.Dispatcher.BeginInvoke(() => RaiseEvent(Closed)); }
方式2:异步执行View的Close调用
修改TestViewModel_Closed方法,将Dispatcher.Invoke改为Dispatcher.BeginInvoke,把Close操作放到Dispatcher队列的后续执行:
private void TestViewModel_Closed(object sender, EventArgs e) { _contextClosed = true; // 延迟执行Close,让当前Closing事件处理完成后再执行 Application.Current.Dispatcher.BeginInvoke(() => Close()); }
两者的本质都是让第二次Close操作避开当前的Closing事件处理周期,等窗口状态重置为正常后再执行,即可解决异常。
内容的提问来源于stack exchange,提问作者AntikM

