NavisworksTransport/doc/guide/design_principles.md

71 KiB
Raw Blame History

NavisworksTransport 设计指导原则

本文档记录了NavisworksTransport项目中积累的设计原则和最佳实践为后续开发提供指导。

1. 线程安全与UI更新

问题描述

Navisworks插件开发中经常遇到UI线程死锁和跨线程操作异常特别是在后台任务需要更新UI状态时。

设计原则

1.1 异步事件触发

// ❌ 错误:同步事件触发可能导致死锁
private void OnStatusChanged(string status)
{
    StatusChanged?.Invoke(this, status);
}

// ✅ 正确:异步事件触发避免死锁
private void OnStatusChanged(string status)
{
    try
    {
        if (StatusChanged != null)
        {
            System.Threading.Tasks.Task.Run(() =>
            {
                try
                {
                    StatusChanged?.Invoke(this, status);
                }
                catch (Exception ex)
                {
                    LogManager.Error($"StatusChanged事件触发失败: {ex.Message}");
                }
            });
        }
    }
    catch (Exception ex)
    {
        LogManager.Error($"OnStatusChanged方法异常: {ex.Message}");
    }
}

1.2 UI线程更新策略

// ❌ 错误使用Invoke可能导致死锁
if (!Dispatcher.CheckAccess())
{
    Dispatcher.Invoke(() => UpdateUI());
}

// ✅ 正确使用BeginInvoke异步更新
if (!Dispatcher.CheckAccess())
{
    Dispatcher.BeginInvoke(
        new Action(() => {
            try
            {
                UpdateUI();
            }
            catch (Exception ex)
            {
                LogManager.Error($"UI更新失败: {ex.Message}");
            }
        }),
        DispatcherPriority.Background
    );
}

1.3 WinForms控件线程安全模式

// ✅ WinForms控件的标准线程安全模式
private void UpdateWinFormsControl()
{
    if (someControl.InvokeRequired)
    {
        someControl.BeginInvoke(new Action(() =>
        {
            try
            {
                // 具体的UI更新逻辑
                someControl.Text = "更新的文本";
                someControl.Enabled = true;
            }
            catch (Exception ex)
            {
                LogManager.Error($"控件更新失败: {ex.Message}");
            }
        }));
    }
    else
    {
        // 已在UI线程上直接更新
        someControl.Text = "更新的文本";
        someControl.Enabled = true;
    }
}

// ✅ ListView等复杂控件的批量更新策略
private void UpdateListViewSafely(ListView listView, List<ListViewItem> newItems)
{
    if (listView.InvokeRequired)
    {
        listView.BeginInvoke(new Action(() =>
        {
            try
            {
                // 批量更新,减少重绘次数
                listView.BeginUpdate();
                listView.Items.Clear();
                listView.Items.AddRange(newItems.ToArray());
                listView.EndUpdate();
            }
            catch (Exception ex)
            {
                LogManager.Error($"ListView更新失败: {ex.Message}");
            }
        }));
    }
    else
    {
        listView.BeginUpdate();
        listView.Items.Clear();
        listView.Items.AddRange(newItems.ToArray());
        listView.EndUpdate();
    }
}

1.4 事件处理器中的线程安全

// ✅ 事件处理器应该总是检查线程安全
private static void OnCurrentRouteChanged(object sender, PathRoute newRoute)
{
    GlobalExceptionHandler.SafeExecute(() =>
    {
        LogManager.Info($"[UI同步] 当前路径已变更: {newRoute?.Name ?? "无路径"}");

        // 确保在UI线程上执行
        if (_controlPanelForm != null && _controlPanelForm.InvokeRequired)
        {
            _controlPanelForm.BeginInvoke(new Action(() =>
            {
                try
                {
                    OnCurrentRouteChanged(sender, newRoute);
                }
                catch (Exception ex)
                {
                    LogManager.Error($"路径变更UI更新失败: {ex.Message}");
                }
            }));
            return;
        }

        // 实际的UI更新逻辑
        UpdateCurrentPathPointsList();
        UpdatePathList();

    }, "处理当前路径变更");
}

适用场景

  • 后台任务状态更新
  • 路径规划进度报告
  • 动画播放状态同步
  • 错误信息显示

2. 异常处理架构

问题描述

插件崩溃通常由未捕获异常导致,需要建立多层异常处理机制确保系统稳定性。

设计原则

2.1 多层异常处理

// 1. 全局异常处理器
public static class GlobalExceptionHandler
{
    public static void Initialize()
    {
        AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
        TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
        Application.ThreadException += OnThreadException;
    }
}

// 2. 业务层安全执行
public static void SafeExecute(Action action, string operationName = "操作")
{
    try
    {
        action();
    }
    catch (Exception ex)
    {
        LogManager.Error($"{operationName}失败: {ex.Message}");
        // 显示用户友好错误信息
        ShowErrorToast($"{operationName}失败: {ex.Message}");
    }
}

// 3. 关键操作的精确异常处理
try
{
    // 关键业务逻辑
    var result = SomeImportantOperation();
}
catch (SpecificException ex)
{
    // 特定异常的处理逻辑
    HandleSpecificError(ex);
}
catch (Exception ex)
{
    // 通用异常处理
    LogManager.Error($"操作失败: {ex.Message}");
    throw new BusinessException("业务操作失败", ex);
}

2.2 异常恢复机制

private static void TryRecoverComponents()
{
    try
    {
        // 重置路径编辑状态
        var activeManager = PathPlanningManager.GetActivePathManager();
        activeManager?.ResetPathEditState();
        
        // 清除临时高亮
        NavisApplication.ActiveDocument?.Models?.ResetAllTemporaryMaterials();
        
        // 清理资源
        CleanupResources();
    }
    catch (Exception ex)
    {
        LogManager.Error($"组件恢复失败: {ex.Message}");
    }
}

适用场景

18. 坐标系分层原则

问题描述

项目已经同时面对以下三类不同语义的坐标:

  • Navisworks 世界坐标
  • 程序内部计算坐标
  • 工程业务基准坐标(如球心、安装基准、轨道参考面)

如果这三层语义混在一起使用,就会出现典型问题:

  • 把世界原点误当成业务球心
  • 把世界 Z 误当成项目唯一的 up 方向
  • 一部分代码按源模型坐标算,一部分代码按转换后坐标算
  • Y-up / Z-up 项目切换后,路径、姿态、渲染结果彼此不一致

设计原则

18.1 必须明确区分三层坐标语义

flowchart LR
    A["Navisworks 世界坐标\n(模型原始坐标, 可能是 Y-up 或 Z-up 语义)"] --> B["内部统一坐标\n(程序所有计算都使用)"]
    B --> C["业务基准坐标\n(球心、安装方向、轨道参考面)"]

    C --> D["终端安装仿真"]
    C --> E["Rail 姿态/动画"]
    C --> F["碰撞检测/结果恢复"]

    B --> G["路径规划/网格/几何运算"]
    B --> H["渲染/可视化"]

    I["UI 输入/日志/坐标显示"] <-->|按需转换| B

三层语义的职责如下:

  • Navisworks 世界坐标

    • 这是 API 直接返回的坐标、包围盒、变换矩阵
    • 反映模型当前在 Navisworks 场景中的真实位置
    • 不能自动等同于业务上的球心、安装参考面或统一 up 方向
  • 内部统一坐标

    • 程序内部所有几何计算、路径规划、姿态计算应尽量统一使用这一层
    • 这一层负责消化源模型的 Y-up / Z-up 差异
    • 这一层只处理坐标轴和方向语义,不处理业务规则
  • 业务基准坐标

    • 用来表达球心、安装基准点、轨道参考面等工程语义
    • 这一层不应偷用世界原点或世界某个固定轴
    • 业务基准点应显式配置或显式计算,不得隐式假设

18.2 坐标系层只负责轴语义转换,不负责业务解释

坐标系抽象层应只回答以下问题:

  • 当前项目 up 方向是什么
  • 高程轴是哪一轴
  • 水平面由哪两轴组成
  • 点和向量如何在源模型坐标与内部统一坐标之间转换

坐标系层不应负责以下业务问题:

  • 球心是否在 (0,0,0)
  • 终端安装参考线是否指向球心
  • 顶面/底面对接如何定义
  • 轨道参考面偏移量是多少

这些都属于业务基准层。

18.3 业务逻辑不得直接写死世界原点和世界 Z

以下写法都应视为高风险设计:

// ❌ 错误:把世界原点直接当成业务球心
Vector3D direction = new Vector3D(centerPoint.X, centerPoint.Y, centerPoint.Z);

// ❌ 错误把世界Z直接当成项目up
var worldUp = new Vector3D(0, 0, 1);

// ❌ 错误把Min.Z / Max.Z直接当成统一的高程语义
double top = bounds.Max.Z;
double bottom = bounds.Min.Z;

更合理的写法应当是:

