From 176952d52c7d62ffc79a6ffcd8b5b90d9c981fae Mon Sep 17 00:00:00 2001 From: SugarChes <103551815+SugarChes@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:03:44 +0800 Subject: [PATCH] fix(legacy): finalize hosted module lifecycle cleanup --- 插件库/Lskj.Control/GridControlEx.cs | 69 ++- .../Lskj.Control/Model/BandedGridDragGrid.cs | 154 ++++++- 插件库/Lskj.Control/Model/GridDragGrid.cs | 125 +++++- 插件库/Lskj.Control/Model/StaticControl.cs | 417 +++++++++++++++--- 插件库/Lskj.Control/TabControlEx.cs | 129 +++++- 5 files changed, 806 insertions(+), 88 deletions(-) diff --git a/插件库/Lskj.Control/GridControlEx.cs b/插件库/Lskj.Control/GridControlEx.cs index ce99427..9113d30 100644 --- a/插件库/Lskj.Control/GridControlEx.cs +++ b/插件库/Lskj.Control/GridControlEx.cs @@ -631,7 +631,15 @@ namespace Lskj.Control internal void ReleaseResourcesForDispose() { - DisposeGridResources(); + // 模块级清理只解除事件和菜单引用。GridControl/GridView 的生命周期 + // 由真正的 WinForms 父容器负责,并在 Dispose(bool) 中统一结束。 + DetachGridViewEventHandlers(); + if (gridViewRightMenu != null) + { + gridViewRightMenu.ReleaseResourcesForDispose(); + gridViewRightMenu = null; + } + ParentControl = null; } private void DetachGridViewEventHandlers() @@ -658,6 +666,13 @@ namespace Lskj.Control gridView.ShowFilterPopupCheckedListBox -= OnShowFilterPopupCheckedListBox; gridView.DataSourceChanged -= OnGridDataSourceChanged; gridView.RowCellStyle -= GridView_RowCellStyle; + // These handlers are attached dynamically by SetGridRowColors and + // AddHeadquarters. They are not part of the constructor hookup, so + // removing only the static handlers above leaves a GridView event + // delegate rooted to this GridControlEx after module close. + gridView.RowCellStyle -= OnGridViewRowCellStyle; + gridView.RowStyle -= OnGridViewRowStyle; + gridView.CellValueChanged -= onCellValueChanged; gridView.CustomRowFilter -= GridView_CustomRowFilter; gridView.EndGrouping -= GridView_EndGrouping; gridView.ShowingEditor -= GridView_ShowingEditor; @@ -735,6 +750,52 @@ namespace Lskj.Control customFont.Dispose(); customFont = null; } + + // Dispose 完成后,包装对象不应继续保留已释放的 DevExpress 控件、 + // 数据表和模块字典。尤其是左右联动事件链仍可能短暂持有包装对象, + // 这里主动断开引用可以避免已关闭页签继续保留整棵控件对象图。 + gridView = null; + gridControl = null; + GridRowColorsTable = null; + ChartTable = null; + rightMenuButtonTable = null; + InitGridColumsTab = null; + ModuleEnclosure?.Clear(); + ModuleEnclosure = null; + mControlSourceDic?.Clear(); + mControlSourceDic = null; + mControlList?.Clear(); + mControlList = null; + ColumnList?.Clear(); + ColumnList = null; + AutoPadDataReleColList?.Clear(); + AutoPadDataReleColList = null; + ImageList?.Clear(); + ImageList = null; + RepeatedVerificationNames?.Clear(); + RepeatedVerificationNames = null; + PromptMessage?.Clear(); + PromptMessage = null; + _CheckedList?.Clear(); + _CheckedList = null; + modifyThePosition?.Clear(); + modifyThePosition = null; + ColumnsList?.Clear(); + ColumnsList = null; + SelectedDataSource?.Clear(); + SelectedDataSource = null; + ProgressBarColumn?.Clear(); + ProgressBarColumn = null; + ManuallyAddData?.Clear(); + ManuallyAddData = null; + FrozenColumns = null; + CurrentOperGridView = null; + CurrentOperModel = null; + HeaderColumnSift = null; + sumPanel = null; + PopupEdit = null; + CustomGroupBandEx = null; + CustomGroupTreeBandEx = null; } private void PrepareForColumnRebuild() @@ -3825,7 +3886,7 @@ namespace Lskj.Control DllName = "", Id = this.Model.ModuleCode, Name = this.Model.FormText, - BeforePage = ERPInfo.Instance.PageControl.SelectedTabPage + BeforePage = ERPInfo.Instance.CurrentModulePage }; module.ModuleForm = frmGridSift; string guid = Guid.NewGuid().ToString(); @@ -3842,8 +3903,8 @@ namespace Lskj.Control tp.Controls.Add(frmGridSift); tp.ShowCloseButton = DefaultBoolean.True; tp.Dock = DockStyle.Fill; - ERPInfo.Instance.PageControl.TabPages.Add(tp); - ERPInfo.Instance.PageControl.SelectedTabPage = tp; + ERPInfo.Instance.AddModulePage(tp); + ERPInfo.Instance.CurrentModulePage = tp; ERPInfo.Instance.ModuleForms.Add(guid, module); frmGridSift.Show(); } diff --git a/插件库/Lskj.Control/Model/BandedGridDragGrid.cs b/插件库/Lskj.Control/Model/BandedGridDragGrid.cs index 1144e06..81240ea 100644 --- a/插件库/Lskj.Control/Model/BandedGridDragGrid.cs +++ b/插件库/Lskj.Control/Model/BandedGridDragGrid.cs @@ -44,19 +44,39 @@ namespace Lskj.Control.Model Dictionary> registrations, BandedGridView gridView) { + if (registrations == null) + return; + foreach (KeyValuePair> entry in registrations.ToList()) { bool removeKey = ReferenceEquals(entry.Key, gridView); - foreach (BandedGridDragGrid registration in entry.Value.ToList()) + List registrationsForView = entry.Value; + if (registrationsForView == null) { - if (removeKey || registration == null || registration._disposed || registration.References(gridView)) + registrations.Remove(entry.Key); + continue; + } + + foreach (BandedGridDragGrid registration in registrationsForView.ToList()) + { + if (removeKey || registration == null || registration._disposed || + registration.References(gridView)) { if (registration != null) - registration.Dispose(); - entry.Value.Remove(registration); + { + try + { + registration.Dispose(); + } + catch (Exception exception) + { + System.Diagnostics.Debug.WriteLine("清理带状表格拖拽注册失败:" + exception); + } + } + registrationsForView.Remove(registration); } } - if (removeKey || entry.Value.Count == 0) + if (removeKey || registrationsForView.Count == 0) registrations.Remove(entry.Key); } } @@ -65,23 +85,71 @@ namespace Lskj.Control.Model Dictionary> registrations, GridView gridView) { + if (registrations == null) + return; + foreach (KeyValuePair> entry in registrations.ToList()) { bool removeKey = ReferenceEquals(entry.Key, gridView); - foreach (BandedGridDragGrid registration in entry.Value.ToList()) + List registrationsForView = entry.Value; + if (registrationsForView == null) { - if (removeKey || registration == null || registration._disposed || registration.References(gridView)) + registrations.Remove(entry.Key); + continue; + } + + foreach (BandedGridDragGrid registration in registrationsForView.ToList()) + { + if (removeKey || registration == null || registration._disposed || + registration.References(gridView)) { if (registration != null) - registration.Dispose(); - entry.Value.Remove(registration); + { + try + { + registration.Dispose(); + } + catch (Exception exception) + { + System.Diagnostics.Debug.WriteLine("清理带状表格拖拽注册失败:" + exception); + } + } + registrationsForView.Remove(registration); } } - if (removeKey || entry.Value.Count == 0) + if (removeKey || registrationsForView.Count == 0) registrations.Remove(entry.Key); } } + private static bool IsDisposedView(BandedGridView view) + { + if (view == null) + return true; + try + { + return view.GridControl == null || view.GridControl.IsDisposed; + } + catch + { + return true; + } + } + + private static bool IsDisposedView(GridView view) + { + if (view == null) + return true; + try + { + return view.GridControl == null || view.GridControl.IsDisposed; + } + catch + { + return true; + } + } + internal bool References(GridView gridView) { return gridView != null && @@ -124,8 +192,14 @@ namespace Lskj.Control.Model /// 目标GridView. public BandedGridDragGrid(BandedGridView sourceView, GridView targetView) { + // Do not retain an incomplete registration in the global dictionaries. + if (IsDisposedView(sourceView) || IsDisposedView(targetView)) + return; + this._sourceGridView = sourceView; this._targetGridView = targetView; + try + { //记录所有拖拽类到来源表中,后每次进入目标表禁用其他拖拽类的拖拽事件 if (StaticBandedControl.BandedGridViewDragGridDic.ContainsKey(sourceView)) { @@ -171,6 +245,21 @@ namespace Lskj.Control.Model { this._targetGridView.Disposed += OnGridViewDisposed; } + } + catch + { + // Roll back dictionary entries and event handlers from a partial + // registration before surfacing the original failure. + try + { + Dispose(); + } + catch (Exception exception) + { + System.Diagnostics.Debug.WriteLine("回滚带状表格拖拽注册失败:" + exception); + } + throw; + } } private void OnGridViewDisposed(object sender, EventArgs e) @@ -225,7 +314,8 @@ namespace Lskj.Control.Model if (ReferenceEquals( StaticBandedControl.SourceDragBandedGridView, - sourceGridView)) + sourceGridView) && + !HasRegistrationForView(sourceGridView)) { StaticBandedControl.SourceDragBandedGridView = null; } @@ -237,6 +327,37 @@ namespace Lskj.Control.Model _targetGridView = null; } + private static bool HasRegistrationForView(BandedGridView gridView) + { + if (gridView == null) + return false; + + List items; + if (StaticBandedControl.BandedGridViewDragGridDic != null && + StaticBandedControl.BandedGridViewDragGridDic.TryGetValue(gridView, out items) && + ContainsLiveRegistration(items, gridView)) + return true; + + if (StaticBandedControl.BandedTargetViewDragGridDic != null) + foreach (KeyValuePair> entry in + StaticBandedControl.BandedTargetViewDragGridDic) + if (ContainsLiveRegistration(entry.Value, gridView)) + return true; + return false; + } + + private static bool ContainsLiveRegistration( + IEnumerable registrations, + GridView gridView) + { + if (registrations == null || gridView == null) + return false; + foreach (BandedGridDragGrid item in registrations) + if (item != null && !item._disposed && item.References(gridView)) + return true; + return false; + } + private static void RemoveRegistration( Dictionary> registrations, TKey key, @@ -248,12 +369,23 @@ namespace Lskj.Control.Model return; } + if (registrations == null) + { + return; + } + List registrationsForView; if (!registrations.TryGetValue(key, out registrationsForView)) { return; } + if (registrationsForView == null) + { + registrations.Remove(key); + return; + } + registrationsForView.Remove(registration); if (registrationsForView.Count == 0) { diff --git a/插件库/Lskj.Control/Model/GridDragGrid.cs b/插件库/Lskj.Control/Model/GridDragGrid.cs index 282f152..e634681 100644 --- a/插件库/Lskj.Control/Model/GridDragGrid.cs +++ b/插件库/Lskj.Control/Model/GridDragGrid.cs @@ -46,23 +46,62 @@ namespace Lskj.Control.Model Dictionary> registrations, GridView gridView) { + if (registrations == null) + return; + + // A module-close operation must only remove registrations that + // reference the exact view being closed. Never infer ownership + // from GridControl.IsDisposed here: another open tab can be in a + // transient initialization/dispose state and its registration + // must remain untouched. foreach (KeyValuePair> entry in registrations.ToList()) { bool removeKey = ReferenceEquals(entry.Key, gridView); - foreach (GridDragGrid registration in entry.Value.ToList()) + List registrationsForView = entry.Value; + if (registrationsForView == null) { - if (removeKey || registration == null || registration._disposed || registration.References(gridView)) + registrations.Remove(entry.Key); + continue; + } + + foreach (GridDragGrid registration in registrationsForView.ToList()) + { + if (removeKey || registration == null || registration._disposed || + registration.References(gridView)) { if (registration != null) - registration.Dispose(); - entry.Value.Remove(registration); + { + try + { + registration.Dispose(); + } + catch (Exception exception) + { + System.Diagnostics.Debug.WriteLine("清理表格拖拽注册失败:" + exception); + } + } + registrationsForView.Remove(registration); } } - if (removeKey || entry.Value.Count == 0) + if (removeKey || registrationsForView.Count == 0) registrations.Remove(entry.Key); } } + private static bool IsDisposedView(GridView view) + { + if (view == null) + return true; + try + { + return view.GridControl == null || view.GridControl.IsDisposed; + } + catch + { + return true; + } + } + internal bool References(GridView gridView) { return gridView != null && @@ -115,8 +154,16 @@ namespace Lskj.Control.Model /// 目标GridView. public GridDragGrid(GridView sourceView, GridView targetView) { + // Do not put an incomplete registration in the global dictionaries. + // A GridView without an owner GridControl cannot receive the drag + // events below and would otherwise remain a strong static reference. + if (IsDisposedView(sourceView) || IsDisposedView(targetView)) + return; + this._sourceGridView = sourceView; this._targetGridView = targetView; + try + { //记录所有拖拽类到来源表中,后每次进入目标表禁用其他拖拽类的拖拽事件 if (StaticControl.GridViewDragGridDic.ContainsKey(sourceView)) { @@ -162,6 +209,21 @@ namespace Lskj.Control.Model { this._targetGridView.Disposed += OnGridViewDisposed; } + } + catch + { + // Registration is a two-dictionary operation. If event wiring + // fails, undo everything that was attached before rethrowing. + try + { + Dispose(); + } + catch (Exception exception) + { + System.Diagnostics.Debug.WriteLine("回滚表格拖拽注册失败:" + exception); + } + throw; + } } private void OnGridViewDisposed(object sender, EventArgs e) @@ -214,7 +276,8 @@ namespace Lskj.Control.Model targetGridView, this); - if (ReferenceEquals(StaticControl.SourceDragGridView, sourceGridView)) + if (ReferenceEquals(StaticControl.SourceDragGridView, sourceGridView) && + !HasRegistrationForView(sourceGridView)) { StaticControl.SourceDragGridView = null; } @@ -226,6 +289,45 @@ namespace Lskj.Control.Model _targetGridView = null; } + private static bool HasRegistrationForView(GridView gridView) + { + if (gridView == null) + return false; + + return HasRegistration(StaticControl.GridViewDragGridDic, gridView) || + HasRegistration(StaticControl.TargetViewDragGridDic, gridView); + } + + internal static void ClearSourceReferenceIfUnused(GridView gridView) + { + if (gridView != null && + ReferenceEquals(StaticControl.SourceDragGridView, gridView) && + !HasRegistrationForView(gridView)) + { + StaticControl.SourceDragGridView = null; + } + } + + private static bool HasRegistration( + Dictionary> registrations, + GridView gridView) + where TKey : class + { + if (registrations == null || gridView == null) + return false; + + foreach (KeyValuePair> entry in registrations) + { + List items = entry.Value; + if (items == null) + continue; + foreach (GridDragGrid item in items) + if (item != null && !item._disposed && item.References(gridView)) + return true; + } + return false; + } + private static void RemoveRegistration( Dictionary> registrations, TKey key, @@ -237,12 +339,23 @@ namespace Lskj.Control.Model return; } + if (registrations == null) + { + return; + } + List registrationsForView; if (!registrations.TryGetValue(key, out registrationsForView)) { return; } + if (registrationsForView == null) + { + registrations.Remove(key); + return; + } + registrationsForView.Remove(registration); if (registrationsForView.Count == 0) { diff --git a/插件库/Lskj.Control/Model/StaticControl.cs b/插件库/Lskj.Control/Model/StaticControl.cs index 412fb15..315c2cb 100644 --- a/插件库/Lskj.Control/Model/StaticControl.cs +++ b/插件库/Lskj.Control/Model/StaticControl.cs @@ -1,4 +1,5 @@ using DevExpress.XtraGrid.Views.Grid; +using DevExpress.XtraGrid.Views.BandedGrid; using DevExpress.XtraTab; using Lskj.Control.ZKFinger; using Lskj.Util; @@ -8,7 +9,9 @@ using System.Collections.Generic; using System.Data; using System.Diagnostics; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; +using System.Threading; using System.Windows.Forms; using Lskj.Model; using System.Reflection; @@ -18,24 +21,112 @@ namespace Lskj.Control.Model { public static class StaticControl { + /// + /// 在窗体关闭前捕获模块控件树及其 GridView。WinForms 的 Close/Dispose + /// 流程可能会先清空 Controls 或重建 GridView;保留这个快照可以让后续 + /// 引用解绑仍然针对原始实例执行。 + /// + public sealed class ModuleReferenceSnapshot + { + internal readonly List Controls; + internal readonly List GridViews; + + internal ModuleReferenceSnapshot( + List controls, + List gridViews) + { + Controls = controls ?? new List(); + GridViews = gridViews ?? new List(); + } + } + + public static ModuleReferenceSnapshot CaptureModuleReferences( + WinFormsControl moduleRoot, + XtraTabPage modulePage) + { + List moduleControls = new List(); + try + { + moduleControls = CollectModuleControls(moduleRoot, modulePage); + } + catch (Exception exception) + { + TraceCleanupFailure("收集模块控件引用", exception); + } + + List gridViews = new List(); + try + { + gridViews = CollectGridViews(moduleControls); + } + catch (Exception exception) + { + TraceCleanupFailure("收集模块表格引用", exception); + } + return new ModuleReferenceSnapshot(moduleControls, gridViews); + } + /// /// Releases static references owned by a closed legacy module. Cleanup is /// best-effort so it cannot change the module close result. /// public static void ReleaseModuleReferences(WinFormsControl moduleRoot, XtraTabPage modulePage, string moduleCode) { - List gridViews = new List(); + ReleaseModuleReferences( + CaptureModuleReferences(moduleRoot, modulePage), + modulePage, + moduleCode); + } + + /// + /// Releases references using a snapshot captured before Close/Dispose. + /// This overload is used by the WPF host, where the legacy form can be + /// detached from its Controls collection during Form.Close. + /// + public static void ReleaseModuleReferences( + ModuleReferenceSnapshot snapshot, + XtraTabPage modulePage, + string moduleCode) + { + List moduleControls = snapshot == null || snapshot.Controls == null + ? new List() + : snapshot.Controls; + List gridViews = snapshot == null || snapshot.GridViews == null + ? new List() + : snapshot.GridViews; + List tabControls = new List(); try { - gridViews = CollectGridViews(moduleRoot); + tabControls = moduleControls.OfType().ToList(); } catch (Exception exception) { - TraceCleanupFailure("收集表格引用", exception); + TraceCleanupFailure("收集模块引用", exception); } try { + // WPF 承载旧 WinForms 页面时,移除页签不一定会同步触发 + // TabControlEx.Disposed。先解除左右联动事件,避免 GridView + // 的事件委托继续闭包持有已经关闭的 TabControlEx/模块。 + foreach (TabControlEx tabControl in tabControls) + { + try + { + tabControl?.ReleaseRuntimeReferences(); + } + catch (Exception exception) + { + TraceCleanupFailure("释放多页签运行时引用", exception); + } + } + + // GridControlEx 自身也会把事件处理器、右键菜单和父控件 + // 引用挂在 GridView/菜单对象上。这里只处理关闭前快照中的 + // 控件,不调用 Control.Dispose,也不扫描全局控件集合,因而 + // 不会影响仍然打开的模块或共享的 GridView 生命周期。 + ReleaseGridControlRuntimeReferences(moduleControls); + foreach (GridView gridView in gridViews) { try @@ -48,18 +139,27 @@ namespace Lskj.Control.Model } } - TryCleanup(() => RemoveConditionPanels(moduleCode), "释放条件面板引用"); - TryCleanup(() => RemoveProtectedReferences(moduleRoot, modulePage), "释放权限保护引用"); - TryCleanup(() => ClearRightMenuReferences(moduleRoot, gridViews), "释放右键菜单引用"); TryCleanup( - () => StaticBandedControl.ReleaseModuleReferences(moduleRoot, modulePage, moduleCode), + () => RemoveConditionPanels(moduleCode, moduleControls), + "释放条件面板引用"); + TryCleanup( + () => RemoveProtectedReferences(moduleControls, modulePage), + "释放权限保护引用"); + TryCleanup( + () => ClearRightMenuReferences(moduleControls, gridViews), + "释放右键菜单引用"); + TryCleanup( + () => StaticBandedControl.ReleaseModuleReferences(moduleControls, modulePage, moduleCode), "释放带状表格引用"); } finally { - // DynamicModel caches retain completed tasks, result tables and - // their closures. Always release them after other references. - TryCleanup(() => ClearDataCaches(moduleRoot), "释放动态数据缓存"); + // Only remove cache entries owned by this module. The cached + // values are owned by their module/control and must not be + // disposed from a global static cleanup routine. + TryCleanup( + () => RemoveModuleCacheReferences(moduleControls, gridViews), + "移除动态数据缓存引用"); } } @@ -80,21 +180,104 @@ namespace Lskj.Control.Model Debug.WriteLine("释放旧模块引用失败[" + operation + "]:" + exception); } - private static List CollectGridViews(WinFormsControl root) + private static void ReleaseGridControlRuntimeReferences( + IEnumerable moduleControls) + { + if (moduleControls == null) + return; + + HashSet released = + new HashSet(ReferenceEqualityComparer.Instance); + foreach (GridControlEx gridControl in moduleControls.OfType()) + { + if (gridControl == null || !released.Add(gridControl)) + continue; + + try + { + gridControl.ReleaseResourcesForDispose(); + } + catch (Exception exception) + { + TraceCleanupFailure("释放表格控件运行时引用", exception); + } + } + } + + private static List CollectModuleControls( + WinFormsControl moduleRoot, + XtraTabPage modulePage) + { + List result = new List(); + AddModuleControls(result, moduleRoot); + if (!ReferenceEquals(moduleRoot, modulePage)) + AddModuleControls(result, modulePage); + return result; + } + + private static void AddModuleControls( + List target, + WinFormsControl root) + { + if (root == null) + return; + + foreach (WinFormsControl control in EnumerateControls(root)) + if (!ContainsControl(target, control)) + target.Add(control); + } + + private static List CollectGridViews(IEnumerable controls) { List result = new List(); - if (root == null) + if (controls == null) return result; - foreach (WinFormsControl control in EnumerateControls(root)) + + foreach (WinFormsControl control in controls) { DevExpress.XtraGrid.GridControl grid = control as DevExpress.XtraGrid.GridControl; - GridView view = grid == null ? null : grid.MainView as GridView; - if (view != null && !result.Contains(view)) - result.Add(view); + if (grid == null) + continue; + + try + { + GridView mainView = grid.MainView as GridView; + AddGridView(result, mainView); + + // A GridControl can retain detail views in ViewCollection even + // when they are not the current MainView. + foreach (DevExpress.XtraGrid.Views.Base.BaseView baseView in grid.ViewCollection) + AddGridView(result, baseView as GridView); + } + catch (Exception exception) + { + // A form can already be partially disposed when the shell + // releases its static references. Keep the views collected + // from the remaining controls. + TraceCleanupFailure("读取表格视图", exception); + } } return result; } + private static void AddGridView(List target, GridView view) + { + if (view != null && !ContainsGridView(target, view)) + target.Add(view); + } + + private static bool ContainsGridView( + IEnumerable gridViews, + GridView value) + { + if (gridViews == null || value == null) + return false; + foreach (GridView gridView in gridViews) + if (ReferenceEquals(gridView, value)) + return true; + return false; + } + private static IEnumerable EnumerateControls(WinFormsControl root) { yield return root; @@ -109,114 +292,222 @@ namespace Lskj.Control.Model return; GridDragGrid.DisposeRegistrations(GridViewDragGridDic, TargetViewDragGridDic, gridView); - if (ReferenceEquals(SourceDragGridView, gridView) || - (SourceDragGridView != null && SourceDragGridView.GridControl != null && SourceDragGridView.GridControl.IsDisposed)) - SourceDragGridView = null; - if (ReferenceEquals(_RightMenuGridView, gridView) || - (_RightMenuGridView != null && _RightMenuGridView.GridControl != null && _RightMenuGridView.GridControl.IsDisposed)) + // BandedGridDragGrid uses a separate source dictionary but shares + // the GridView target dictionary. Clean both sides whenever a + // GridView is released so control-level Dispose also removes + // banded drag helpers without disposing the view itself. + BandedGridDragGrid.DisposeRegistrations( + StaticBandedControl.BandedGridViewDragGridDic, + StaticBandedControl.BandedTargetViewDragGridDic, + gridView as BandedGridView, + gridView); + GridDragGrid.ClearSourceReferenceIfUnused(gridView); + if (ReferenceEquals(_RightMenuGridView, gridView)) RightMenuGridView = null; } - private static void RemoveConditionPanels(string moduleCode) + private static void RemoveConditionPanels( + string moduleCode, + IEnumerable moduleControls) { foreach (string key in ConditionsPanelDic.Keys.ToList()) { ModuleConditionsPanelEx panel; - if (string.Equals(key, moduleCode, StringComparison.OrdinalIgnoreCase) || - !ConditionsPanelDic.TryGetValue(key, out panel) || panel == null || panel.IsDisposed) + if (!ConditionsPanelDic.TryGetValue(key, out panel) || panel == null || panel.IsDisposed) + { + ConditionsPanelDic.Remove(key); + continue; + } + + // The dictionary is keyed only by module code. Two instances of + // the same module can therefore share that key. Remove a live + // panel only when its owner can be tied to the closing instance; + // otherwise leave it for the still-open module. + if (string.Equals(key, moduleCode, StringComparison.OrdinalIgnoreCase) && + IsConditionPanelOwnedByModule(panel, moduleControls)) ConditionsPanelDic.Remove(key); } } - private static void RemoveProtectedReferences(WinFormsControl root, XtraTabPage page) + private static bool IsConditionPanelOwnedByModule( + ModuleConditionsPanelEx panel, + IEnumerable moduleControls) + { + if (panel == null) + return false; + if (ContainsControl(moduleControls, panel) || ContainsControl(moduleControls, panel.Owner)) + return true; + + MyControl parentControl = panel.ParentControlObj; + return parentControl != null && + parentControl.MainGridEx != null && + ContainsControl(moduleControls, parentControl.MainGridEx); + } + + private static void RemoveProtectedReferences( + IEnumerable moduleControls, + XtraTabPage page) { while (page != null && DogVerifyModuleForms.Remove(page)) { } foreach (XtraTabPage item in DogVerifyModuleForms.ToList()) - if (item == null || item.IsDisposed || IsContained(root, item)) + if (item == null || item.IsDisposed || ContainsControl(moduleControls, item)) DogVerifyModuleForms.Remove(item); foreach (IForm item in DogVerifyNoPageForms.ToList()) - if (item == null || item.SubForm == null || item.SubForm.IsDisposed || IsContained(root, item.SubForm)) + if (item == null || item.SubForm == null || item.SubForm.IsDisposed || + ContainsControl(moduleControls, item.SubForm)) DogVerifyNoPageForms.Remove(item); } - private static bool IsContained(WinFormsControl root, WinFormsControl value) + private static bool ContainsControl( + IEnumerable moduleControls, + WinFormsControl value) { - if (root == null || value == null) + if (moduleControls == null || value == null) return false; - if (ReferenceEquals(root, value)) - return true; - foreach (WinFormsControl child in root.Controls) - if (IsContained(child, value)) + foreach (WinFormsControl control in moduleControls) + if (ReferenceEquals(control, value)) return true; return false; } - private static void ClearRightMenuReferences(WinFormsControl root, List gridViews) + private static void ClearRightMenuReferences( + IEnumerable moduleControls, + List gridViews) { - if (_RightMenuGridView != null && (gridViews.Contains(_RightMenuGridView) || - (_RightMenuGridView.GridControl != null && _RightMenuGridView.GridControl.IsDisposed))) + if (_RightMenuGridView != null && ContainsGridView(gridViews, _RightMenuGridView)) RightMenuGridView = null; if (RightMenuMyControl != null && RightMenuMyControl.MainGridEx != null && - (RightMenuMyControl.MainGridEx.IsDisposed || IsContained(root, RightMenuMyControl.MainGridEx))) + (RightMenuMyControl.MainGridEx.IsDisposed || + ContainsControl(moduleControls, RightMenuMyControl.MainGridEx))) RightMenuMyControl = null; - if (BomUnionPage != null && (BomUnionPage.IsDisposed || IsContained(root, BomUnionPage))) + if (BomUnionPage != null && + (BomUnionPage.IsDisposed || ContainsControl(moduleControls, BomUnionPage))) BomUnionPage = null; - if (AddParentGrid.Value != null && (AddParentGrid.Value.IsDisposed || IsContained(root, AddParentGrid.Value))) + if (AddParentGrid.Value != null && + (AddParentGrid.Value.IsDisposed || ContainsControl(moduleControls, AddParentGrid.Value))) AddParentGrid = new KeyValuePair(); } - private static void ClearDataCaches(WinFormsControl root) + private static void RemoveModuleCacheReferences( + IEnumerable moduleControls, + IEnumerable gridViews) { - if (root == null) + if (moduleControls == null) return; - // Clear the root first because child enumeration can fail while a - // DevExpress control is disposing its child collection. - ClearDynamicModels(root); - try + HashSet moduleReferences = new HashSet(ReferenceEqualityComparer.Instance); + foreach (WinFormsControl control in moduleControls) + if (control != null) + moduleReferences.Add(control); + if (gridViews != null) + foreach (GridView gridView in gridViews) + if (gridView != null) + moduleReferences.Add(gridView); + + HashSet> moduleCaches = + new HashSet>(ReferenceEqualityComparer>.Instance); + HashSet moduleModels = + new HashSet(ReferenceEqualityComparer.Instance); + foreach (WinFormsControl control in moduleControls.ToList()) { - foreach (WinFormsControl control in EnumerateControls(root).Skip(1).ToList()) - ClearDynamicModels(control); + try + { + CollectDynamicModels(control, moduleCaches, moduleModels); + } + catch (Exception exception) + { + TraceCleanupFailure("收集动态缓存引用", exception); + } } - catch (Exception exception) + + foreach (DynamicModel model in moduleModels) + if (model != null) + moduleReferences.Add(model); + + // A DataCaches dictionary is normally module-local, but the legacy + // code can share one dictionary between the form, its child controls + // and model objects. Never clear a whole dictionary here: a module. + // can be opening concurrently and may not have been registered in + // ERPInfo.ModuleForms yet. Removing only keys that belong to the + // closing controls/views keeps every other module's cache intact. + foreach (Dictionary caches in moduleCaches) { - TraceCleanupFailure("遍历动态数据缓存", exception); + try + { + RemoveModuleCacheEntries(caches, moduleReferences); + } + catch (Exception exception) + { + TraceCleanupFailure("移除动态缓存引用", exception); + } } + + foreach (DynamicModel model in moduleModels) + if (model != null && model.DataCaches != null) + model.DataCaches = null; } - private static void ClearDynamicModels(object instance) + private static void CollectDynamicModels( + object instance, + HashSet> caches, + HashSet models) { - if (instance == null) + if (instance == null || caches == null || models == null) return; BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; for (Type type = instance.GetType(); type != null; type = type.BaseType) foreach (FieldInfo field in type.GetFields(flags | BindingFlags.DeclaredOnly)) try { - ClearDynamicModel(field.GetValue(instance)); + DynamicModel model = field.GetValue(instance) as DynamicModel; + if (model == null || model.DataCaches == null) + continue; + + caches.Add(model.DataCaches); + models.Add(model); } catch (Exception exception) { - Debug.WriteLine("清理动态缓存字段失败:" + exception); + Debug.WriteLine("收集动态缓存字段失败:" + exception); } } - private static void ClearDynamicModel(object value) + private static void RemoveModuleCacheEntries( + Dictionary caches, + HashSet moduleReferences) { - DynamicModel model = value as DynamicModel; - if (model == null || model.DataCaches == null) + if (caches == null || moduleReferences == null || moduleReferences.Count == 0) return; - Dictionary caches = model.DataCaches; - foreach (Hashtable cache in caches.Values.OfType().ToList()) + + // DataCaches is shared by several controls in a module. Remove only + // entries keyed by controls/views in the closing module and leave + // all cached values untouched. Their actual owner decides when and + // how to dispose them. + lock (caches) { - foreach (IDisposable item in cache.Values.OfType().ToList()) - try { item.Dispose(); } catch { } - cache.Clear(); + foreach (object key in caches.Keys.Cast().ToList()) + if (moduleReferences.Contains(key)) + caches.Remove(key); + } + } + + private sealed class ReferenceEqualityComparer : IEqualityComparer + where T : class + { + public static readonly ReferenceEqualityComparer Instance = + new ReferenceEqualityComparer(); + + public bool Equals(T x, T y) + { + return ReferenceEquals(x, y); + } + + public int GetHashCode(T value) + { + return value == null ? 0 : RuntimeHelpers.GetHashCode(value); } - caches.Clear(); - model.DataCaches = null; } /// /// 记录当前U盾权限窗口 diff --git a/插件库/Lskj.Control/TabControlEx.cs b/插件库/Lskj.Control/TabControlEx.cs index c2ce9af..b1ed64d 100644 --- a/插件库/Lskj.Control/TabControlEx.cs +++ b/插件库/Lskj.Control/TabControlEx.cs @@ -160,6 +160,19 @@ namespace Lskj.Control /// private readonly HashSet _rightTabRefreshRegisteredGrids = new HashSet(); /// + /// 右侧联动事件委托。必须保存委托实例,Dispose 时才能从 GridView 正确解绑。 + /// 匿名 lambda 如果不保存实例,会把 TabControlEx 通过事件闭包继续挂在已关闭的表格上。 + /// + private readonly Dictionary _rightTabRefreshHandlers = + new Dictionary(); + /// + /// 保存注册事件时的 GridView 实例。GridControlEx 在 Dispose 过程中可能 + /// 清空或替换 GridView 属性;解绑时必须使用当初注册事件的同一个实例, + /// 不能再次通过 GridControlEx.GridView 取值。 + /// + private readonly Dictionary _rightTabRefreshHandlerViews = + new Dictionary(); + /// /// 左右结构中的右侧页签集合。 /// 只有这些页签会根据数据行数禁用 ModuleGridEx 底部操作栏。 /// @@ -435,9 +448,14 @@ namespace Lskj.Control if (leftGrid == null || rightTab == null) return; _rightTabsByLeftGrid[leftGrid] = rightTab; - if (!_rightTabRefreshRegisteredGrids.Contains(leftGrid)) + GridView leftView = leftGrid.GridView; + if (!_rightTabRefreshRegisteredGrids.Contains(leftGrid) && leftView != null) { - leftGrid.GridView.FocusedRowObjectChanged += (s, e) => RefreshRightTabAfterLeftBind(leftGrid); + DevExpress.XtraGrid.Views.Base.FocusedRowObjectChangedEventHandler handler = + (s, e) => RefreshRightTabAfterLeftBind(leftGrid); + leftView.FocusedRowObjectChanged += handler; + _rightTabRefreshHandlers[leftGrid] = handler; + _rightTabRefreshHandlerViews[leftGrid] = leftView; _rightTabRefreshRegisteredGrids.Add(leftGrid); } RefreshRightTab(rightTab, leftGrid);// 首次加载 @@ -480,18 +498,30 @@ namespace Lskj.Control { if (ParentGridEx != null && ParentGridEx.CustomGroupBandEx != null) { + ParentGridEx.CustomGroupBandEx.BandedView.LeftCoordChanged -= OnBandedViewLeftCoordChanged; + ParentGridEx.CustomGroupBandEx.BandedView.ColumnWidthChanged -= OnBandedViewColumnWidthChanged; + ParentGridEx.OnBuildGroupedColumnCallBack -= OnBandedViewColumnWidthChanged; ParentGridEx.CustomGroupBandEx.BandedView.LeftCoordChanged += OnBandedViewLeftCoordChanged; ParentGridEx.CustomGroupBandEx.BandedView.ColumnWidthChanged += OnBandedViewColumnWidthChanged; ParentGridEx.OnBuildGroupedColumnCallBack += OnBandedViewColumnWidthChanged; } if (ParentGridEx != null && ParentGridEx.CustomGroupTreeBandEx != null) { + ParentGridEx.CustomGroupTreeBandEx.TreeListObj.LeftCoordChanged -= OnBandedViewLeftCoordChanged; + ParentGridEx.CustomGroupTreeBandEx.TreeListObj.ColumnWidthChanged -= OnBandedViewColumnWidthChanged; + ParentGridEx.OnBuildGroupedColumnCallBack -= OnBandedViewColumnWidthChanged; ParentGridEx.CustomGroupTreeBandEx.TreeListObj.LeftCoordChanged += OnBandedViewLeftCoordChanged; ; ParentGridEx.CustomGroupTreeBandEx.TreeListObj.ColumnWidthChanged += OnBandedViewColumnWidthChanged; ParentGridEx.OnBuildGroupedColumnCallBack += OnBandedViewColumnWidthChanged; } this._gridDetail = gridDetail; - if (!isAddPages) this.xtc_container.TabPages.Clear(); + if (!isAddPages) + { + // 重建明细页签前先解除旧左右联动关系。仅清空 TabPages 不会移除 + // GridView 上的事件,因此旧页签仍可能通过事件闭包存活。 + DetachRightTabRefreshHandlers(); + this.xtc_container.TabPages.Clear(); + } // 1️⃣ 预分组:Rightdockid > 0 的明细 var rightGroups = gridDetail @@ -549,7 +579,12 @@ namespace Lskj.Control rightTab.ShowTabHeader = DevExpress.Utils.DefaultBoolean.False; - GridControlEx leftControl = new GridControlEx(); + // Only reuse an existing grid from the left detail. The + // previous unconditional `new GridControlEx()` created an + // orphan control whenever leftCtrl was a non-grid control; + // it was never added to the visual tree or disposed, and + // its GridView event handlers kept the whole wrapper alive. + GridControlEx leftControl = null; if (leftCtrl is GridControlEx left1) { leftControl = left1; @@ -646,6 +681,7 @@ namespace Lskj.Control this._gridDetail = gridDetail; if (!isAddPages) { + DetachRightTabRefreshHandlers(); this.xtc_container.TabPages.Clear(); } string DetaiName = string.Empty; @@ -1865,6 +1901,7 @@ namespace Lskj.Control public void InitTabPages(List gridDetail, string parentValue) { this._gridDetail = gridDetail; + DetachRightTabRefreshHandlers(); this.xtc_container.TabPages.Clear(); bool ProcessExists = (Lskj.Data.Caches.CacheSYSConfig.Instance().JudgeProcessExists() && SystemInfo.Instance.isExecload); @@ -2685,8 +2722,92 @@ namespace Lskj.Control /// private void OnTabControlExDisposed(object sender, EventArgs e) { + ReleaseRuntimeReferences(); + } + + /// + /// 在模块关闭但控件尚未进入 WinForms Dispose 流程时,主动解除 + /// TabControlEx 持有的运行时引用。这里只解除事件和清空本控件的 + /// 映射,不销毁共享的 GridControl/GridView。 + /// + internal void ReleaseRuntimeReferences() + { + DetachRightTabRefreshHandlers(); + DetachParentGridHandlers(); PageControlsDic?.Clear(); DetailTabPagesDic?.Clear(); + _gridDetail?.Clear(); + _gridIntoDetail?.Clear(); + ParentGridEx = null; + MrpParentGridEx = null; + MrpDetailGridEx = null; + ParentControlEx = null; + ModuleGridObj = null; + ReplaceControlEx = null; + } + + /// + /// 解除左右联动刷新事件。事件委托由字典保存,保证能够与注册时使用同一个实例。 + /// + private void DetachRightTabRefreshHandlers() + { + foreach (KeyValuePair item in + _rightTabRefreshHandlers.ToArray()) + { + try + { + GridView registeredView; + if (_rightTabRefreshHandlerViews.TryGetValue(item.Key, out registeredView) && + registeredView != null && item.Value != null) + { + registeredView.FocusedRowObjectChanged -= item.Value; + } + } + catch + { + // 表格可能已先于 TabControlEx 进入 Dispose,解绑失败不应阻断剩余清理。 + } + } + + _rightTabRefreshHandlers.Clear(); + _rightTabRefreshHandlerViews.Clear(); + _rightTabsByLeftGrid.Clear(); + _rightTabRefreshRegisteredGrids.Clear(); + _rightSideTabPages.Clear(); + } + + /// + /// 解除父表聚合视图的滚动/列宽事件,避免父表事件链反向持有当前 TabControlEx。 + /// + private void DetachParentGridHandlers() + { + GridControlEx parentGrid = ParentGridEx; + try + { + if (parentGrid != null && parentGrid.CustomGroupBandEx != null) + { + if (parentGrid.CustomGroupBandEx.BandedView != null) + { + parentGrid.CustomGroupBandEx.BandedView.LeftCoordChanged -= OnBandedViewLeftCoordChanged; + parentGrid.CustomGroupBandEx.BandedView.ColumnWidthChanged -= OnBandedViewColumnWidthChanged; + } + parentGrid.OnBuildGroupedColumnCallBack -= OnBandedViewColumnWidthChanged; + } + + if (parentGrid != null && parentGrid.CustomGroupTreeBandEx != null) + { + if (parentGrid.CustomGroupTreeBandEx.TreeListObj != null) + { + parentGrid.CustomGroupTreeBandEx.TreeListObj.LeftCoordChanged -= OnBandedViewLeftCoordChanged; + parentGrid.CustomGroupTreeBandEx.TreeListObj.ColumnWidthChanged -= OnBandedViewColumnWidthChanged; + } + parentGrid.OnBuildGroupedColumnCallBack -= OnBandedViewColumnWidthChanged; + } + } + catch + { + // 父表可能已经释放,事件清理应保持 best-effort。 + } } /// /// 水平滚动条改变时