1779 lines
48 KiB
Markdown
1779 lines
48 KiB
Markdown
# 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<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 事件处理器中的线程安全
|
||
|
||
```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}");
|
||
}
|
||
}
|
||
```
|
||
|
||
### 适用场景
|
||
|
||
- 所有对外接口方法
|
||
- 事件处理器
|
||
- 后台任务执行
|
||
- Navisworks API调用
|
||
|
||
## 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<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 事件定义规范
|
||
|
||
```csharp
|
||
// ✅ 标准事件定义
|
||
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 事件订阅管理
|
||
|
||
```csharp
|
||
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调用封装
|
||
|
||
```csharp
|
||
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协调使用
|
||
|
||
```csharp
|
||
// ✅ 正确的双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 状态枚举定义
|
||
|
||
```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<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 配置文件结构
|
||
|
||
```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<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 数据迁移机制
|
||
|
||
```csharp
|
||
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 日志级别使用
|
||
|
||
```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, vehicleSize);
|
||
StructuredLogger.LogOperation("AutoPlanPath",
|
||
new { start, end, vehicleSize },
|
||
stopwatch.Elapsed, true);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
StructuredLogger.LogOperation("AutoPlanPath",
|
||
new { start, end, vehicleSize },
|
||
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<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 性能监控代码
|
||
|
||
```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, vehicleSize);
|
||
}
|
||
```
|
||
|
||
#### 10.2 缓存策略
|
||
|
||
```csharp
|
||
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 高效的渲染循环
|
||
```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<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 智能位置修正
|
||
```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<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 渐进式错误处理
|
||
```csharp
|
||
public PathRoute AutoPlanPath(Point3D startPoint, Point3D endPoint,
|
||
double vehicleSize = 1.0, double safetyMargin = 0.5)
|
||
{
|
||
try
|
||
{
|
||
// 第一次尝试:使用原始参数
|
||
return PlanPathInternal(startPoint, endPoint, vehicleSize, safetyMargin);
|
||
}
|
||
catch (AutoPathPlanningException ex) when (ex.Message.Contains("位于障碍物上"))
|
||
{
|
||
LogManager.Info("使用智能修正重试路径规划...");
|
||
|
||
try
|
||
{
|
||
// 第二次尝试:启用智能修正
|
||
return PlanPathWithCorrection(startPoint, endPoint, vehicleSize, safetyMargin);
|
||
}
|
||
catch (Exception innerEx)
|
||
{
|
||
LogManager.Warning($"智能修正也失败: {innerEx.Message}");
|
||
|
||
// 第三次尝试:降低精度重试
|
||
var reducedMargin = safetyMargin * 0.5;
|
||
var reducedSize = vehicleSize * 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<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 异步处理复杂性
|
||
|
||
```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<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 职责分离和重复检查
|
||
|
||
```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<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.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<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设计原则
|
||
|
||
```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>(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 性能优化考虑
|
||
|
||
```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 |
|
||
|
||
*本文档将随着项目发展持续更新,确保设计指导的有效性和实用性。*
|