// ✅ 正确:球心来自业务基准配置或业务计算
Vector3D direction = centerPoint - sphereCenter;

// ✅ 正确up方向来自坐标系抽象或对象自身姿态
Vector3D worldUp = CoordinateSystemManager.Instance.Current.UpVector;

// ✅ 正确:高程语义来自坐标系抽象
double elevation = coordinateSystem.GetElevation(point);

18.4 程序内部应优先统一计算坐标,再按需转换回用户语义

当客户项目坚持使用 Y-up 坐标时,不应要求客户先把模型硬转成 Z-up 再继续使用。

更合理的做法是:

  • 保留客户熟悉的输入输出坐标语义
  • 程序内部统一转换到内部计算坐标
  • 计算完成后,再把需要展示给用户的数据映射回客户语义

也就是说:

  • 客户层可以是 Y-up
  • 内部计算层可以统一成程序更容易处理的一套坐标
  • 显示/日志层再根据需要转回客户语义

18.5 不要在全项目到处散落 Y-up / Z-up 条件分支

不推荐这种扩散式兼容写法:

// ❌ 错误:在业务逻辑中到处散落坐标系分支
if (isYUp)
{
    // ...
}
else
{
    // ...
}

更推荐的方式是:

  • 在坐标系层统一封装 UpVectorGetElevation()GetHorizontalCoords() 等能力
  • 在业务层统一消费抽象结果
  • 让路径、姿态、渲染尽量只面对“内部统一坐标”

18.6 业务基准点必须单独配置或显式求解

像“球心”这类点,不属于坐标系本身的一部分,必须单独处理。

例如:

  • 球心可以来自项目配置
  • 或来自多条已知向心轴线的拟合结果
  • 或来自设计资料中的明确基准点

但不能因为过去某批模型里球心正好在世界原点,就长期把它写死。

适用场景

  • 终端安装仿真

  • Rail 三维姿态与动画

  • 路径规划中的高程与水平平面计算

  • 多坐标系项目的输入、显示与日志输出

  • 所有对外接口方法

  • 事件处理器

  • 后台任务执行

  • Navisworks API调用

19. 框架先行与测试先行原则

问题描述

当功能涉及到底层几何语义、坐标系语义、局部轴约定或姿态矩阵时,如果直接在业务链路里一边试一边改,通常会出现这些问题:

  • 虚拟物体和真实模型的语义被混用
  • 一处补丁修好,另一条链路被带坏
  • 日志越来越多,但问题边界越来越模糊
  • 业务代码里充满临时补偿,最终难以维护

这类问题的根因往往不是业务流程本身,而是底层框架语义没有先被验证。

设计原则

19.1 先抽离框架,再接业务

对于以下类型的问题,不应先改业务代码:

  • 坐标系转换
  • 局部轴约定
  • 姿态构造
  • 纯几何补偿
  • 宿主坐标与内部统一坐标之间的映射

正确顺序应当是:

  1. 先抽出纯框架层
  2. 用最小测试验证框架层
  3. 通过后再接入业务模块

错误示例:

// ❌ 错误:尚未验证坐标/姿态语义,直接改动画和渲染链路
if (isYUp)
{
    rotation = routeRotation * someCorrection;
}
else
{
    rotation = routeRotation;
}

正确示例:

// ✅ 正确:先让纯框架层定义并验证语义
Quaternion rotation = canonicalPoseBuilder.CreateQuaternion(
    canonicalForward,
    canonicalUp,
    modelAxisConvention);

// 业务层只消费已验证的结果
frame.Rotation = ToNavisworksRotation(rotation);

19.2 可测试的核心框架不得直接依赖宿主 API

凡是需要单元测试验证的核心几何框架,不应直接绑定 Navisworks Point3DVector3DBoundingBox3D 这类宿主类型。

原因是:

  • 宿主 API 在脱离 Navisworks 进程时通常无法初始化
  • 会导致测试只能在宿主环境里间接验证
  • 这样测试粒度太粗,定位问题非常慢

更合理的方式是:

  • 框架核心使用纯数学类型
    • 例如 System.Numerics.Vector3
    • 例如 System.Numerics.Matrix4x4
    • 例如 System.Numerics.Quaternion
  • 只有边界适配层才接触 Navisworks API

也就是说:

  • 纯数学层:可单元测试
  • 宿主适配层:负责包装和转换
  • 业务层:消费已验证结果

19.3 先验证“语义”,再验证“效果”

这类框架测试的重点不是先看动画画面,而是先验证最小语义:

  • Y-up 到 Canonical Z-up 的点/向量转换是否正确
  • 本地 X-forward / Y-upX-forward / Z-up 的轴约定是否正确
  • 给定世界前进方向和上方向,生成的姿态矩阵列向量是否正确
  • 包围盒和参考点经过往返转换是否保持一致

只有这些最小语义先对,业务画面才值得继续看。

19.4 业务接入应尽量只做“选择约定”,不做“临时补偿”

当框架层已经定义了:

  • 宿主坐标系
  • 内部统一坐标
  • 模型局部轴约定

那么业务层最理想的职责应该只是:

  • 选择当前对象使用哪一种约定
  • 调用框架生成姿态或坐标

而不是在业务层继续做:

  • +90°
  • -90°
  • “如果 Y-up 就补一下”
  • “如果真实模型就再扭一下”

这些都属于典型的补丁式开发,会掩盖框架问题。

19.5 一旦测试表明框架不纯,必须先回到框架层

如果在测试阶段发现:

  • 纯框架层还依赖宿主 API
  • 测试无法脱离 Navisworks 运行
  • 同一个姿态语义在框架和业务里各算一遍

就不应该继续改业务代码,而应立即回到框架层收口。

这比继续在业务链路里加日志、加补偿更重要。

适用场景

  • 坐标系改造
  • Y-up / Z-up 兼容
  • Rail 三维姿态
  • 真实模型与虚拟物体局部轴约定统一
  • 碰撞恢复姿态语义

3. 内存管理与性能优化

问题描述

Navisworks插件长时间运行可能出现内存泄漏特别是在大模型操作和复杂算法执行时。

设计原则

3.1 及时资源释放

// ✅ 使用using语句自动释放资源
using (var disposableResource = new SomeDisposableClass())
{
    // 使用资源
}

// ✅ 手动清理大对象
public void Cleanup()
{
    try
    {
        _largeDataStructure?.Clear();
        _largeDataStructure = null;
        
        // 强制垃圾回收(谨慎使用)
        if (memoryPressure > threshold)
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }
    }
    catch (Exception ex)
    {
        LogManager.Error($"资源清理失败: {ex.Message}");
    }
}

3.2 大数据处理策略

// ✅ 分批处理大数据集
public void ProcessLargeDataSet(IEnumerable<ModelItem> items)
{
    const int batchSize = 1000;
    var batch = new List<ModelItem>(batchSize);
    
    foreach (var item in items)
    {
        batch.Add(item);
        
        if (batch.Count >= batchSize)
        {
            ProcessBatch(batch);
            batch.Clear();
            
            // 检查内存压力
            if (NeedMemoryRelief())
            {
                GC.Collect();
            }
        }
    }
    
    // 处理剩余项目
    if (batch.Count > 0)
    {
        ProcessBatch(batch);
    }
}

适用场景

  • 大模型数据处理
  • A*路径规划算法
  • 批量属性设置
  • 3D渲染操作

4. 事件驱动架构

问题描述

插件各组件间需要松耦合通信,避免直接依赖导致的紧耦合问题。

设计原则

4.1 事件定义规范

// ✅ 标准事件定义
public class PathPlanningManager
{
    // 状态变更事件
    public event EventHandler<string> StatusChanged;
    public event EventHandler<string> ErrorOccurred;
    
    // 业务事件
    public event EventHandler<PathRoute> RouteGenerated;
    public event EventHandler<PathEditState> PathEditStateChanged;
    
    // 安全触发事件
    protected virtual void OnStatusChanged(string status)
    {
        // 使用异步触发避免死锁
        Task.Run(() => {
            try
            {
                StatusChanged?.Invoke(this, status);
            }
            catch (Exception ex)
            {
                LogManager.Error($"事件触发失败: {ex.Message}");
            }
        });
    }
}

4.2 事件订阅管理

public class EventSubscriptionManager : IDisposable
{
    private readonly List<Action> _unsubscribeActions = new List<Action>();
    
    public void Subscribe<T>(EventHandler<T> handler, 
                           Action<EventHandler<T>> subscribe, 
                           Action<EventHandler<T>> unsubscribe)
    {
        subscribe(handler);
        _unsubscribeActions.Add(() => unsubscribe(handler));
    }
    
    public void Dispose()
    {
        foreach (var unsubscribe in _unsubscribeActions)
        {
            try
            {
                unsubscribe();
            }
            catch (Exception ex)
            {
                LogManager.Error($"事件取消订阅失败: {ex.Message}");
            }
        }
        _unsubscribeActions.Clear();
    }
}

