335 lines
12 KiB
Markdown
335 lines
12 KiB
Markdown
# Command框架同步/异步双模式设计方案
|
||
|
||
## 文档信息
|
||
|
||
- **创建日期**:2026-01-14
|
||
- **状态**:待实施
|
||
- **优先级**:中
|
||
- **相关文件**:
|
||
- `src/Commands/CommandBase.cs`
|
||
- `src/Commands/IPathPlanningCommand.cs`
|
||
|
||
## 1. 当前问题
|
||
|
||
### 1.1 问题描述
|
||
|
||
Command框架要求所有Command的 `ExecuteInternalAsync` 方法必须返回 `Task<PathPlanningResult>`,导致:
|
||
|
||
1. **伪异步Command**:很多Command标记为 `async` 但内部没有 `await` 操作
|
||
2. **CS1998编译警告**:编译器提示"此异步方法缺少await运算符"
|
||
3. **代码意图不清晰**:无法区分哪些Command真正需要异步,哪些只是同步操作
|
||
|
||
### 1.2 受影响的Command(8个)
|
||
|
||
| Command | 文件 | 行号 | 风险等级 | 原因 |
|
||
|---------|------|------|---------|------|
|
||
| FloorAnalysisCommand.ExecuteAsync | LayerManagementCommands.cs | 154 | 极高 | 直接调用Navisworks COM API |
|
||
| PreviewSplitCommand.ExecuteAsync | LayerManagementCommands.cs | 410 | 极高 | 直接调用Navisworks COM API |
|
||
| VoxelPathFindingTestCommand.ExecuteInternalAsync | VoxelPathFindingTestCommand.cs | 55 | 高 | 内部调用Navisworks COM API |
|
||
| GenerateCollisionReportCommand.ExecuteInternalAsync | GenerateCollisionReportCommand.cs | 132 | 中 | 可能包含COM API |
|
||
| ProcessSingleLayerAsync | LayerManagementViewModel.cs | 1549 | 低 | 已正确处理线程安全 |
|
||
| ProcessSingleLayerAsync | LayerManagementViewModel.cs | 2255 | 低 | 已正确处理线程安全 |
|
||
| ProcessSingleLayerAsync | ModelSplitterManager.cs | 1128 | 低 | 已正确处理线程安全 |
|
||
| VoxelGridSDFTestCommand.ExecuteInternalAsync | VoxelGridSDFTestCommand.cs | 53 | 高 | 内部调用Navisworks COM API |
|
||
|
||
### 1.3 真正异步的Command示例
|
||
|
||
**AutoPathPlanningCommand**:
|
||
```csharp
|
||
protected override async Task<PathPlanningResult> ExecuteInternalAsync(CancellationToken cancellationToken)
|
||
{
|
||
// 使用await进行真正的异步操作
|
||
await _uiStateManager.ExecuteUIUpdateAsync(() => { });
|
||
var pathPlanTask = _pathPlanningManager.AutoPlanPath(...);
|
||
generatedRoute = await pathPlanTask;
|
||
}
|
||
```
|
||
|
||
**SetLogisticsAttributeCommand**:
|
||
```csharp
|
||
protected override async Task<PathPlanningResult> ExecuteInternalAsync(CancellationToken cancellationToken)
|
||
{
|
||
// 使用Task.Run在后台线程执行
|
||
await Task.Run(() => {
|
||
// 批量设置物流属性
|
||
});
|
||
}
|
||
```
|
||
|
||
## 2. 设计方案
|
||
|
||
### 2.1 核心思路
|
||
|
||
**同步/异步双模式**:允许Command选择性地实现同步或异步版本,框架自动选择合适的执行方式。
|
||
|
||
### 2.2 接口设计
|
||
|
||
```csharp
|
||
public abstract class CommandBase : IPathPlanningCommand
|
||
{
|
||
/// <summary>
|
||
/// 同步执行命令(默认实现)
|
||
/// 子类可以重写此方法以提供同步实现
|
||
/// </summary>
|
||
protected virtual PathPlanningResult ExecuteInternal(CancellationToken cancellationToken)
|
||
{
|
||
throw new NotImplementedException(
|
||
$"Command {DisplayName} 必须实现 ExecuteInternal 或 ExecuteInternalAsync");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步执行命令(可选实现)
|
||
/// 子类可以重写此方法以提供异步实现
|
||
/// 如果子类重写了此方法,框架将优先使用异步版本
|
||
/// </summary>
|
||
protected virtual Task<PathPlanningResult> ExecuteInternalAsync(CancellationToken cancellationToken)
|
||
{
|
||
// 默认实现:将同步方法包装为异步
|
||
return Task.FromResult(ExecuteInternal(cancellationToken));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断Command是否支持异步执行
|
||
/// </summary>
|
||
protected virtual bool SupportsAsyncExecution
|
||
{
|
||
get
|
||
{
|
||
// 检查子类是否重写了ExecuteInternalAsync
|
||
var asyncMethod = GetType().GetMethod(
|
||
"ExecuteInternalAsync",
|
||
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||
var baseMethod = typeof(CommandBase).GetMethod(
|
||
"ExecuteInternalAsync",
|
||
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||
|
||
return asyncMethod?.DeclaringType != baseMethod?.DeclaringType;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步执行命令(框架入口)
|
||
/// </summary>
|
||
public async Task<PathPlanningResult> ExecuteAsync(CancellationToken cancellationToken = default)
|
||
{
|
||
if (Status == CommandExecutionStatus.Executing)
|
||
{
|
||
return PathPlanningResult.Failure("命令正在执行中,请勿重复执行");
|
||
}
|
||
|
||
var stopwatch = Stopwatch.StartNew();
|
||
|
||
try
|
||
{
|
||
// 创建内部取消令牌源,组合外部令牌
|
||
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||
var combinedToken = _cancellationTokenSource.Token;
|
||
|
||
// 验证阶段
|
||
Status = CommandExecutionStatus.Validating;
|
||
UpdateProgress(0, "正在验证命令参数...");
|
||
|
||
var validationResult = await ValidateAsync(combinedToken);
|
||
if (!validationResult.IsSuccess)
|
||
{
|
||
Status = CommandExecutionStatus.Failed;
|
||
validationResult.ElapsedMilliseconds = stopwatch.ElapsedMilliseconds;
|
||
return validationResult;
|
||
}
|
||
|
||
// 执行阶段
|
||
Status = CommandExecutionStatus.Executing;
|
||
UpdateProgress(10, "开始执行命令...");
|
||
|
||
PathPlanningResult result;
|
||
|
||
// 根据Command是否支持异步,选择执行方式
|
||
if (SupportsAsyncExecution)
|
||
{
|
||
LogManager.Info($"命令 {DisplayName} 使用异步执行模式");
|
||
result = await ExecuteInternalAsync(combinedToken);
|
||
}
|
||
else
|
||
{
|
||
LogManager.Info($"命令 {DisplayName} 使用同步执行模式");
|
||
result = ExecuteInternal(combinedToken);
|
||
}
|
||
|
||
// 设置执行时间
|
||
result.ElapsedMilliseconds = stopwatch.ElapsedMilliseconds;
|
||
|
||
// 根据结果设置状态
|
||
if (combinedToken.IsCancellationRequested)
|
||
{
|
||
Status = CommandExecutionStatus.Cancelled;
|
||
return PathPlanningResult.Failure("命令执行已取消");
|
||
}
|
||
else if (result.IsSuccess)
|
||
{
|
||
Status = CommandExecutionStatus.Completed;
|
||
UpdateProgress(100, "命令执行完成");
|
||
}
|
||
else
|
||
{
|
||
Status = CommandExecutionStatus.Failed;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
Status = CommandExecutionStatus.Cancelled;
|
||
LogManager.Error($"命令 {DisplayName} 执行被取消");
|
||
return PathPlanningResult.Failure("命令执行已取消");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Status = CommandExecutionStatus.Failed;
|
||
LogManager.Error($"命令 {DisplayName} 执行出现异常", ex);
|
||
return PathPlanningResult.Failure($"命令执行失败: {ex.Message}", ex);
|
||
}
|
||
finally
|
||
{
|
||
stopwatch.Stop();
|
||
_cancellationTokenSource?.Dispose();
|
||
_cancellationTokenSource = null;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2.3 使用示例
|
||
|
||
#### 示例1:同步Command(伪异步Command改造)
|
||
|
||
**改造前**:
|
||
```csharp
|
||
public class VoxelPathFindingTestCommand : CommandBase
|
||
{
|
||
protected override async Task<PathPlanningResult> ExecuteInternalAsync(CancellationToken cancellationToken)
|
||
{
|
||
// CS1998警告:此异步方法缺少await运算符
|
||
var spaceBounds = GetSpaceBounds(_document);
|
||
var allModelItems = GetAllModelItems(_document);
|
||
var voxelGrid = generator.GenerateFromBIMWithSDF(...);
|
||
return PathPlanningResult.Success("完成");
|
||
}
|
||
}
|
||
```
|
||
|
||
**改造后**:
|
||
```csharp
|
||
public class VoxelPathFindingTestCommand : CommandBase
|
||
{
|
||
protected override PathPlanningResult ExecuteInternal(CancellationToken cancellationToken)
|
||
{
|
||
// 同步执行,不再有CS1998警告
|
||
var spaceBounds = GetSpaceBounds(_document);
|
||
var allModelItems = GetAllModelItems(_document);
|
||
var voxelGrid = generator.GenerateFromBIMWithSDF(...);
|
||
return PathPlanningResult.Success("完成");
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 示例2:异步Command(保持不变)
|
||
|
||
```csharp
|
||
public class AutoPathPlanningCommand : CommandBase
|
||
{
|
||
protected override async Task<PathPlanningResult> ExecuteInternalAsync(CancellationToken cancellationToken)
|
||
{
|
||
// 真正的异步操作
|
||
await _uiStateManager.ExecuteUIUpdateAsync(() => { });
|
||
var pathPlanTask = _pathPlanningManager.AutoPlanPath(...);
|
||
generatedRoute = await pathPlanTask;
|
||
return PathPlanningResult.Success("完成");
|
||
}
|
||
}
|
||
```
|
||
|
||
## 3. 优缺点分析
|
||
|
||
### 3.1 优点
|
||
|
||
1. ✅ **消除CS1998警告**:同步Command不需要 `async` 关键字
|
||
2. ✅ **代码意图清晰**:通过方法签名明确表达同步/异步意图
|
||
3. ✅ **向后兼容**:异步Command不需要修改
|
||
4. ✅ **灵活性**:Command可以选择最适合自己的执行模式
|
||
5. ✅ **类型安全**:编译时检查,避免运行时错误
|
||
|
||
### 3.2 缺点
|
||
|
||
1. ❌ **需要修改基类**:需要修改 `CommandBase.cs`
|
||
2. ❌ **增加复杂度**:需要维护同步和异步两套方法
|
||
3. ❌ **反射开销**:`SupportsAsyncExecution` 使用反射判断(可以优化为虚方法)
|
||
|
||
### 3.3 风险评估
|
||
|
||
| 风险 | 影响 | 概率 | 缓解措施 |
|
||
|------|------|------|---------|
|
||
| 破坏现有Command | 高 | 低 | 向后兼容设计 |
|
||
| 性能开销 | 低 | 中 | 优化反射判断逻辑 |
|
||
| 代码复杂度增加 | 中 | 高 | 添加详细注释和文档 |
|
||
|
||
## 4. 实施计划
|
||
|
||
### 4.1 阶段一:基类修改
|
||
|
||
1. 修改 `CommandBase.cs`:
|
||
- 添加 `ExecuteInternal` 虚方法
|
||
- 修改 `ExecuteInternalAsync` 默认实现
|
||
- 添加 `SupportsAsyncExecution` 属性
|
||
- 修改 `ExecuteAsync` 方法
|
||
|
||
2. 更新 `IPathPlanningCommand.cs`(如果需要)
|
||
|
||
### 4.2 阶段二:Command改造
|
||
|
||
按风险等级逐步改造:
|
||
|
||
**第一批(低风险)**:
|
||
- LayerManagementViewModel.ProcessSingleLayerAsync (2处)
|
||
- ModelSplitterManager.ProcessSingleLayerAsync (1处)
|
||
|
||
**第二批(高风险)**:
|
||
- LayerManagementCommands.ExecuteAsync (2处)
|
||
- VoxelPathFindingTestCommand.ExecuteInternalAsync (1处)
|
||
- VoxelGridSDFTestCommand.ExecuteInternalAsync (1处)
|
||
- GenerateCollisionReportCommand.ExecuteInternalAsync (1处)
|
||
|
||
### 4.3 阶段三:测试验证
|
||
|
||
1. 单元测试:测试同步和异步Command都能正常工作
|
||
2. 集成测试:测试Command框架的执行流程
|
||
3. 回归测试:确保所有现有功能正常
|
||
|
||
### 4.4 阶段四:文档更新
|
||
|
||
1. 更新Command框架文档
|
||
2. 添加Command开发指南
|
||
3. 更新代码注释
|
||
|
||
## 5. 替代方案对比
|
||
|
||
| 方案 | 优点 | 缺点 | 推荐度 |
|
||
|------|------|------|--------|
|
||
| 方案1:同步/异步双模式 | 消除警告、意图清晰 | 需要修改基类 | ⭐⭐⭐⭐⭐ |
|
||
| 方案2:Task.FromResult | 简单 | 仍是伪异步 | ⭐⭐⭐ |
|
||
| 方案3:明确注释 | 不改代码 | 仍有警告 | ⭐⭐ |
|
||
| 方案4:pragma warning | 消除警告 | 隐藏问题 | ⭐ |
|
||
|
||
## 6. 总结
|
||
|
||
**推荐方案**:方案1(同步/异步双模式)
|
||
|
||
**理由**:
|
||
1. 从根本上解决问题,而不是隐藏问题
|
||
2. 提高代码可读性和可维护性
|
||
3. 为未来的Command开发提供更好的模式
|
||
4. 向后兼容,风险可控
|
||
|
||
**注意事项**:
|
||
1. 实施前需要充分测试
|
||
2. 需要更新相关文档
|
||
3. 需要团队培训新的使用模式 |