fix(legacy): finalize hosted module lifecycle cleanup

This commit is contained in:
SugarChes
2026-08-29 18:03:44 +08:00
parent e27d445844
commit 176952d52c
5 changed files with 806 additions and 88 deletions
+354 -63
View File
@@ -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
{
/// <summary>
/// 在窗体关闭前捕获模块控件树及其 GridView。WinForms 的 Close/Dispose
/// 流程可能会先清空 Controls 或重建 GridView;保留这个快照可以让后续
/// 引用解绑仍然针对原始实例执行。
/// </summary>
public sealed class ModuleReferenceSnapshot
{
internal readonly List<WinFormsControl> Controls;
internal readonly List<GridView> GridViews;
internal ModuleReferenceSnapshot(
List<WinFormsControl> controls,
List<GridView> gridViews)
{
Controls = controls ?? new List<WinFormsControl>();
GridViews = gridViews ?? new List<GridView>();
}
}
public static ModuleReferenceSnapshot CaptureModuleReferences(
WinFormsControl moduleRoot,
XtraTabPage modulePage)
{
List<WinFormsControl> moduleControls = new List<WinFormsControl>();
try
{
moduleControls = CollectModuleControls(moduleRoot, modulePage);
}
catch (Exception exception)
{
TraceCleanupFailure("收集模块控件引用", exception);
}
List<GridView> gridViews = new List<GridView>();
try
{
gridViews = CollectGridViews(moduleControls);
}
catch (Exception exception)
{
TraceCleanupFailure("收集模块表格引用", exception);
}
return new ModuleReferenceSnapshot(moduleControls, gridViews);
}
/// <summary>
/// Releases static references owned by a closed legacy module. Cleanup is
/// best-effort so it cannot change the module close result.
/// </summary>
public static void ReleaseModuleReferences(WinFormsControl moduleRoot, XtraTabPage modulePage, string moduleCode)
{
List<GridView> gridViews = new List<GridView>();
ReleaseModuleReferences(
CaptureModuleReferences(moduleRoot, modulePage),
modulePage,
moduleCode);
}
/// <summary>
/// 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.
/// </summary>
public static void ReleaseModuleReferences(
ModuleReferenceSnapshot snapshot,
XtraTabPage modulePage,
string moduleCode)
{
List<WinFormsControl> moduleControls = snapshot == null || snapshot.Controls == null
? new List<WinFormsControl>()
: snapshot.Controls;
List<GridView> gridViews = snapshot == null || snapshot.GridViews == null
? new List<GridView>()
: snapshot.GridViews;
List<TabControlEx> tabControls = new List<TabControlEx>();
try
{
gridViews = CollectGridViews(moduleRoot);
tabControls = moduleControls.OfType<TabControlEx>().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<GridView> CollectGridViews(WinFormsControl root)
private static void ReleaseGridControlRuntimeReferences(
IEnumerable<WinFormsControl> moduleControls)
{
if (moduleControls == null)
return;
HashSet<GridControlEx> released =
new HashSet<GridControlEx>(ReferenceEqualityComparer<GridControlEx>.Instance);
foreach (GridControlEx gridControl in moduleControls.OfType<GridControlEx>())
{
if (gridControl == null || !released.Add(gridControl))
continue;
try
{
gridControl.ReleaseResourcesForDispose();
}
catch (Exception exception)
{
TraceCleanupFailure("释放表格控件运行时引用", exception);
}
}
}
private static List<WinFormsControl> CollectModuleControls(
WinFormsControl moduleRoot,
XtraTabPage modulePage)
{
List<WinFormsControl> result = new List<WinFormsControl>();
AddModuleControls(result, moduleRoot);
if (!ReferenceEquals(moduleRoot, modulePage))
AddModuleControls(result, modulePage);
return result;
}
private static void AddModuleControls(
List<WinFormsControl> target,
WinFormsControl root)
{
if (root == null)
return;
foreach (WinFormsControl control in EnumerateControls(root))
if (!ContainsControl(target, control))
target.Add(control);
}
private static List<GridView> CollectGridViews(IEnumerable<WinFormsControl> controls)
{
List<GridView> result = new List<GridView>();
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<GridView> target, GridView view)
{
if (view != null && !ContainsGridView(target, view))
target.Add(view);
}
private static bool ContainsGridView(
IEnumerable<GridView> 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<WinFormsControl> 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<WinFormsControl> 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<WinFormsControl> 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<WinFormsControl> 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<WinFormsControl> 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<GridView> gridViews)
private static void ClearRightMenuReferences(
IEnumerable<WinFormsControl> moduleControls,
List<GridView> 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<string, GridControlEx>();
}
private static void ClearDataCaches(WinFormsControl root)
private static void RemoveModuleCacheReferences(
IEnumerable<WinFormsControl> moduleControls,
IEnumerable<GridView> 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<object> moduleReferences = new HashSet<object>(ReferenceEqualityComparer<object>.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<Dictionary<object, Hashtable>> moduleCaches =
new HashSet<Dictionary<object, Hashtable>>(ReferenceEqualityComparer<Dictionary<object, Hashtable>>.Instance);
HashSet<DynamicModel> moduleModels =
new HashSet<DynamicModel>(ReferenceEqualityComparer<DynamicModel>.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<object, Hashtable> 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<Dictionary<object, Hashtable>> caches,
HashSet<DynamicModel> 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<object, Hashtable> caches,
HashSet<object> moduleReferences)
{
DynamicModel model = value as DynamicModel;
if (model == null || model.DataCaches == null)
if (caches == null || moduleReferences == null || moduleReferences.Count == 0)
return;
Dictionary<object, Hashtable> caches = model.DataCaches;
foreach (Hashtable cache in caches.Values.OfType<Hashtable>().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<IDisposable>().ToList())
try { item.Dispose(); } catch { }
cache.Clear();
foreach (object key in caches.Keys.Cast<object>().ToList())
if (moduleReferences.Contains(key))
caches.Remove(key);
}
}
private sealed class ReferenceEqualityComparer<T> : IEqualityComparer<T>
where T : class
{
public static readonly ReferenceEqualityComparer<T> Instance =
new ReferenceEqualityComparer<T>();
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;
}
/// <summary>
/// 记录当前U盾权限窗口