适用场景

  • UI状态同步
  • 插件间通信
  • 进度报告
  • 错误传播

5. Navisworks API使用规范

问题描述

Navisworks API调用可能失败需要正确的错误处理和重试机制。

设计原则

5.1 API调用封装

public static class NavisApiHelper
{
    public static T SafeApiCall<T>(Func<T> apiCall, T defaultValue = default(T), 
                                   string operationName = "API调用")
    {
        try
        {
            // 检查API可用性
            if (NavisApplication.ActiveDocument == null)
            {
                throw new InvalidOperationException("Navisworks文档未加载");
            }
            
            return apiCall();
        }
        catch (Exception ex)
        {
            LogManager.Error($"{operationName}失败: {ex.Message}");
            return defaultValue;
        }
    }
    
    public static void SafeApiCall(Action apiCall, string operationName = "API调用")
    {
        SafeApiCall(() => { apiCall(); return true; }, false, operationName);
    }
}

5.2 COM API与.NET API协调使用

// ✅ 正确的双API使用模式
public class CategoryAttributeManager
{
    // 使用.NET API读取
    public static List<ModelItem> GetItemsWithCategory(LogisticsElementType category)
    {
        return NavisApiHelper.SafeApiCall(() =>
        {
            var search = new Search();
            search.SearchConditions.Add(new SearchCondition()
            {
                PropertyName = "Logistics.Category",
                Condition = SearchConditionType.Equal,
                Value = category.ToString()
            });
            return search.FindAll(NavisApplication.ActiveDocument).ToList();
        }, new List<ModelItem>(), "查询分类属性");
    }
    
    // 使用COM API持久化属性
    public static bool SetLogisticsAttribute(ModelItem item, LogisticsElementType category)
    {
        return NavisApiHelper.SafeApiCall(() =>
        {
            var comApi = ComApiBridge.ToInwOaPath(item);
            comApi.SetUserAttribute("Logistics", "Category", category.ToString());
            return true;
        }, false, "设置物流属性");
    }
}

适用场景

  • 模型数据访问
  • 属性读写操作
  • 3D场景操作
  • 文件导入导出

6. 状态管理模式

问题描述

插件需要维护复杂的状态信息,确保状态一致性和可预测性。

设计原则

6.1 状态枚举定义

// ✅ 清晰的状态定义
public enum PathEditState
{
    None,       // 无编辑状态
    Creating,   // 创建新路径
    Editing,    // 编辑现有路径
    Selecting   // 选择路径点
}

public enum AnimationState
{
    Stopped,    // 停止
    Playing,    // 播放中
    Paused,     // 暂停
    Recording   // 录制中
}

6.2 状态机模式

public class PathEditStateMachine
{
    private PathEditState _currentState = PathEditState.None;
    
    public PathEditState CurrentState 
    { 
        get => _currentState; 
        private set
        {
            if (_currentState != value)
            {
                var oldState = _currentState;
                _currentState = value;
                OnStateChanged(oldState, value);
            }
        }
    }
    
    public bool CanTransitionTo(PathEditState newState)
    {
        return ValidTransitions[_currentState].Contains(newState);
    }
    
    public void TransitionTo(PathEditState newState)
    {
        if (!CanTransitionTo(newState))
        {
            throw new InvalidOperationException(
                $"不能从{_currentState}转换到{newState}");
        }
        
        CurrentState = newState;
    }
    
    private static readonly Dictionary<PathEditState, HashSet<PathEditState>> ValidTransitions = 
        new Dictionary<PathEditState, HashSet<PathEditState>>
        {
            [PathEditState.None] = new HashSet<PathEditState> { PathEditState.Creating, PathEditState.Editing },
            [PathEditState.Creating] = new HashSet<PathEditState> { PathEditState.None, PathEditState.Selecting },
            [PathEditState.Editing] = new HashSet<PathEditState> { PathEditState.None, PathEditState.Selecting },
            [PathEditState.Selecting] = new HashSet<PathEditState> { PathEditState.Creating, PathEditState.Editing }
        };
}

适用场景

  • 路径编辑流程
  • 动画播放控制
  • UI模式切换
  • 工具状态管理

7. 配置与数据持久化

问题描述

插件配置和用户数据需要可靠的持久化机制,支持版本升级和迁移。

设计原则

7.1 配置文件结构

[Serializable]
public class PluginConfig
{
    public string Version { get; set; } = "1.0.0";
    public DateTime LastModified { get; set; } = DateTime.Now;
    
    // 用户设置
    public UserSettings User { get; set; } = new UserSettings();
    
    // 路径数据
    public List<PathRouteData> SavedPaths { get; set; } = new List<PathRouteData>();
    
    // 验证配置有效性
    public ValidationResult Validate()
    {
        var result = new ValidationResult();
        
        if (string.IsNullOrEmpty(Version))
            result.Errors.Add("版本号不能为空");
            
        if (SavedPaths?.Any(p => string.IsNullOrEmpty(p.Name)) == true)
            result.Errors.Add("路径名称不能为空");
            
        return result;
    }
}

7.2 数据迁移机制

public class ConfigMigrationManager
{
    private static readonly Dictionary<string, Func<JObject, JObject>> Migrations = 
        new Dictionary<string, Func<JObject, JObject>>
        {
            ["1.0.0"] = MigrateFrom100To101,
            ["1.0.1"] = MigrateFrom101To102
        };
    
    public static PluginConfig MigrateConfig(string configJson, string targetVersion)
    {
        var config = JObject.Parse(configJson);
        var currentVersion = config["Version"]?.ToString() ?? "1.0.0";
        
        while (currentVersion != targetVersion && Migrations.ContainsKey(currentVersion))
        {
            config = Migrations[currentVersion](config);
            currentVersion = config["Version"].ToString();
        }
        
        return config.ToObject<PluginConfig>();
    }
}

适用场景

  • 用户偏好设置
  • 路径数据保存
  • 项目配置管理
  • 插件状态恢复

8. 日志记录规范

问题描述

有效的日志记录对于问题诊断和系统维护至关重要。

设计原则

8.1 日志级别使用

public static class LogManager
{
    // Info: 正常业务流程
    public static void Info(string message)
    {
        WriteLog(LogLevel.Info, message);
    }
    
    // Warning: 可恢复的问题
    public static void Warning(string message)
    {
        WriteLog(LogLevel.Warning, message);
    }
    
    // Error: 需要关注的错误
    public static void Error(string message)
    {
        WriteLog(LogLevel.Error, message);
    }
    
    // Debug: 开发调试信息
    public static void Debug(string message)
    {
        if (IsDebugEnabled)
            WriteLog(LogLevel.Debug, message);
    }
}

// 使用示例
LogManager.Info("开始自动路径规划");
LogManager.Warning($"找到{conflictCount}个潜在冲突点");
LogManager.Error($"路径规划失败: {ex.Message}");
LogManager.Debug($"网格地图尺寸: {width}x{height}");

8.2 结构化日志

public static class StructuredLogger
{
    public static void LogOperation(string operation, object parameters, 
                                   TimeSpan duration, bool success, string error = null)
    {
        var logEntry = new
        {
            Timestamp = DateTime.Now,
            Operation = operation,
            Parameters = parameters,
            Duration = duration.TotalMilliseconds,
            Success = success,
            Error = error,
            ThreadId = Thread.CurrentThread.ManagedThreadId
        };
        
        LogManager.Info(JsonConvert.SerializeObject(logEntry, Formatting.None));
    }
}

// 使用示例
var stopwatch = Stopwatch.StartNew();
try
{
    var result = AutoPlanPath(start, end, objectSize);
    StructuredLogger.LogOperation("AutoPlanPath", 
        new { start, end, objectSize }, 
        stopwatch.Elapsed, true);
}
catch (Exception ex)
{
    StructuredLogger.LogOperation("AutoPlanPath", 
        new { start, end, objectSize }, 
        stopwatch.Elapsed, false, ex.Message);
}

适用场景

  • 操作流程跟踪
  • 性能监控
  • 错误诊断
  • 用户行为分析

9. 测试策略

问题描述

Navisworks插件的测试需要特殊考虑包括UI测试、API模拟等。

设计原则

9.1 可测试性设计

// ✅ 依赖注入提高可测试性
public interface INavisworksApiWrapper
{
    Document ActiveDocument { get; }
    ModelItemCollection FindItems(Search search);
    void SetUserAttribute(ModelItem item, string category, string name, string value);
}

public class PathPlanningManager
{
    private readonly INavisworksApiWrapper _apiWrapper;
    
    public PathPlanningManager(INavisworksApiWrapper apiWrapper = null)
    {
        _apiWrapper = apiWrapper ?? new DefaultNavisworksApiWrapper();
    }
    
    // 业务逻辑与API分离便于测试
    public PathRoute PlanPath(Point3D start, Point3D end)
    {
        var items = _apiWrapper.FindItems(CreateChannelSearch());
        return ExecutePathPlanning(start, end, items);
    }
}

