# 文档切换三级处理机制设计方案 **日期**: 2025-09-14 **作者**: Claude **背景**: 解决Navisworks插件在文档切换时的崩溃问题 ## 一、问题分析 ### 发现的问题 1. Navisworks是SDI(单文档界面)应用,ActiveDocument在整个生命周期中保持不变 2. 文档内容变化通过`Models.CollectionChanged`事件通知 3. 原代码在Models清空时调用了完整的CleanupManagers,导致IdleEventManager被Dispose,引发崩溃 ### 测试观察结果 通过观察版本测试,发现Models.CollectionChanged事件的触发模式: **场景1:第一次打开文件** - 事件#1:Count=0,状态保持无模型(文档清空准备加载) - 事件#2:Count=1,从无模型→有模型(文件加载完成) **场景2:重新打开新文件** - 事件#3:Count=0,从有模型→无模型(旧文件被清空) - 事件#4:Count=1,从无模型→有模型(新文件加载完成) **场景3:关闭程序** - 事件#5:Count=0,从有模型→无模型(程序关闭前清理) ## 二、设计方案 ### 整体架构 ``` 插件生命周期: ├── OnLoaded(插件启动)- 重量级初始化 ├── Models.CollectionChanged(文档切换)- 轻量级状态管理 └── OnUnloading/DestroyControlPane(插件关闭)- 重量级清理 ``` ### 三种处理级别 1. **插件初始化**(OnLoaded) - 创建所有管理器实例 - 初始化UI组件 - 订阅事件 2. **文档状态切换**(Models.CollectionChanged) - 轻量级操作 - 清理/刷新ModelItem引用 - 重置可视化状态 - 保持管理器实例存活 3. **插件关闭**(OnUnloading + DestroyControlPane) - 重量级清理 - 销毁所有管理器实例 - 取消所有事件订阅 - 释放所有资源 ## 三、具体实现 ### 1. 添加必要的成员变量 ```csharp public class Main : DockPanePlugin { // 状态跟踪变量 private bool _hasModels = false; // 跟踪模型状态 private bool _isShuttingDown = false; // 插件关闭标志 private int _eventCount = 0; // 事件计数(调试用) private DateTime _pluginStartTime; // 插件启动时间 } ``` ### 2. OnLoaded - 插件初始化(一次性) ```csharp protected override void OnLoaded() { base.OnLoaded(); _pluginStartTime = DateTime.Now; _isShuttingDown = false; LogManager.Info("[文档管理] 插件OnLoaded - 开始初始化"); // 订阅文档事件 SubscribeToDocumentEvents(); // 一次性初始化管理器(创建实例) if (NavisApplication.ActiveDocument != null) { LogManager.Info("[文档管理] 插件加载时发现活动文档,初始化管理器"); InitializeManagers(); // 创建管理器实例 // 检查文档是否有内容 if (NavisApplication.ActiveDocument.Models?.Count > 0) { _hasModels = true; RefreshDocumentStates(); // 刷新状态 } } } ``` ### 3. OnModelsCollectionChanged - 文档切换处理(频繁) ```csharp private void OnModelsCollectionChanged(object sender, EventArgs e) { _eventCount++; // 如果插件正在关闭,跳过所有处理 if (_isShuttingDown) { LogManager.Info("[文档管理] 插件正在关闭,跳过Models.CollectionChanged处理"); return; } var activeDoc = NavisApplication.ActiveDocument; var modelCount = activeDoc?.Models?.Count ?? 0; bool currentHasModels = modelCount > 0; LogManager.Info($"[文档管理] Models.CollectionChanged #{_eventCount}, Count={modelCount}, 状态: {(_hasModels ? "有" : "无")} -> {(currentHasModels ? "有" : "无")}"); // 状态转换:从有到无(文档被清空) if (_hasModels && !currentHasModels) { LogManager.Info("[文档管理] 文档已清空,执行轻量级清理"); ClearDocumentStates(); } // 状态转换:从无到有(文档加载完成) else if (!_hasModels && currentHasModels) { LogManager.Info("[文档管理] 文档已加载,刷新状态"); RefreshDocumentStates(); } // 状态未变化 else { LogManager.Info("[文档管理] 模型集合更新但状态未变"); } _hasModels = currentHasModels; } ``` ### 4. 新增轻量级方法 ```csharp /// /// 轻量级清理 - 仅清理文档相关状态,不销毁管理器 /// private void ClearDocumentStates() { try { LogManager.Info("[文档管理] 开始轻量级状态清理..."); // 1. 停止动画(如果正在播放) var animationManager = PathAnimationManager.GetInstance(); if (animationManager?.IsAnimating == true) { animationManager.StopAnimation(); } // 清理动画中的ModelItem引用 animationManager?.ClearReferences(); // 2. 清理路径管理器状态 var pathManager = PathPlanningManager.GetActivePathManager(); if (pathManager != null) { pathManager.ResetPathEditState(); pathManager.ClearInvalidReferences(); // 清理ModelItem引用 } // 3. 清理路径可视化 PathPointRenderPlugin.Instance?.ClearAllPaths(); // 4. 清理碰撞检测缓存 ClashDetectiveIntegration.Instance?.ClearCollisionCache(); ClashDetectiveIntegration.ClearAllCaches(); // 5. 清除临时材质 if (NavisApplication.ActiveDocument?.Models != null) { NavisApplication.ActiveDocument.Models.ResetAllTemporaryMaterials(); } // 6. 通知文档状态管理器 DocumentStateManager.Instance.OnDocumentInvalidated(); LogManager.Info("[文档管理] 轻量级状态清理完成"); } catch (Exception ex) { LogManager.Warning($"[文档管理] 状态清理时出现警告: {ex.Message}"); } } /// /// 轻量级刷新 - 仅刷新状态,不重新创建管理器 /// private void RefreshDocumentStates() { try { LogManager.Info("[文档管理] 开始刷新文档状态..."); // 1. 通知文档状态管理器 DocumentStateManager.Instance.OnDocumentReady(); // 2. 清理旧缓存 ClashDetectiveIntegration.ClearAllCaches(); // 3. 刷新路径管理器(如果需要) var pathManager = PathPlanningManager.GetActivePathManager(); pathManager?.RefreshDocument(); LogManager.Info("[文档管理] 文档状态刷新完成"); } catch (Exception ex) { LogManager.Warning($"[文档管理] 状态刷新时出现警告: {ex.Message}"); } } ``` ### 5. OnUnloading - 插件关闭(一次性) ```csharp protected override void OnUnloading() { LogManager.Info("[文档管理] 插件OnUnloading - 开始关闭"); // 设置关闭标志,阻止后续的Models事件处理 _isShuttingDown = true; // 取消订阅文档事件 UnsubscribeFromDocumentEvents(); // 执行完整的管理器清理(销毁实例) CleanupManagers(); base.OnUnloading(); } ``` ### 6. DestroyControlPane - 控件销毁 ```csharp public override void DestroyControlPane(Control pane) { GlobalExceptionHandler.SafeExecute(() => { LogManager.Info("开始销毁DockPane控制面板"); // 设置关闭标志(双重保险) _isShuttingDown = true; // 清理WPF控件资源 if (pane is ElementHost elementHost && elementHost.Child is UI.WPF.LogisticsControlPanel wpfControl) { wpfControl.Cleanup(); elementHost.Child = null; } // 释放控件资源 pane?.Dispose(); LogManager.Info("DockPane控制面板销毁完成"); }, "销毁控制面板"); } ``` ### 7. 保持不变的方法 - **InitializeManagers()** - 保持原样,用于创建管理器实例 - **CleanupManagers()** - 保持原样,用于销毁管理器实例 - **SubscribeToDocumentEvents()** - 保持原样 - **UnsubscribeFromDocumentEvents()** - 保持原样 ## 四、关键设计原则 ### 1. 分离关注点 - 插件生命周期管理(OnLoaded/OnUnloading) - 文档状态管理(Models.CollectionChanged) - UI生命周期管理(CreateControlPane/DestroyControlPane) ### 2. 轻量级 vs 重量级 - **轻量级**:清理引用、重置状态、清空缓存 - **重量级**:创建/销毁实例、订阅/取消事件 ### 3. 防御性编程 - 使用`_isShuttingDown`标志防止关闭时的事件处理 - 所有清理操作都包含try-catch - 使用GlobalExceptionHandler.SafeExecute ### 4. 性能优化 - 文档切换时不重新创建管理器 - 只在必要时清理和刷新 - 避免重复操作 ## 五、预期效果 1. **启动时**:完整初始化,创建所有组件 2. **文档切换时**:快速清理和刷新,保持响应性 3. **关闭时**:干净退出,避免异常 ## 六、实施计划 1. 先实现`_isShuttingDown`标志,防止关闭时的崩溃 2. 实现`ClearDocumentStates()`和`RefreshDocumentStates()`方法 3. 修改`OnModelsCollectionChanged`使用新的轻量级方法 4. 测试各种场景确保稳定性 ## 七、注意事项 1. PathPlanningManager可能需要添加`RefreshDocument()`方法 2. 确保所有管理器都有适当的清理引用方法 3. 考虑添加更多的日志来跟踪状态变化 4. 可能需要在某些管理器中区分"清理状态"和"销毁实例" 这个方案清晰地分离了不同场景的处理逻辑,避免了混淆,确保了稳定性。