NavisworksTransport/doc/working/2025-09-14_文档切换三级处理机制设计.md

9.4 KiB
Raw Permalink Blame History

文档切换三级处理机制设计方案

日期: 2025-09-14 作者: Claude 背景: 解决Navisworks插件在文档切换时的崩溃问题

一、问题分析

发现的问题

  1. Navisworks是SDI单文档界面应用ActiveDocument在整个生命周期中保持不变
  2. 文档内容变化通过Models.CollectionChanged事件通知
  3. 原代码在Models清空时调用了完整的CleanupManagers导致IdleEventManager被Dispose引发崩溃

测试观察结果

通过观察版本测试发现Models.CollectionChanged事件的触发模式

场景1第一次打开文件

  • 事件#1Count=0状态保持无模型文档清空准备加载
  • 事件#2Count=1从无模型→有模型文件加载完成

场景2重新打开新文件

  • 事件#3Count=0从有模型→无模型旧文件被清空
  • 事件#4Count=1从无模型→有模型新文件加载完成

场景3关闭程序

  • 事件#5Count=0从有模型→无模型程序关闭前清理

二、设计方案

整体架构

插件生命周期:
├── OnLoaded插件启动- 重量级初始化
├── Models.CollectionChanged文档切换- 轻量级状态管理
└── OnUnloading/DestroyControlPane插件关闭- 重量级清理

三种处理级别

  1. 插件初始化OnLoaded

    • 创建所有管理器实例
    • 初始化UI组件
    • 订阅事件
  2. 文档状态切换Models.CollectionChanged

    • 轻量级操作
    • 清理/刷新ModelItem引用
    • 重置可视化状态
    • 保持管理器实例存活
  3. 插件关闭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. 性能优化

  • 文档切换时不重新创建管理器
  • 只在必要时清理和刷新
  • 避免重复操作

五、预期效果

  1. 启动时:完整初始化,创建所有组件
  2. 文档切换时:快速清理和刷新,保持响应性
  3. 关闭时:干净退出,避免异常

六、实施计划

  1. 先实现_isShuttingDown标志,防止关闭时的崩溃
  2. 实现ClearDocumentStates()RefreshDocumentStates()方法
  3. 修改OnModelsCollectionChanged使用新的轻量级方法
  4. 测试各种场景确保稳定性

七、注意事项

  1. PathPlanningManager可能需要添加RefreshDocument()方法
  2. 确保所有管理器都有适当的清理引用方法
  3. 考虑添加更多的日志来跟踪状态变化
  4. 可能需要在某些管理器中区分"清理状态"和"销毁实例"

这个方案清晰地分离了不同场景的处理逻辑,避免了混淆,确保了稳定性。