9.2 单元测试结构

[TestClass]
public class PathPlanningManagerTests
{
    private Mock<INavisworksApiWrapper> _mockApi;
    private PathPlanningManager _manager;
    
    [TestInitialize]
    public void Setup()
    {
        _mockApi = new Mock<INavisworksApiWrapper>();
        _manager = new PathPlanningManager(_mockApi.Object);
    }
    
    [TestMethod]
    public void PlanPath_ValidInput_ReturnsPath()
    {
        // Arrange
        var start = new Point3D(0, 0, 0);
        var end = new Point3D(10, 10, 0);
        _mockApi.Setup(x => x.FindItems(It.IsAny<Search>()))
               .Returns(CreateMockChannels());
        
        // Act
        var result = _manager.PlanPath(start, end);
        
        // Assert
        Assert.IsNotNull(result);
        Assert.IsTrue(result.Points.Count > 0);
    }
}

适用场景

  • 核心算法验证
  • API调用测试
  • 业务逻辑验证
  • 回归测试

10. 性能监控与优化

问题描述

插件性能问题需要及时发现和定位,特别是在大模型处理时。

设计原则

10.1 性能监控代码

public class PerformanceMonitor : IDisposable
{
    private readonly string _operationName;
    private readonly Stopwatch _stopwatch;
    private readonly long _initialMemory;
    
    public PerformanceMonitor(string operationName)
    {
        _operationName = operationName;
        _stopwatch = Stopwatch.StartNew();
        _initialMemory = GC.GetTotalMemory(false);
        
        LogManager.Debug($"[性能] 开始监控: {operationName}");
    }
    
    public void Dispose()
    {
        _stopwatch.Stop();
        var finalMemory = GC.GetTotalMemory(false);
        var memoryDelta = finalMemory - _initialMemory;
        
        LogManager.Info($"[性能] {_operationName} 完成: " +
                       $"耗时{_stopwatch.ElapsedMilliseconds}ms, " +
                       $"内存变化{memoryDelta / 1024}KB");
        
        if (_stopwatch.ElapsedMilliseconds > 5000) // 超过5秒警告
        {
            LogManager.Warning($"[性能] {_operationName} 执行时间过长");
        }
    }
}

// 使用方式
using (new PerformanceMonitor("自动路径规划"))
{
    var result = AutoPlanPath(start, end, objectSize);
}

10.2 缓存策略

public class ModelDataCache
{
    private static readonly Dictionary<string, CacheEntry> _cache = 
        new Dictionary<string, CacheEntry>();
    private static readonly TimeSpan DefaultTtl = TimeSpan.FromMinutes(10);
    
    public static T GetOrCreate<T>(string key, Func<T> factory, TimeSpan? ttl = null)
    {
        CleanExpiredEntries();
        
        if (_cache.TryGetValue(key, out var entry) && !entry.IsExpired)
        {
            LogManager.Debug($"[缓存] 命中: {key}");
            return (T)entry.Value;
        }
        
        LogManager.Debug($"[缓存] 未命中,创建: {key}");
        var value = factory();
        _cache[key] = new CacheEntry(value, DateTime.Now.Add(ttl ?? DefaultTtl));
        
        return value;
    }
}

适用场景

  • 算法性能监控
  • 内存使用跟踪
  • 缓存效果评估
  • 瓶颈识别

11. 3D渲染性能优化

问题描述

Navisworks RenderPlugin在用户进行视图操作时会频繁调用如果渲染逻辑复杂或存在性能问题会导致界面卡顿甚至挂起。

设计原则

11.1 高效的渲染循环

public override void Render(View view, Graphics graphics)
{
    if (!_isEnabled) return;

    try
    {
        // ✅ 快速检查避免频繁的API调用
        var activeDoc = Application.ActiveDocument;
        if (activeDoc?.Models == null || activeDoc.Models.Count == 0)
        {
            return; // 静默返回,避免日志泛滥
        }
        
        // ✅ 早期退出,避免无意义的渲染
        int markerCount;
        lock (_lockObject)
        {
            markerCount = _circleMarkers.Count;
        }
        if (markerCount == 0) return;
        
        graphics.BeginModelContext();
        
        // ✅ 缓存计算结果,减少重复计算
        double lineRadiusInModelUnits = 0.2 * GetMetersToModelUnitsConversionFactor();
        
        lock (_lockObject)
        {
            // ✅ 使用数组而非List提高迭代性能
            var markers = _circleMarkers.ToArray();
            
            // 高效的渲染逻辑
            foreach (var marker in markers)
            {
                graphics.Color(marker.Color, marker.Alpha);
                graphics.Sphere(marker.Center, marker.Radius);
            }
        }
        
        graphics.EndModelContext();
    }
    catch (Exception ex)
    {
        // ✅ 渲染异常应静默处理,避免影响主程序
        LogManager.WriteLog($"[渲染异常] {ex.Message}");
    }
}

11.2 防抖机制避免频繁刷新

// ❌ 错误:每次操作都刷新视图
public void AddMarker(Point3D position)
{
    _markers.Add(new Marker(position));
    Application.ActiveDocument.ActiveView.RequestDelayedRedraw(ViewRedrawRequests.Render);
}

// ✅ 正确:使用防抖机制
private static DateTime _lastRefreshTime = DateTime.MinValue;

private void RequestViewRefresh()
{
    try
    {
        var now = DateTime.Now;
        if ((now - _lastRefreshTime).TotalMilliseconds < 50) // 最小间隔50ms
        {
            return; // 忽略过于频繁的刷新请求
        }
        _lastRefreshTime = now;

        if (Application.ActiveDocument?.ActiveView != null)
        {
            Application.ActiveDocument.ActiveView.RequestDelayedRedraw(ViewRedrawRequests.Render);
        }
    }
    catch (Exception ex)
    {
        LogManager.WriteLog($"[视图刷新] 失败: {ex.Message}");
    }
}

11.3 数据结构优化

// ✅ 使用高效的数据结构
public class OptimizedRenderPlugin : RenderPlugin
{
    // 使用数组存储频繁访问的数据
    private CircleMarker[] _cachedMarkers = new CircleMarker[0];
    private bool _cacheInvalid = true;
    
    // 批量更新机制
    public void BatchUpdateMarkers(IEnumerable<CircleMarker> newMarkers)
    {
        lock (_lockObject)
        {
            _circleMarkers.Clear();
            _circleMarkers.AddRange(newMarkers);
            _cacheInvalid = true;
        }
        
        RequestViewRefresh();
    }
    
    public override void Render(View view, Graphics graphics)
    {
        // 延迟更新缓存
        if (_cacheInvalid)
        {
            lock (_lockObject)
            {
                _cachedMarkers = _circleMarkers.ToArray();
                _cacheInvalid = false;
            }
        }
        
        // 使用缓存的数组进行渲染
        foreach (var marker in _cachedMarkers)
        {
            graphics.Color(marker.Color, marker.Alpha);
            graphics.Sphere(marker.Center, marker.Radius);
        }
    }
}

适用场景

  • 3D标记和路径可视化
  • 实时渲染更新
  • 大量图形元素渲染
  • 用户交互响应优化

12. 路径规划智能容错

问题描述

用户在复杂楼层环境中选择起点和终点时,经常会无意中点击到障碍物(墙体、柱子等)位置,导致路径规划失败。

设计原则

12.1 智能位置修正

// ❌ 错误:严格验证,用户体验差
if (!gridMap.IsWalkable(startGrid))
{
    throw new AutoPathPlanningException($"起点位于障碍物上");
}

// ✅ 正确:智能修正,提升用户体验
var correctedStartGrid = FindNearestWalkablePosition(gridMap, startGrid, "起点");
if (correctedStartGrid == null)
{
    throw new AutoPathPlanningException($"起点附近没有可通行区域");
}

// 记录修正信息
if (correctedStartGrid.Value != startGrid)
{
    var correctedWorldStart = gridMap.GridToWorld(correctedStartGrid.Value);
    LogManager.Info($"起点已自动修正: ({start.X:F2}, {start.Y:F2}) -> ({correctedWorldStart.X:F2}, {correctedWorldStart.Y:F2})");
    start = correctedWorldStart;
    startGrid = correctedStartGrid.Value;
}

12.2 BFS最近邻搜索算法

