如何在C#中正确调用并关闭WPF窗体以避免插件崩溃
问题描述
为某应用开发了.NET 4.8版本插件,该应用同时支持.NET 4.8和.NET 8,两个版本下均出现相同问题:此前用WinForms开发插件无异常,迁移至WPF后,首次打开插件窗体正常,但关闭后再次打开时主应用崩溃,抛出错误:
无法访问插件信息 [调用线程无法访问此对象,因为另一个线程拥有它。]
从主应用错误日志判断,问题出在尝试打开新窗体实例时,之前的窗体或其部分资源未完全释放。以下两种打开WPF窗体的代码均会导致该崩溃:
版本一
public class SendConfigurationFormx { public static void Create() { var thread = new Thread(() => { var window = new ConfigFormWPF { WindowStartupLocation = WindowStartupLocation.CenterScreen }; window.ShowDialog(); // 内部运行模态循环 }); thread.SetApartmentState(ApartmentState.STA); thread.Start(); thread.Join(); // 等待窗口关闭后再继续 } }
版本二
public class SendConfigurationForm { public static void Create() { var thread = new Thread(() => { if (Application.Current == null) { new Application(); } var window = new ConfigFormWPF { WindowStartupLocation = WindowStartupLocation.CenterScreen }; window.ShowDialog(); // 若要无模态可使用Show(),但需要Dispatcher循环 }); thread.SetApartmentState(ApartmentState.STA); thread.Start(); thread.Join(); // 等待窗口关闭后再继续 } }
无论是否保留thread.Join();,问题都无法解决。此前WinForms版本的代码无异常,代码如下:
public class SendConfigurationForm { [STAThread] public static void Create() { if (!FormHelper.BringToFront("JanJan ProfileFile")) { Application.EnableVisualStyles(); //Application.SetCompatibleTextRenderingDefault(false); var form = new ConfigurationForm(); var location = new Point(Screen.AllScreens[0].Bounds.X + 1000, 500); form.Location = Config.GenFormLocation("ConfigDefaultMonitor"); form.ShowDialog(); // Application.Run(new ConfigurationForm()); } } }
解决方案
问题根源
- WPF线程绑定特性:WPF控件和窗口实例严格绑定到创建它们的STA线程,线程结束后若未正确清理
Application对象或窗口资源,残留的Dispatcher线程资源会与新创建的线程/窗口产生冲突。 - 未终止WPF应用上下文:在独立线程中创建
Application但未调用Shutdown(),导致Application.Current残留,后续创建新窗口时复用错误的线程上下文。
修正后的代码方案
方案一:确保线程内的Application正确关闭
在窗口关闭后显式调用Application.Shutdown(),彻底清理线程内的WPF上下文:
public class SendConfigurationForm { public static void Create() { var thread = new Thread(() => { var app = new Application(); var window = new ConfigFormWPF { WindowStartupLocation = WindowStartupLocation.CenterScreen }; // 窗口关闭时触发Application关闭 window.Closed += (s, e) => app.Shutdown(); app.Run(window); // 用app.Run替代ShowDialog,确保上下文正确管理 }); thread.SetApartmentState(ApartmentState.STA); thread.Start(); thread.Join(); } }
方案二:复用主应用的Dispatcher(推荐,若主应用是WPF)
如果主应用本身是WPF应用,直接在主应用的UI线程上创建窗口,避免跨线程资源冲突:
public class SendConfigurationForm { public static void Create() { // 极端情况:主应用无WPF上下文,回退到独立线程方案 if (Application.Current == null) { var thread = new Thread(() => { var app = new Application(); var window = new ConfigFormWPF { WindowStartupLocation = WindowStartupLocation.CenterScreen }; window.Closed += (s, e) => app.Shutdown(); app.Run(window); }); thread.SetApartmentState(ApartmentState.STA); thread.Start(); thread.Join(); return; } // 不在UI线程则调度到UI线程执行 if (!Application.Current.Dispatcher.CheckAccess()) { Application.Current.Dispatcher.Invoke(Create); return; } // 避免重复打开同一窗口实例 var existingWindow = Application.Current.Windows.OfType<ConfigFormWPF>().FirstOrDefault(); if (existingWindow != null) { existingWindow.Activate(); return; } var window = new ConfigFormWPF { WindowStartupLocation = WindowStartupLocation.CenterScreen }; window.ShowDialog(); } }
额外注意事项
- 资源清理:在
ConfigFormWPF的Closed事件中,手动解除所有事件订阅、释放非托管资源,避免内存泄漏和残留引用。 - 跨线程操作:任何对窗口控件的后续操作必须通过
Dispatcher.Invoke调度到创建窗口的线程执行,禁止跨线程直接访问控件。
内容的提问来源于stack exchange,提问作者dandan21
相关产品推荐
相关产品推荐

