9.4 KiB
9.4 KiB
文档切换三级处理机制设计方案
日期: 2025-09-14 作者: Claude 背景: 解决Navisworks插件在文档切换时的崩溃问题
一、问题分析
发现的问题
- Navisworks是SDI(单文档界面)应用,ActiveDocument在整个生命周期中保持不变
- 文档内容变化通过
Models.CollectionChanged事件通知 - 原代码在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(插件关闭)- 重量级清理
三种处理级别
-
插件初始化(OnLoaded)
- 创建所有管理器实例
- 初始化UI组件
- 订阅事件
-
文档状态切换(Models.CollectionChanged)
- 轻量级操作
- 清理/刷新ModelItem引用
- 重置可视化状态
- 保持管理器实例存活
-
插件关闭(OnUnloading + DestroyControlPane)
- 重量级清理
- 销毁所有管理器实例
- 取消所有事件订阅
- 释放所有资源
三、具体实现
1. 添加必要的成员变量
public class Main : DockPanePlugin
{
// 状态跟踪变量
private bool _hasModels = false; // 跟踪模型状态
private bool _isShuttingDown = false; // 插件关闭标志
private int _eventCount = 0; // 事件计数(调试用)
private DateTime _pluginStartTime; // 插件启动时间
}
2. OnLoaded - 插件初始化(一次性)
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 - 文档切换处理(频繁)
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. 新增轻量级方法
/// <summary>
/// 轻量级清理 - 仅清理文档相关状态,不销毁管理器
/// </summary>
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}");
}
}
/// <summary>
/// 轻量级刷新 - 仅刷新状态,不重新创建管理器
/// </summary>
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 - 插件关闭(一次性)
protected override void OnUnloading()
{
LogManager.Info("[文档管理] 插件OnUnloading - 开始关闭");
// 设置关闭标志,阻止后续的Models事件处理
_isShuttingDown = true;
// 取消订阅文档事件
UnsubscribeFromDocumentEvents();
// 执行完整的管理器清理(销毁实例)
CleanupManagers();
base.OnUnloading();
}
6. DestroyControlPane - 控件销毁
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. 性能优化
- 文档切换时不重新创建管理器
- 只在必要时清理和刷新
- 避免重复操作
五、预期效果
- 启动时:完整初始化,创建所有组件
- 文档切换时:快速清理和刷新,保持响应性
- 关闭时:干净退出,避免异常
六、实施计划
- 先实现
_isShuttingDown标志,防止关闭时的崩溃 - 实现
ClearDocumentStates()和RefreshDocumentStates()方法 - 修改
OnModelsCollectionChanged使用新的轻量级方法 - 测试各种场景确保稳定性
七、注意事项
- PathPlanningManager可能需要添加
RefreshDocument()方法 - 确保所有管理器都有适当的清理引用方法
- 考虑添加更多的日志来跟踪状态变化
- 可能需要在某些管理器中区分"清理状态"和"销毁实例"
这个方案清晰地分离了不同场景的处理逻辑,避免了混淆,确保了稳定性。