private Point2D? FindNearestWalkablePosition(GridMap gridMap, Point2D originalPos, 
                                           string positionName, int maxDistance = 10)
{
    // 如果原始位置已经可通行,直接返回
    if (gridMap.IsWalkable(originalPos))
    {
        return originalPos;
    }

    // 使用BFS搜索最近的可通行位置
    var visited = new HashSet<Point2D>();
    var queue = new Queue<(Point2D pos, int distance)>();
    
    queue.Enqueue((originalPos, 0));
    visited.Add(originalPos);

    // 8个方向的偏移量
    var directions = new[]
    {
        new Point2D(0, 1), new Point2D(0, -1),     // 上下
        new Point2D(1, 0), new Point2D(-1, 0),     // 左右
        new Point2D(1, 1), new Point2D(1, -1),     // 对角线
        new Point2D(-1, 1), new Point2D(-1, -1)
    };

    while (queue.Count > 0)
    {
        var (currentPos, distance) = queue.Dequeue();

        if (distance > maxDistance) break;

        foreach (var dir in directions)
        {
            var neighborPos = new Point2D(currentPos.X + dir.X, currentPos.Y + dir.Y);

            if (visited.Contains(neighborPos) || 
                !gridMap.IsValidGridPosition(neighborPos))
            {
                continue;
            }

            visited.Add(neighborPos);

            if (gridMap.IsWalkable(neighborPos))
            {
                LogManager.Info($"位置已修正到距离{distance + 1}格的位置");
                return neighborPos;
            }

            queue.Enqueue((neighborPos, distance + 1));
        }
    }

    return null; // 未找到可通行位置
}

12.3 渐进式错误处理

public PathRoute AutoPlanPath(Point3D startPoint, Point3D endPoint, 
                             double objectSize = 1.0, double safetyMargin = 0.5)
{
    try
    {
        // 第一次尝试:使用原始参数
        return PlanPathInternal(startPoint, endPoint, objectSize, safetyMargin);
    }
    catch (AutoPathPlanningException ex) when (ex.Message.Contains("位于障碍物上"))
    {
        LogManager.Info("使用智能修正重试路径规划...");
        
        try
        {
            // 第二次尝试:启用智能修正
            return PlanPathWithCorrection(startPoint, endPoint, objectSize, safetyMargin);
        }
        catch (Exception innerEx)
        {
            LogManager.Warning($"智能修正也失败: {innerEx.Message}");
            
            // 第三次尝试:降低精度重试
            var reducedMargin = safetyMargin * 0.5;
            var reducedSize = objectSize * 0.8;
            
            LogManager.Info($"降低参数重试: 物体尺寸{reducedSize:F1}m, 安全边距{reducedMargin:F1}m");
            return PlanPathInternal(startPoint, endPoint, reducedSize, reducedMargin);
        }
    }
}

12.4 用户反馈机制

// 在UI层提供清晰的反馈
private void OnPathPlanningResult(PathRoute result, bool wasPositionCorrected)
{
    if (result != null)
    {
        if (wasPositionCorrected)
        {
            StatusText = "路径规划成功(起点/终点已自动调整到最近可通行位置)";
            // 可选:高亮显示修正后的位置
            HighlightCorrectedPositions();
        }
        else
        {
            StatusText = "路径规划成功";
        }
    }
}

// 提供手动精确定位的选项
private void ShowPositionCorrectionDialog(Point3D originalPos, Point3D correctedPos)
{
    var message = $"所选位置位于障碍物上,已自动调整到最近可通行位置:\n" +
                  $"原位置: ({originalPos.X:F2}, {originalPos.Y:F2})\n" +
                  $"调整后: ({correctedPos.X:F2}, {correctedPos.Y:F2})\n\n" +
                  $"是否接受此调整?";
                  
    var result = MessageBox.Show(message, "位置自动调整", 
                                MessageBoxButtons.YesNo, MessageBoxIcon.Question);
    
    if (result == DialogResult.No)
    {
        // 允许用户重新选择
        RequestPositionReselection();
    }
}

适用场景

  • 复杂建筑环境的路径规划
  • 用户交互式点选位置
  • 精度要求高的导航系统
  • 自动化路径生成工具

13. WPF数据绑定最佳实践避免自定义集合陷阱

问题描述

在NavisworksTransport项目中发现了一个经典的WPF数据绑定问题自定义的 ThreadSafeObservableCollection 与WPF标准数据绑定机制不兼容导致UI显示重复数据。这个问题揭示了"过度工程"的风险以及回归标准实践的重要性。

问题根本原因分析

13.1 设计理念冲突

// ❌ 问题自定义线程安全集合与WPF冲突
public class ThreadSafeObservableCollection<T> : ObservableCollection<T>
{
    // 内部实现复杂的UI线程marshaling
    private void OnCollectionChanged()
    {
        // 自定义的UI线程处理机制
        _uiStateManager.QueueUIUpdate(() => 
        {
            base.OnCollectionChanged(...);
        });
    }
}

// ✅ 解决方案使用WPF标准集合
public ObservableCollection<PathRouteViewModel> PathRoutes { get; set; }
    = new ObservableCollection<PathRouteViewModel>();

核心冲突

  • ThreadSafeObservableCollection试图提供线程安全通过内部机制自动将变更marshaling到UI线程
  • WPF数据绑定期望使用标准的ObservableCollection由框架本身处理UI线程marshaling
  • 双重UI线程处理机制导致重复通知和不可预测的行为

13.2 异步处理复杂性

// ❌ 导致问题的异步初始化机制
public PathRouteViewModel()
{
    InitializeDefaults();
    _ = InitializeAsync(); // "火后不理"的异步调用
}

public async Task InitializeAsync()
{
    await _uiStateManager.ExecuteUIUpdateAsync(() =>
    {
        // 异步UI更新可能与同步数据创建产生时序问题
        Points.CollectionChanged += OnPointsCollectionChanged;
        _isInitialized = true;
    });
}

时序问题分析

  1. 同步的数据创建RefreshPathRoutes
  2. 异步的UI初始化InitializeAsync
  3. UI更新队列积压保底定时器强制处理
  4. 重复的UI更新执行

13.3 事件处理重叠

发现有三个机制同时处理相同的数据变更:

// 机制1手动刷新
RefreshPathRoutes() // 从Core数据创建UI路径点

// 机制2事件响应  
OnPathPointsListUpdated() // 响应路径点更新事件

// 机制3路径生成事件
OnRouteGenerated() // 处理自动路径生成

// 结果同样的路径点被多次添加到UI

完整解决方案

13.4 回归WPF标准实践

// ✅ 正确做法使用标准ObservableCollection
public class PathEditingViewModel : ViewModelBase
{
    // 路径集合使用标准集合
    public ObservableCollection<PathRouteViewModel> PathRoutes { get; private set; }
        = new ObservableCollection<PathRouteViewModel>();
}

public class PathRouteViewModel : ViewModelBase
{
    // 路径点集合也使用标准集合
    public ObservableCollection<PathPointViewModel> Points { get; private set; }
        = new ObservableCollection<PathPointViewModel>();
    
    // 简化初始化:同步完成,无异步复杂性
    public PathRouteViewModel()
    {
        InitializeDefaults();
        CompleteInitialization();
    }
    
    private void CompleteInitialization()
    {
        // 直接订阅事件,无需异步
        Points.CollectionChanged += OnPointsCollectionChanged;
        _isInitialized = true;
    }
}

13.5 职责分离和重复检查

// ✅ 事件处理器包含重复检查逻辑
private async void OnPathPointsListUpdated(object sender, PathPointsListUpdatedEventArgs e)
{
    if (e?.Route == null) return;

    await SafeExecuteAsync(() =>
    {
        var pathViewModel = PathRoutes.FirstOrDefault(p => p.Name == e.Route.Name);
        if (pathViewModel != null)
        {
            // 关键:检查是否需要更新,避免重复处理
            if (pathViewModel.Points.Count == e.Route.Points.Count)
            {
                LogManager.Info($"路径点数量已正确({pathViewModel.Points.Count}),跳过重复更新");
                return;
            }
            
            // 执行实际更新
            pathViewModel.Points.Clear();
            foreach (var point in e.Route.Points)
            {
                // 添加路径点...
            }
        }
    }, "处理路径点列表更新事件");
}

关键经验教训

13.6 过度工程的陷阱

问题:试图通过复杂的自定义机制"改进"框架的标准行为 结果:引入了与框架机制的冲突,造成更多问题 教训WPF的ObservableCollection已经是充分测试的成熟解决方案

// ❌ 过度设计:复杂的自定义线程安全集合
public class ThreadSafeObservableCollection<T> : ObservableCollection<T>
{
    private readonly UIStateManager _uiStateManager;
    private readonly object _lockObject = new object();
    
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        // 复杂的线程安全逻辑
        if (_uiStateManager != null)
        {
            _uiStateManager.QueueUIUpdate(() => base.OnCollectionChanged(e));
        }
        // 与WPF绑定机制产生冲突
    }
}

// ✅ 简单有效:使用标准解决方案
public ObservableCollection<T> Items { get; private set; } = new ObservableCollection<T>();

13.7 线程安全的正确处理方式

在WPF中正确的线程安全做法是

  • 数据操作在适当的线程中执行
  • UI更新统一通过Dispatcher.InvokeUIStateManager在UI线程执行
  • 不要在集合层面实现线程安全,而是在操作层面控制
