# NavisworksTransport 设计指导原则 本文档记录了NavisworksTransport项目中积累的设计原则和最佳实践,为后续开发提供指导。 ## 1. 线程安全与UI更新 ### 问题描述 Navisworks插件开发中经常遇到UI线程死锁和跨线程操作异常,特别是在后台任务需要更新UI状态时。 ### 设计原则 #### 1.1 异步事件触发 ```csharp // ❌ 错误:同步事件触发可能导致死锁 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线程更新策略 ```csharp // ❌ 错误:使用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控件线程安全模式 ```csharp // ✅ 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 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 事件处理器中的线程安全 ```csharp // ✅ 事件处理器应该总是检查线程安全 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 多层异常处理 ```csharp // 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 异常恢复机制 ```csharp 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 必须明确区分三层坐标语义 ```mermaid 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 以下写法都应视为高风险设计: ```csharp // ❌ 错误:把世界原点直接当成业务球心 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; ``` 更合理的写法应当是: ```csharp // ✅ 正确:球心来自业务基准配置或业务计算 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 条件分支 不推荐这种扩散式兼容写法: ```csharp // ❌ 错误:在业务逻辑中到处散落坐标系分支 if (isYUp) { // ... } else { // ... } ``` 更推荐的方式是: - 在坐标系层统一封装 `UpVector`、`GetElevation()`、`GetHorizontalCoords()` 等能力 - 在业务层统一消费抽象结果 - 让路径、姿态、渲染尽量只面对“内部统一坐标” #### 18.6 业务基准点必须单独配置或显式求解 像“球心”这类点,不属于坐标系本身的一部分,必须单独处理。 例如: - 球心可以来自项目配置 - 或来自多条已知向心轴线的拟合结果 - 或来自设计资料中的明确基准点 但不能因为过去某批模型里球心正好在世界原点,就长期把它写死。 ### 适用场景 - 终端安装仿真 - Rail 三维姿态与动画 - 路径规划中的高程与水平平面计算 - 多坐标系项目的输入、显示与日志输出 - 所有对外接口方法 - 事件处理器 - 后台任务执行 - Navisworks API调用 ## 19. 框架先行与测试先行原则 ### 问题描述 当功能涉及到底层几何语义、坐标系语义、局部轴约定或姿态矩阵时,如果直接在业务链路里一边试一边改,通常会出现这些问题: - 虚拟物体和真实模型的语义被混用 - 一处补丁修好,另一条链路被带坏 - 日志越来越多,但问题边界越来越模糊 - 业务代码里充满临时补偿,最终难以维护 这类问题的根因往往不是业务流程本身,而是底层框架语义没有先被验证。 ### 设计原则 #### 19.1 先抽离框架,再接业务 对于以下类型的问题,不应先改业务代码: - 坐标系转换 - 局部轴约定 - 姿态构造 - 纯几何补偿 - 宿主坐标与内部统一坐标之间的映射 正确顺序应当是: 1. 先抽出纯框架层 2. 用最小测试验证框架层 3. 通过后再接入业务模块 错误示例: ```csharp // ❌ 错误:尚未验证坐标/姿态语义,直接改动画和渲染链路 if (isYUp) { rotation = routeRotation * someCorrection; } else { rotation = routeRotation; } ``` 正确示例: ```csharp // ✅ 正确:先让纯框架层定义并验证语义 Quaternion rotation = canonicalPoseBuilder.CreateQuaternion( canonicalForward, canonicalUp, modelAxisConvention); // 业务层只消费已验证的结果 frame.Rotation = ToNavisworksRotation(rotation); ``` #### 19.2 可测试的核心框架不得直接依赖宿主 API 凡是需要单元测试验证的核心几何框架,不应直接绑定 Navisworks `Point3D`、`Vector3D`、`BoundingBox3D` 这类宿主类型。 原因是: - 宿主 API 在脱离 Navisworks 进程时通常无法初始化 - 会导致测试只能在宿主环境里间接验证 - 这样测试粒度太粗,定位问题非常慢 更合理的方式是: - 框架核心使用纯数学类型 - 例如 `System.Numerics.Vector3` - 例如 `System.Numerics.Matrix4x4` - 例如 `System.Numerics.Quaternion` - 只有边界适配层才接触 Navisworks API 也就是说: - **纯数学层**:可单元测试 - **宿主适配层**:负责包装和转换 - **业务层**:消费已验证结果 #### 19.3 先验证“语义”,再验证“效果” 这类框架测试的重点不是先看动画画面,而是先验证最小语义: - Y-up 到 Canonical Z-up 的点/向量转换是否正确 - 本地 `X-forward / Y-up` 与 `X-forward / Z-up` 的轴约定是否正确 - 给定世界前进方向和上方向,生成的姿态矩阵列向量是否正确 - 包围盒和参考点经过往返转换是否保持一致 只有这些最小语义先对,业务画面才值得继续看。 #### 19.4 业务接入应尽量只做“选择约定”,不做“临时补偿” 当框架层已经定义了: - 宿主坐标系 - 内部统一坐标 - 模型局部轴约定 那么业务层最理想的职责应该只是: - 选择当前对象使用哪一种约定 - 调用框架生成姿态或坐标 而不是在业务层继续做: - `+90°` - `-90°` - “如果 Y-up 就补一下” - “如果真实模型就再扭一下” 这些都属于典型的补丁式开发,会掩盖框架问题。 #### 19.5 一旦测试表明框架不纯,必须先回到框架层 如果在测试阶段发现: - 纯框架层还依赖宿主 API - 测试无法脱离 Navisworks 运行 - 同一个姿态语义在框架和业务里各算一遍 就不应该继续改业务代码,而应立即回到框架层收口。 这比继续在业务链路里加日志、加补偿更重要。 ### 适用场景 - 坐标系改造 - Y-up / Z-up 兼容 - Rail 三维姿态 - 真实模型与虚拟物体局部轴约定统一 - 碰撞恢复姿态语义 ## 3. 内存管理与性能优化 ### 问题描述 Navisworks插件长时间运行可能出现内存泄漏,特别是在大模型操作和复杂算法执行时。 ### 设计原则 #### 3.1 及时资源释放 ```csharp // ✅ 使用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 大数据处理策略 ```csharp // ✅ 分批处理大数据集 public void ProcessLargeDataSet(IEnumerable items) { const int batchSize = 1000; var batch = new List(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 事件定义规范 ```csharp // ✅ 标准事件定义 public class PathPlanningManager { // 状态变更事件 public event EventHandler StatusChanged; public event EventHandler ErrorOccurred; // 业务事件 public event EventHandler RouteGenerated; public event EventHandler PathEditStateChanged; // 安全触发事件 protected virtual void OnStatusChanged(string status) { // 使用异步触发避免死锁 Task.Run(() => { try { StatusChanged?.Invoke(this, status); } catch (Exception ex) { LogManager.Error($"事件触发失败: {ex.Message}"); } }); } } ``` #### 4.2 事件订阅管理 ```csharp public class EventSubscriptionManager : IDisposable { private readonly List _unsubscribeActions = new List(); public void Subscribe(EventHandler handler, Action> subscribe, Action> 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调用封装 ```csharp public static class NavisApiHelper { public static T SafeApiCall(Func 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协调使用 ```csharp // ✅ 正确的双API使用模式 public class CategoryAttributeManager { // 使用.NET API读取 public static List 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(), "查询分类属性"); } // 使用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 状态枚举定义 ```csharp // ✅ 清晰的状态定义 public enum PathEditState { None, // 无编辑状态 Creating, // 创建新路径 Editing, // 编辑现有路径 Selecting // 选择路径点 } public enum AnimationState { Stopped, // 停止 Playing, // 播放中 Paused, // 暂停 Recording // 录制中 } ``` #### 6.2 状态机模式 ```csharp 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> ValidTransitions = new Dictionary> { [PathEditState.None] = new HashSet { PathEditState.Creating, PathEditState.Editing }, [PathEditState.Creating] = new HashSet { PathEditState.None, PathEditState.Selecting }, [PathEditState.Editing] = new HashSet { PathEditState.None, PathEditState.Selecting }, [PathEditState.Selecting] = new HashSet { PathEditState.Creating, PathEditState.Editing } }; } ``` ### 适用场景 - 路径编辑流程 - 动画播放控制 - UI模式切换 - 工具状态管理 ## 7. 配置与数据持久化 ### 问题描述 插件配置和用户数据需要可靠的持久化机制,支持版本升级和迁移。 ### 设计原则 #### 7.1 配置文件结构 ```csharp [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 SavedPaths { get; set; } = new List(); // 验证配置有效性 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 数据迁移机制 ```csharp public class ConfigMigrationManager { private static readonly Dictionary> Migrations = new Dictionary> { ["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(); } } ``` ### 适用场景 - 用户偏好设置 - 路径数据保存 - 项目配置管理 - 插件状态恢复 ## 8. 日志记录规范 ### 问题描述 有效的日志记录对于问题诊断和系统维护至关重要。 ### 设计原则 #### 8.1 日志级别使用 ```csharp 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 结构化日志 ```csharp 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 可测试性设计 ```csharp // ✅ 依赖注入提高可测试性 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 单元测试结构 ```csharp [TestClass] public class PathPlanningManagerTests { private Mock _mockApi; private PathPlanningManager _manager; [TestInitialize] public void Setup() { _mockApi = new Mock(); _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())) .Returns(CreateMockChannels()); // Act var result = _manager.PlanPath(start, end); // Assert Assert.IsNotNull(result); Assert.IsTrue(result.Points.Count > 0); } } ``` ### 适用场景 - 核心算法验证 - API调用测试 - 业务逻辑验证 - 回归测试 ## 10. 性能监控与优化 ### 问题描述 插件性能问题需要及时发现和定位,特别是在大模型处理时。 ### 设计原则 #### 10.1 性能监控代码 ```csharp 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 缓存策略 ```csharp public class ModelDataCache { private static readonly Dictionary _cache = new Dictionary(); private static readonly TimeSpan DefaultTtl = TimeSpan.FromMinutes(10); public static T GetOrCreate(string key, Func 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 高效的渲染循环 ```csharp 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 防抖机制避免频繁刷新 ```csharp // ❌ 错误:每次操作都刷新视图 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 数据结构优化 ```csharp // ✅ 使用高效的数据结构 public class OptimizedRenderPlugin : RenderPlugin { // 使用数组存储频繁访问的数据 private CircleMarker[] _cachedMarkers = new CircleMarker[0]; private bool _cacheInvalid = true; // 批量更新机制 public void BatchUpdateMarkers(IEnumerable 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 智能位置修正 ```csharp // ❌ 错误:严格验证,用户体验差 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最近邻搜索算法 ```csharp private Point2D? FindNearestWalkablePosition(GridMap gridMap, Point2D originalPos, string positionName, int maxDistance = 10) { // 如果原始位置已经可通行,直接返回 if (gridMap.IsWalkable(originalPos)) { return originalPos; } // 使用BFS搜索最近的可通行位置 var visited = new HashSet(); 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 渐进式错误处理 ```csharp 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 用户反馈机制 ```csharp // 在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 设计理念冲突 ```csharp // ❌ 问题:自定义线程安全集合与WPF冲突 public class ThreadSafeObservableCollection : ObservableCollection { // 内部实现复杂的UI线程marshaling private void OnCollectionChanged() { // 自定义的UI线程处理机制 _uiStateManager.QueueUIUpdate(() => { base.OnCollectionChanged(...); }); } } // ✅ 解决方案:使用WPF标准集合 public ObservableCollection PathRoutes { get; set; } = new ObservableCollection(); ``` **核心冲突**: - `ThreadSafeObservableCollection`试图提供线程安全,通过内部机制自动将变更marshaling到UI线程 - WPF数据绑定期望使用标准的`ObservableCollection`,由框架本身处理UI线程marshaling - 双重UI线程处理机制导致重复通知和不可预测的行为 #### 13.2 异步处理复杂性 ```csharp // ❌ 导致问题的异步初始化机制 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 事件处理重叠 发现有三个机制同时处理相同的数据变更: ```csharp // 机制1:手动刷新 RefreshPathRoutes() // 从Core数据创建UI路径点 // 机制2:事件响应 OnPathPointsListUpdated() // 响应路径点更新事件 // 机制3:路径生成事件 OnRouteGenerated() // 处理自动路径生成 // 结果:同样的路径点被多次添加到UI ``` ### 完整解决方案 #### 13.4 回归WPF标准实践 ```csharp // ✅ 正确做法:使用标准ObservableCollection public class PathEditingViewModel : ViewModelBase { // 路径集合使用标准集合 public ObservableCollection PathRoutes { get; private set; } = new ObservableCollection(); } public class PathRouteViewModel : ViewModelBase { // 路径点集合也使用标准集合 public ObservableCollection Points { get; private set; } = new ObservableCollection(); // 简化初始化:同步完成,无异步复杂性 public PathRouteViewModel() { InitializeDefaults(); CompleteInitialization(); } private void CompleteInitialization() { // 直接订阅事件,无需异步 Points.CollectionChanged += OnPointsCollectionChanged; _isInitialized = true; } } ``` #### 13.5 职责分离和重复检查 ```csharp // ✅ 事件处理器包含重复检查逻辑 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`已经是充分测试的成熟解决方案 ```csharp // ❌ 过度设计:复杂的自定义线程安全集合 public class ThreadSafeObservableCollection : ObservableCollection { 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 Items { get; private set; } = new ObservableCollection(); ``` #### 13.7 线程安全的正确处理方式 在WPF中,正确的线程安全做法是: - **数据操作**在适当的线程中执行 - **UI更新**统一通过`Dispatcher.Invoke`或`UIStateManager`在UI线程执行 - **不要在集合层面**实现线程安全,而是在操作层面控制 ```csharp // ✅ 正确的线程安全模式 public async Task AddPathAsync(PathRouteViewModel path) { await _uiStateManager.ExecuteUIUpdateAsync(() => { PathRoutes.Add(path); // 在UI线程上操作标准集合 }); } ``` #### 13.8 调试复杂问题的方法论 从这个案例中学到的调试方法: 1. **详细日志追踪**:记录事件时序和调用栈 2. **识别异步副作用**:关注"火后不理"的异步调用 3. **分析处理机制重叠**:多个组件处理相同数据的情况 4. **回归简单方案**:当复杂方案出问题时,考虑标准做法 ### 设计原则总结 #### 13.9 集合使用原则 ```csharp // ✅ WPF UI绑定:使用标准ObservableCollection public ObservableCollection Items { get; private set; } = new ObservableCollection(); // ✅ 后台数据处理:可以使用线程安全集合 private readonly ConcurrentBag _processingQueue = new ConcurrentBag(); // ✅ UI更新时:统一在UI线程操作 public async Task UpdateUI(List newData) { await _uiStateManager.ExecuteUIUpdateAsync(() => { Items.Clear(); foreach (var item in newData) { Items.Add(new ItemViewModel(item)); } }); } ``` #### 13.10 ViewModel设计原则 ```csharp // ✅ 保持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 死锁问题的根本原因 ```csharp // ❌ 导致死锁的典型模式 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 修复模式标准化 ```csharp // 标准修复模式 - 适用于简单控件 private void SafeUpdateControl(T control, Action 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 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 性能优化考虑 ```csharp // ✅ 减少跨线程调用的开销 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 调试和监控 ```csharp // 添加线程安全检查的调试代码 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"`,确保窗口始终置顶: ```xml ``` **参考实现**:`GenerateNavigationMapDialog.xaml`、`EditRotationWindow.xaml`、`AerialHeightDialog.xaml` #### 15.2 代码层解决方案(备用) 在ViewModel中动态查找Owner窗口: ```csharp // ✅ 正确:多重备用方案查找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设置 ```csharp // ✅ 正确:为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)`: ```csharp // ✅ 正确:从UserControl获取父窗口 private void ShowHelpDialog() { var helpDialog = new Views.HelpDialog { Owner = Window.GetWindow(this) // 关键:获取当前UserControl所在的Window }; helpDialog.ShowDialog(); } ``` **参考实现**:`LogisticsControlPanel.xaml.cs`、`PathEditingView.xaml.cs` ### 实际修复案例 #### 案例1:CollisionAnalysisDialog(预计算碰撞分析) **问题**:打开大模型点击生成动画后,碰撞结果分析窗口藏在主窗口背后,无法点击 **修复**: 1. XAML添加 `Topmost="True"` 2. ViewModel添加备用Owner查找逻辑 ```xml ``` ```csharp // AnimationControlViewModel.cs - ShowCollisionAnalysisDialog方法 // 添加备用Owner查找逻辑(详见15.2节) ``` #### 案例2:重复配置提示MessageBox **问题**:检测配置重复提示的MessageBox也可能被主窗口遮挡 **修复**: ```csharp // 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开发中常见的运行时错误。 ### 错误示例 ```xml Visibility="{Binding HasItems, Converter={StaticResource InverseBoolToVisibilityConverter}}" Visibility="{Binding HasItems, Converter={StaticResource BoolToVisibilityConverter}, ConverterParameter=Inverse}" ``` ### 项目中已定义的资源 | 资源名 | 类型 | 说明 | |--------|------|------| | `BoolToVisibilityConverter` | BoolToVisibilityConverter | 布尔转可见性,支持Inverse参数 | ### 开发工作流程 1. **每添加一个资源引用,立即确认其存在** ``` ❌ 错误做法: - 复制其他文件的XAML代码 - 一次性写大量XAML再测试 ✅ 正确做法: - 每写一行 {StaticResource xxx},立即检查 xxx 是否已定义 - 使用 Ctrl+F 搜索 xxx 确认在当前文件或合并字典中存在 ``` 2. **资源定义位置优先级**: - 本文件 `` 或 `` 内定义 - 引用的外部资源字典(如 `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` 提供了统一的对话框处理方法: ```csharp // 使用方式1:Code-Behind 中从 UserControl 获取 Owner var dialog = new MyDialog(); DialogHelper.SetOwnerFromUserControl(dialog, this); dialog.ShowDialog(); // 使用方式2:ViewModel 中自动查找 Owner var dialog = new MyDialog(); DialogHelper.SetOwnerSafely(dialog); dialog.ShowDialog(); // 使用方式3:一键显示对话框(自动处理 Owner) var result = DialogHelper.ShowDialog(new MyDialog(), this); // 使用方式4:MessageBox 确保置顶 var result = DialogHelper.ShowMessageBox( "确认删除?", "提示", MessageBoxButton.YesNo, MessageBoxImage.Question); ``` ### 方法说明 | 方法 | 场景 | 说明 | |------|------|------| | `SetOwnerFromUserControl` | Code-Behind | 从 UserControl 获取父窗口 | | `FindOwnerWindow` | ViewModel | 查找合适的 Owner(MainWindow -> 活动窗口) | | `SetOwnerSafely` | ViewModel | 安全设置 Owner,带异常处理 | | `ShowDialog` | 通用 | 自动设置 Owner 并显示对话框 | | `ShowMessageBox` | 通用 | 显示置顶 MessageBox | | `SetWin32Owner` | 特殊 | 设置 Win32 父窗口(Navisworks 主窗口) | ### 迁移示例 **旧代码(重复且冗长)**: ```csharp // 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(); ``` ```csharp // 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(); ``` **新代码(简洁统一)**: ```csharp // Code-Behind DialogHelper.ShowDialog(new HelpDialog(), this); // ViewModel DialogHelper.ShowDialog(new MyDialog()); ``` ### 设计原则 1. **代码复用优先**:使用 `DialogHelper` 替代重复的 Owner 设置代码 2. **渐进式降级**:自动尝试多种方式获取 Owner(MainWindow -> 活动窗口 -> 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` 恢复分支 --- *本文档将随着项目发展持续更新,确保设计指导的有效性和实用性。*