332 lines
9.4 KiB
Markdown
332 lines
9.4 KiB
Markdown
# 文档切换三级处理机制设计方案
|
||
|
||
**日期**: 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
|
||
/// <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 - 插件关闭(一次性)
|
||
|
||
```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. 可能需要在某些管理器中区分"清理状态"和"销毁实例"
|
||
|
||
这个方案清晰地分离了不同场景的处理逻辑,避免了混淆,确保了稳定性。
|