// ✅ 正确的线程安全模式
public async Task AddPathAsync(PathRouteViewModel path)
{
    await _uiStateManager.ExecuteUIUpdateAsync(() =>
    {
        PathRoutes.Add(path); // 在UI线程上操作标准集合
    });
}

13.8 调试复杂问题的方法论

从这个案例中学到的调试方法:

  1. 详细日志追踪:记录事件时序和调用栈
  2. 识别异步副作用:关注"火后不理"的异步调用
  3. 分析处理机制重叠:多个组件处理相同数据的情况
  4. 回归简单方案:当复杂方案出问题时,考虑标准做法

设计原则总结

13.9 集合使用原则

// ✅ WPF UI绑定使用标准ObservableCollection
public ObservableCollection<ItemViewModel> Items { get; private set; } 
    = new ObservableCollection<ItemViewModel>();

// ✅ 后台数据处理:可以使用线程安全集合
private readonly ConcurrentBag<ProcessingItem> _processingQueue 
    = new ConcurrentBag<ProcessingItem>();

// ✅ UI更新时统一在UI线程操作
public async Task UpdateUI(List<ItemData> newData)
{
    await _uiStateManager.ExecuteUIUpdateAsync(() =>
    {
        Items.Clear();
        foreach (var item in newData)
        {
            Items.Add(new ItemViewModel(item));
        }
    });
}

13.10 ViewModel设计原则

// ✅ 保持ViewModel简单和同步
public class SimpleViewModel : ViewModelBase
{
    public SimpleViewModel()
    {
        // 同步初始化,避免复杂的异步逻辑
        InitializeProperties();
        SubscribeToEvents();
    }
    
    // ✅ 使用标准属性更改通知
    private string _status;
    public string Status
    {
        get => _status;
        set => SetProperty(ref _status, value);
    }
}

// ❌ 避免复杂的异步初始化
public class ComplexViewModel : ViewModelBase
{
    public ComplexViewModel()
    {
        _ = InitializeAsync(); // 导致时序问题
    }
    
    private async Task InitializeAsync()
    {
        // 复杂的异步初始化逻辑
        // 可能与UI绑定产生冲突
    }
}

适用场景和建议

13.11 何时使用标准集合

使用ObservableCollection的场景

  • WPF数据绑定
  • UI列表显示
  • 用户交互集合
  • ViewModel中的集合属性

使用线程安全集合的场景

  • 后台数据处理
  • 多线程生产者-消费者模式
  • 缓存和队列
  • 非UI相关的数据结构

13.12 实施检查清单

在代码审查中重点检查:

  • ViewModel中的集合是否使用标准ObservableCollection
  • 是否避免了"过度设计"的自定义集合?
  • UI更新是否统一在UI线程执行
  • 是否存在多个机制处理相同数据的情况?
  • 异步初始化是否真的必要?

结论

这个UI重复问题的根本原因是试图通过自定义的ThreadSafeObservableCollection来"改进"WPF的标准数据绑定但这种改进引入了与框架机制的冲突导致重复的UI更新和不可预测的行为

解决方案是回归WPF的最佳实践使用标准集合和框架提供的机制。这个案例很好地说明了在软件开发中,简单、标准的解决方案往往比复杂的自定义方案更可靠。

14. 线程安全实践经验总结

问题描述

在实际开发中发现UI线程死锁是导致插件崩溃的主要原因之一。特别是在自动路径规划等后台任务完成后更新UI时经常出现界面冻结和崩溃问题。

关键经验

13.1 死锁问题的根本原因

// ❌ 导致死锁的典型模式
public void UpdateUI()
{
    // 后台线程试图更新UI
    Application.Current.Dispatcher.Invoke(() => {
        // UI线程被阻塞等待后台任务完成
        // 后台任务又在等待UI线程响应 -> 死锁
        someLabel.Text = "更新文本";
    });
}

// ✅ 避免死锁的正确做法
public void UpdateUI()
{
    Application.Current.Dispatcher.BeginInvoke(
        new Action(() => {
            try {
                someLabel.Text = "更新文本";
            } catch (Exception ex) {
                LogManager.Error($"UI更新失败: {ex.Message}");
            }
        }),
        DispatcherPriority.Background
    );
}

13.2 系统性检查清单

基于实际修复经验,以下是需要重点检查的场景:

  1. 事件处理器 - 所有UI事件处理都需要线程安全检查
  2. ListView/ComboBox操作 - Items.Clear()、Items.Add()等操作
  3. Label/TextBox更新 - Text属性、ForeColor属性更新
  4. Button状态控制 - Enabled、Visible属性更新
  5. ProgressBar更新 - Value属性在动画过程中的更新
  6. 状态标签更新 - 在异步操作完成后的状态显示

13.3 修复模式标准化

// 标准修复模式 - 适用于简单控件
private void SafeUpdateControl<T>(T control, Action<T> updateAction) 
    where T : Control
{
    if (control.InvokeRequired)
    {
        control.BeginInvoke(new Action(() => {
            try {
                updateAction(control);
            } catch (Exception ex) {
                LogManager.Error($"控件更新失败: {ex.Message}");
            }
        }));
    }
    else
    {
        updateAction(control);
    }
}

// 使用示例
SafeUpdateControl(statusLabel, label => {
    label.Text = "操作完成";
    label.ForeColor = Color.Green;
});

// 复杂控件的批量更新模式
private void SafeUpdateListView(ListView listView, Action<ListView> updateAction)
{
    if (listView.InvokeRequired)
    {
        listView.BeginInvoke(new Action(() => {
            try {
                listView.BeginUpdate();
                updateAction(listView);
                listView.EndUpdate();
            } catch (Exception ex) {
                LogManager.Error($"ListView更新失败: {ex.Message}");
            }
        }));
    }
    else
    {
        listView.BeginUpdate();
        updateAction(listView);
        listView.EndUpdate();
    }
}

13.4 性能优化考虑

// ✅ 减少跨线程调用的开销
private void UpdateMultipleControls()
{
    // 准备所有数据
    var statusText = "操作完成";
    var itemCount = pathList.Count;
    var progressValue = 100;
    
    // 一次性跨线程调用,而不是多次
    if (InvokeRequired)
    {
        BeginInvoke(new Action(() => {
            try {
                statusLabel.Text = statusText;
                countLabel.Text = $"共{itemCount}项";
                progressBar.Value = progressValue;
            } catch (Exception ex) {
                LogManager.Error($"批量UI更新失败: {ex.Message}");
            }
        }));
    }
}

// ❌ 效率低下的多次跨线程调用
private void UpdateMultipleControlsWrongWay()
{
    SafeUpdateControl(statusLabel, l => l.Text = "操作完成");
    SafeUpdateControl(countLabel, l => l.Text = $"共{pathList.Count}项");
    SafeUpdateControl(progressBar, p => p.Value = 100);
    // 每个控件都单独调用BeginInvoke开销大
}

13.5 调试和监控

// 添加线程安全检查的调试代码
public static class ThreadSafetyChecker
{
    public static void CheckUIThread(string operationName)
    {
        if (!Application.Current.Dispatcher.CheckAccess())
        {
            LogManager.Warning($"[线程警告] {operationName} 在非UI线程上调用");
            // 在调试模式下抛出异常,强制修复
            #if DEBUG
            throw new InvalidOperationException($"{operationName} 必须在UI线程上调用");
            #endif
        }
    }
}

// 在关键UI更新点添加检查
private void UpdateCurrentPathStatus(string status)
{
    ThreadSafetyChecker.CheckUIThread("UpdateCurrentPathStatus");
    _currentPathStatusLabel.Text = status;
}

实施建议

  1. 预防性修复 - 在新功能开发时主动应用线程安全模式
  2. 代码审查重点 - 重点检查UI更新相关的代码
  3. 测试策略 - 在高负载场景下测试多线程行为
  4. 监控机制 - 添加线程安全违规的监控和报警

常见陷阱

  1. ListView.SelectedItems访问 - 在事件处理器中直接访问可能引发跨线程异常
  2. Control.Text属性 - 看似简单但必须在UI线程上执行
  3. 事件链式调用 - 一个事件触发另一个事件,可能导致递归的线程问题
  4. Dispose检查遗漏 - 在BeginInvoke回调中访问已释放的控件

总结

这些设计原则基于NavisworksTransport项目的实际开发经验总结涵盖了插件开发中的关键技术挑战。遵循这些原则可以

  1. 提高系统稳定性 - 通过多层异常处理和安全的线程操作
  2. 增强代码可维护性 - 通过清晰的架构设计和规范的编码实践
  3. 优化性能表现 - 通过有效的资源管理和性能监控
  4. 简化问题诊断 - 通过完善的日志记录和错误跟踪
  5. 消除UI死锁 - 通过系统性的线程安全实践

重点实践建议

基于实际修复经验,以下实践最为关键:

线程安全优先级

  1. 最高优先级 - 所有UI更新操作必须使用BeginInvoke模式
  2. 高优先级 - 事件处理器必须包含线程安全检查
  3. 中优先级 - 复杂控件操作使用批量更新策略

代码审查检查点

  • 是否所有控件更新都有InvokeRequired检查
  • 是否使用BeginInvoke而不是Invoke
  • 是否在UI更新中添加了异常处理
  • 是否避免了嵌套的Invoke调用

测试验证要点

  • 在多路径规划场景下测试UI响应性
  • 验证后台任务完成后的UI更新是否正常
  • 检查动画播放过程中的界面稳定性
  • 确认异常情况下的优雅降级

在后续开发中,应该:

  • 优先应用线程安全原则 - 在所有UI相关代码中
  • 持续更新和完善指导文档 - 记录新发现的问题和解决方案
  • 在代码审查中重点检查线程安全 - 特别关注UI更新相关代码
  • 定期进行压力测试 - 验证多线程环境下的稳定性
  • 建立监控机制 - 及时发现和修复线程安全问题

附录

Windows 兼容性对比 | .NET Framework 版本 | 最低 Windows 版本要求 | |-------------------|---------------------| | 4.8 | Windows 7 SP1 | | 4.8.1 | Windows 10 1607+ |

你的选择很明智!保持 .NET Framework 4.8 可以:

支持更多 Windows 版本:包括 Windows 7、8、8.1 和所有 Windows 10 版本 更好的企业兼容性:很多企业环境仍在使用较老的 Windows 版本 Navisworks 兼容性:确保在各种客户环境中都能正常运行 C# 7.3 功能总结 既然我们使用 .NET Framework 4.8 + C# 7.3,你可以使用的现代 C# 功能包括:

模式匹配(刚才我们用的) out 变量声明 元组和解构 表达式体成员 字符串插值 null 条件运算符 async/await

C# 版本兼容性表 | .NET Framework | 默认 C# 版本 | 最高支持 C# 版本 | |----------------|-------------|-----------------| | 4.6.1 | C# 7.0 | C# 7.3 | | 4.7 | C# 7.0 | C# 7.3 | | 4.7.1 | C# 7.1 | C# 7.3 | | 4.7.2 | C# 7.3 | C# 7.3 | | 4.8 | C# 7.3 | C# 7.3 | | 4.8.1 | C# 7.3 | C# 7.3 |

15. WPF对话框置顶问题窗口Z-Order

问题描述

在Navisworks插件开发中WPF对话框可能会出现在主窗口背后导致用户无法操作。这个问题在处理大模型时尤为明显

  • 小模型:动画生成速度快,对话框弹出时主窗口仍处于活动状态
  • 大模型:预计算耗时较长,用户可能点击主窗口,导致窗口焦点/Z-Order变化。对话框弹出时不在最前面

根本原因

  1. Navisworks是Win32应用程序WPF作为插件嵌入其中Application.Current.MainWindow 不可靠
  2. Owner属性未正确设置时,WindowStartupLocation="CenterOwner" 无法正常工作
  3. 异步操作耗时导致窗口焦点在对话框显示前发生变化

解决方案

15.1 XAML层解决方案推荐

在对话框XAML中添加 Topmost="True",确保窗口始终置顶:

<!-- ✅ 正确设置Topmost确保窗口置顶 -->
<Window x:Class="NavisworksTransport.UI.WPF.Views.CollisionAnalysisDialog"
        Title="预计算碰撞分析 - 排除建议"
        WindowStartupLocation="CenterOwner"
        Topmost="True"
        ...>

参考实现GenerateNavigationMapDialog.xamlEditRotationWindow.xamlAerialHeightDialog.xaml

15.2 代码层解决方案(备用)

在ViewModel中动态查找Owner窗口

// ✅ 正确多重备用方案查找Owner窗口
private void ShowDialogWithOwner(Window dialog)
{
    // 方案1尝试使用MainWindow
    var mainWindow = System.Windows.Application.Current?.MainWindow;
    if (mainWindow != null && mainWindow.IsVisible)
    {
        try
        {
            dialog.Owner = mainWindow;
        }
        catch (InvalidOperationException)
        {
            LogManager.Debug("[对话框] 无法设置MainWindow为Owner尝试备用方案");
        }
    }
    
    // 方案2遍历所有窗口找活动窗口
    if (dialog.Owner == null && System.Windows.Application.Current != null)
    {
        foreach (Window window in System.Windows.Application.Current.Windows)
        {
            if (window.IsActive && window.IsLoaded)
            {
                try
                {
                    dialog.Owner = window;
                    LogManager.Debug($"[对话框] 设置活动窗口为Owner: {window.Title}");
                    break;
                }
                catch (InvalidOperationException)
                {
                    // 继续尝试下一个窗口
                }
            }
        }
    }
    
    dialog.ShowDialog();
}

15.3 MessageBox的Owner设置

// ✅ 正确为MessageBox传入owner参数
Window owner = System.Windows.Application.Current?.MainWindow;
if (owner == null || !owner.IsVisible)
{
    foreach (Window window in System.Windows.Application.Current?.Windows ?? new WindowCollection())
    {
        if (window.IsActive)
        {
            owner = window;
            break;
        }
    }
}

var result = System.Windows.MessageBox.Show(
    owner,  // 关键传入owner确保MessageBox置顶
    message,
    "检测配置已存在",
    System.Windows.MessageBoxButton.YesNo,
    System.Windows.MessageBoxImage.Question);

15.4 Code-Behind中的Owner设置

当在UserControl中显示对话框时使用 Window.GetWindow(this)

// ✅ 正确从UserControl获取父窗口
private void ShowHelpDialog()
{
    var helpDialog = new Views.HelpDialog
    {
        Owner = Window.GetWindow(this)  // 关键获取当前UserControl所在的Window
    };
    helpDialog.ShowDialog();
}

参考实现LogisticsControlPanel.xaml.csPathEditingView.xaml.cs

实际修复案例

案例1CollisionAnalysisDialog预计算碰撞分析

问题:打开大模型点击生成动画后,碰撞结果分析窗口藏在主窗口背后,无法点击

修复

  1. XAML添加 Topmost="True"
  2. ViewModel添加备用Owner查找逻辑
<!-- CollisionAnalysisDialog.xaml -->
<Window ...
        WindowStartupLocation="CenterOwner"
        Topmost="True">  <!-- 添加此行 -->
// AnimationControlViewModel.cs - ShowCollisionAnalysisDialog方法
// 添加备用Owner查找逻辑详见15.2节)

案例2重复配置提示MessageBox

问题检测配置重复提示的MessageBox也可能被主窗口遮挡

修复

// ShowDuplicateConfigDialog方法
var result = System.Windows.MessageBox.Show(
    owner,  // 传入owner参数
    message,
    "检测配置已存在",
    System.Windows.MessageBoxButton.YesNo,
    System.Windows.MessageBoxImage.Question);

设计原则

场景 推荐方案 说明
自定义WPF对话框 Topmost="True" 最简单可靠
需要居中对齐 Topmost="True" + 动态设置Owner 兼顾置顶和位置
MessageBox 传入owner参数 系统对话框特殊处理
UserControl内调用 Window.GetWindow(this) 获取正确的父窗口

检查清单

  • 所有模态对话框是否设置了 Topmost="True"
  • 需要居中的对话框是否正确设置了Owner
  • MessageBox是否传入了owner参数
  • UserControl中显示对话框是否使用了 Window.GetWindow(this)
  • 是否有多重备用方案处理Owner查找失败

常见陷阱

  1. 仅依赖 Application.Current.MainWindow - 在Navisworks环境中不可靠
  2. 只设置 WindowStartupLocation="CenterOwner" 但不设置Owner - 窗口位置不可预测
  3. 忽略异步操作的时序问题 - 耗时操作后窗口焦点可能已变化
  4. MessageBox不传入owner - 系统对话框也可能被遮挡

16. XAML资源引用检查清单

问题描述

使用未在XAML中定义的Converter/Style资源会导致窗口无法显示这是WPF开发中常见的运行时错误。

错误示例

<!-- ❌ 错误使用了未定义的Converter -->
Visibility="{Binding HasItems, Converter={StaticResource InverseBoolToVisibilityConverter}}"

<!-- ✅ 正确使用已定义的Converter -->
Visibility="{Binding HasItems, Converter={StaticResource BoolToVisibilityConverter}, ConverterParameter=Inverse}"

项目中已定义的资源

资源名 类型 说明
BoolToVisibilityConverter BoolToVisibilityConverter 布尔转可见性支持Inverse参数

开发工作流程

  1. 每添加一个资源引用,立即确认其存在

    ❌ 错误做法:
       - 复制其他文件的XAML代码
       - 一次性写大量XAML再测试
    
    ✅ 正确做法:
       - 每写一行 {StaticResource xxx},立即检查 xxx 是否已定义
       - 使用 Ctrl+F 搜索 xxx 确认在当前文件或合并字典中存在
    
  2. 资源定义位置优先级

    • 本文件 <Window.Resources><UserControl.Resources> 内定义
    • 引用的外部资源字典(如 NavisworksStyles.xaml
    • 必须在 XAML 顶部检查 MergedDictionaries 是否正确合并
  3. 新增XAML文件时必做检查清单

    □ 所有 {StaticResource xxx} 引用都有定义
    □ 所有 xmlns:local 命名空间映射正确
    □ 所有 ValueConverter 在 xaml.cs 中已实现
    □ 合并的资源字典路径正确pack://application:,,,/...
    □ 编译通过后立即运行测试,验证窗口能正常打开
    
  4. 常见错误模式与修正

    错误写法 问题 正确写法
    InverseBoolToVisibilityConverter 未定义 BoolToVisibilityConverter + ConverterParameter=Inverse
    VisibilityConverter 未定义 BoolToVisibilityConverter
    BooleanToVisibilityConverter (键名) 引用时使用 BoolToVisibilityConverter 键名和引用保持一致
    拼写错误的Style名 找不到资源 检查资源字典中的精确定义
  5. 调试XAML资源错误

    • 错误信息:无法在资源字典中找到名为 xxx 的资源
    • 定位方法从堆栈跟踪找到最后加载的XAML元素
    • 快速修复:临时注释掉报错元素,添加简化版本测试
    • 预防措施:小步增量开发,每步验证窗口可正常显示

历史错误记录

  • 2025-02-14: PathAnalysisDialog.xaml 使用了 BoolToVisibilityConverter 但定义的是 BooleanToVisibilityConverter,导致窗口崩溃
  • 根本原因:没有逐条执行检查清单,过度自信

AI助手强制检查点

步骤 强制动作 验证方法
1. 写完XAML结构 列出所有 {StaticResource xxx} grep -n "StaticResource" File.xaml
2. 检查每个引用 在文件中搜索资源定义 确认 x:Key="xxx" 存在
3. 检查Converter 确认 xaml.cs 中有实现 grep "class.*Converter.*:" File.xaml.cs
4. 首次编译 必须通过 compile.bat
5. 运行时验证 窗口能正常打开 实际运行测试

17. 对话框辅助类DialogHelper

问题描述

项目中多处重复代码用于设置对话框 Owner 和确保窗口置顶。需要统一封装,减少重复代码。

DialogHelper 工具类

src/Utils/DialogHelper.cs 提供了统一的对话框处理方法:

// 使用方式1Code-Behind 中从 UserControl 获取 Owner
var dialog = new MyDialog();
DialogHelper.SetOwnerFromUserControl(dialog, this);
dialog.ShowDialog();

// 使用方式2ViewModel 中自动查找 Owner
var dialog = new MyDialog();
DialogHelper.SetOwnerSafely(dialog);
dialog.ShowDialog();

// 使用方式3一键显示对话框自动处理 Owner
var result = DialogHelper.ShowDialog(new MyDialog(), this);

// 使用方式4MessageBox 确保置顶
var result = DialogHelper.ShowMessageBox(
    "确认删除?", 
    "提示", 
    MessageBoxButton.YesNo, 
    MessageBoxImage.Question);

方法说明

方法 场景 说明
SetOwnerFromUserControl Code-Behind 从 UserControl 获取父窗口
FindOwnerWindow ViewModel 查找合适的 OwnerMainWindow -> 活动窗口)
SetOwnerSafely ViewModel 安全设置 Owner带异常处理
ShowDialog 通用 自动设置 Owner 并显示对话框
ShowMessageBox 通用 显示置顶 MessageBox
SetWin32Owner 特殊 设置 Win32 父窗口Navisworks 主窗口)

迁移示例

旧代码(重复且冗长)

// Code-Behind
var dialog = new HelpDialog();
try
{
    var parentWindow = Window.GetWindow(this);
    if (parentWindow != null)
    {
        dialog.Owner = parentWindow;
    }
}
catch (Exception ownerEx)
{
    LogManager.Warning($"设置Owner失败: {ownerEx.Message}");
}
dialog.ShowDialog();
// ViewModel
var dialog = new MyDialog();
var mainWindow = System.Windows.Application.Current?.MainWindow;
if (mainWindow != null && mainWindow.IsVisible)
{
    try
    {
        dialog.Owner = mainWindow;
    }
    catch (InvalidOperationException)
    {
        // 备用方案...
        foreach (Window window in System.Windows.Application.Current.Windows)
        {
            // ...
        }
    }
}
dialog.ShowDialog();

新代码(简洁统一)

// Code-Behind
DialogHelper.ShowDialog(new HelpDialog(), this);

// ViewModel
DialogHelper.ShowDialog(new MyDialog());

设计原则

  1. 代码复用优先:使用 DialogHelper 替代重复的 Owner 设置代码
  2. 渐进式降级:自动尝试多种方式获取 OwnerMainWindow -> 活动窗口 -> null
  3. 静默失败:设置 Owner 失败时不影响对话框显示(仅记录日志)
  4. Topmost 仍是主要手段XAML 中仍需设置 Topmost="True"

18. 基础框架改造的开发顺序

原则

当功能涉及以下任一类基础语义时,禁止直接在业务代码里试错式修补:

  • 坐标系
  • 三维姿态
  • 几何局部轴
  • 单位系统
  • 动画/碰撞恢复的定位基准

正确顺序必须是:

  1. 先抽出框架层/数学层
  2. 先写最小可验证测试
  3. 测试通过后再接回业务模块

原因

这类问题一旦直接在业务链路里反复修补,最容易出现:

  • 虚拟物体正确、真实物体错误
  • 起点正确、动画第一帧错误
  • 动画正确、碰撞恢复错误
  • 中心点正确、渲染几何局部轴错误

根因通常不是“又差一个补偿”,而是底层语义没有先被验证。

本项目实证经验

本项目在 Rail 三维姿态与坐标系改造中,采用以下顺序后明显更稳:

  1. 先建立 HostCoordinateAdapter / ModelAxisConvention / CanonicalRailPoseBuilder
  2. 再写单元测试验证 Y-up / Z-up、局部 forward/up、四元数/线性姿态
  3. 最后再接入:
    • 终端安装仿真
    • Rail 姿态
    • 动画播放
    • 碰撞恢复

结论:

  • 对基础语义改造,测试先行接业务 是推荐流程
  • 没有测试支撑时,不应在业务代码里靠补偿和 fallback 猜结果

额外经验 1物体物理尺寸必须固定

对于真实物体动画,物体的物理尺寸一旦确定,就必须固定存储并重复使用:

  • 起点贴合使用的尺寸
  • 动画帧预计算使用的尺寸
  • 通行空间尺寸语义
  • 碰撞恢复使用的尺寸

这些必须来自同一份固定物理尺寸,不能在动画过程中反复从“当前已旋转的 AABB”重新推导。

否则会出现典型问题:

  • 起点贴合正确
  • 动画第一帧立刻拉开固定间隙

额外经验 2部署必须校验 WPF 资源完整性

对 WPF 插件来说,仅校验 DLL 被复制成功还不够。

必须确认构建产物包含完整的 .g.resources / .baml 资源;否则会出现:

  • 插件 DLL 存在
  • Navisworks 能加载程序集
  • 但一打开面板就因缺少 *.baml 崩溃

本项目已实际遇到:

  • TransportPlugin.dll 成功部署
  • 但缺少 LogisticsControlPanel.baml / PathEditingView.baml / AnimationControlView.baml
  • 最终表现为“插件布局丢失”或“手工打开插件窗口即崩溃”

因此经验是:

  • 单元测试顺带产出的 DLL 不能默认用于最终部署
  • 最终部署前应至少确认主项目完整构建成功
  • 必要时校验关键 WPF 视图资源是否真的编入程序集

额外经验 3三维碰撞验证前禁止先重置宿主姿态

对受 PathAnimationManager 控制的三维动画对象,ClashDetective 候选验证前不能先调用:

  • doc.Models.ResetPermanentTransform(modelItems)

原因:

  • PathAnimationManager 的内部跟踪姿态记录的是“当前动画目标姿态”
  • 一旦先把宿主物体重置回 CAD 原始姿态,宿主真实姿态就会变成单位姿态
  • 如果随后仍复用 PathAnimationManager.MoveAnimatedObjectToPose(...)
  • 则它会把“缓存姿态”误当成当前姿态,导致增量旋转退化成单位旋转,只剩平移

实际后果:

  • 动画结束后,碰撞检测汇总一启动,真实物体会被摆平或姿态跳变
  • 后续所有 ClashDetective 验证都在错误姿态上进行

正确做法:

  • 对三维 PAM 主链路对象,直接复用 MoveAnimatedObjectToPose(...)
  • 不再在验证入口额外执行 ResetPermanentTransform
  • ResetPermanentTransform 只保留给旧二维/非 PAM 恢复分支

本文档将随着项目发展持续更新,确保设计指导的有效性和实用性。