本文档定义了 Universal Device Toolkit Plugins 项目的代码风格和最佳实践,确保代码质量、可维护性和团队协作一致性。
项目根目录包含 .editorconfig 文件,Visual Studio 和 VS Code 会自动应用这些设置。
| 规则 | 约束 | 级别 |
|---|---|---|
| 私有字段命名 | _camelCase(下划线前缀) |
warning |
| 接口命名 | IInterfaceName(I前缀) |
suggestion |
| 文件作用域命名空间 | file_scoped namespace |
warning |
| 空检查 | pattern matching |
warning |
| 大括号要求 | 所有代码块必须使用大括号 | warning |
| 修饰符要求 | 所有访问修饰符必须显式声明 | warning |
| readonly字段 | 应标记为 readonly |
warning |
| 语句末尾 | 不使用分号 | - |
用于:
- 类名:
public class ViveToolService - 接口名:
public interface IViveToolService - 方法名:
public void StartRuntime() - 属性名:
public string PluginName - 公共字段:
public const int DefaultTimeout - 命名空间:
namespace UniversalDeviceToolkit.Plugins.ViveTool
用于:
- 局部变量:
var pluginId = "custom-mouse" - 参数:
void Configure(string pluginId) - 私有字段:
private readonly HttpClient _httpClient
用于:
- 私有实例字段:
private readonly HttpClientManager _httpClientManager - 静态私有字段:
private static readonly Lazy<HttpClient> _sharedClient
using System;
using System.Threading.Tasks;
using UniversalDeviceToolkit.Plugins.Shared;
namespace UniversalDeviceToolkit.Plugins.ViveTool.Services;
public class ViveToolFeatureService : IViveToolFeatureService
{
// 类成员按以下顺序组织:
// 1. 常量
// 2. 私有字段
// 3. 公共属性
// 4. 构造函数
// 5. 公共方法
// 6. 私有方法
// 7. 事件处理器
}- System 命名空间
- Microsoft 命名空间
- 第三方命名空间
- 项目命名空间
using System;
using System.IO;
using Microsoft.Extensions.Logging;
using Wpf.Ui;
using UniversalDeviceToolkit.Plugins.Shared;必须遵循:
- 所有异步方法返回
Task或Task<T> - 禁止使用
async void(除了事件处理器) - 使用
ConfigureAwait(false)在库代码中 - 不使用
Task.Wait()或Task.Result
// ✅ 正确
public async Task InitializeAsync()
{
await DownloadAsync().ConfigureAwait(false);
}
// ❌ 错误
public async void InitializeAsync()
{
await DownloadAsync(); // async void,异常无法捕获
}
// ❌ 错误
public void Initialize()
{
DownloadAsync().Wait(); // 阻塞调用
}所有长时间运行的操作必须支持取消:
public async Task RunAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
await ProcessAsync(cancellationToken).ConfigureAwait(false);
}
}事件处理器是唯一允许 async void 的地方,但必须添加异常处理:
private async void OnButtonClick(object sender, RoutedEventArgs e)
{
try
{
await ProcessAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Button click handler failed");
}
}- 禁止空 catch 块:必须记录或处理异常
- 区分异常级别:Error、Warning、Info
- 特定异常优先:先捕获具体异常,再捕获通用异常
// ✅ 正确
try
{
await ProcessAsync();
}
catch (IOException ex)
{
_logger.LogWarning(ex, "IO operation failed, using fallback");
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error during processing");
throw;
}
// ❌ 错误
try
{
await ProcessAsync();
}
catch { } // 空catch块吞掉异常- 使用
throw而不是throw ex保持堆栈跟踪 - 验证参数使用
ArgumentNullException.ThrowIfNull
// ✅ 正确
public void Configure(string path)
{
ArgumentNullException.ThrowIfNull(path);
if (!File.Exists(path))
throw new FileNotFoundException($"Configuration file not found: {path}", path);
}
// ❌ 错误
catch (Exception ex)
{
// Do something
throw ex; // 丢失原始堆栈跟踪
}使用 HttpClientManager 而不是创建新实例:
// ✅ 正确
private static readonly HttpClient _httpClient = HttpClientManager.GetSharedClient();
// ❌ 错误
using var client = new HttpClient(); // Socket耗尽风险使用 using 或 using var:
// ✅ 正确
using var stream = File.OpenRead(path);
await stream.ReadAsync(buffer);
// ✅ 正确
using (var stream = File.OpenRead(path))
{
await stream.ReadAsync(buffer);
}禁止手动创建进程,使用 ProcessRunner:
// ✅ 正确
var result = await ProcessRunner.RunAsync(
"tool.exe",
"--feature 123",
timeoutSeconds: 30
);
// ❌ 错误
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "tool.exe",
Arguments = userInput // 命令注入风险
}
};所有 UI 控件必须使用 WpfFallbackHelper:
public class ViveToolPage : Page, IPluginPage
{
public ViveToolPage()
{
WpfFallbackHelper.TryInitializeComponent(this, BuildFallbackUi);
}
private void BuildFallbackUi()
{
var panel = new StackPanel();
// 构建回退UI
this.Content = panel;
}
}插件 UI 的视觉值只能取自 Plugins/Shared/DesignTokens.xaml 与宿主画刷,XAML 与 C# BuildFallbackUi 同等约束。本节未列出的值一律不允许出现。
只允许以下 7 个值(XAML 与 C# 回退 UI 同等约束):
| 值 | 令牌 | 用途 |
|---|---|---|
| 0 | CornerRadiusNone |
直角 |
| 3 | CornerRadiusProgressBar |
进度条 |
| 8 | CornerRadiusCompact / PluginCornerRadiusCompact |
小贴片(chips) |
| 12 | CornerRadiusControl / PluginCornerRadiusControl |
按钮、输入框 |
| 18 | CornerRadiusCard / PluginCornerRadiusCard |
卡片容器 |
| 20 | CornerRadiusSurface / PluginCornerRadiusSurface |
页面级表面 |
| 999 | CornerRadiusRound |
圆形/胶囊 |
<!-- ✅ 正确 -->
<Border CornerRadius="{StaticResource CornerRadiusCard}" />
<!-- ❌ 错误:词汇外数值(4/6/9/10/24 等),XAML 与 C# 中均禁止 -->
<Border CornerRadius="10" />- 间距只允许 4/8/12/16/24,对应令牌
PluginSpacingXXS/PluginSpacingXS/PluginSpacingSM/PluginSpacingMD/PluginSpacingLG - 卡片内边距统一
PluginCardPadding(16,14) - 卡片之间的外边距用
PluginSectionMargin(0,0,0,16)
只允许四档:PluginFontSizeCaption(12)、PluginFontSizeBody(14)、PluginFontSizeSection(15)、PluginFontSizeMetric(28),插件内不得出现其他 FontSize 值。唯一例外是 SymbolIcon 图标字号,走 PluginIconSizeMD(18)/ PluginIconSizeLG(24)。
- 禁止在插件 UI 中硬编码 hex 颜色;颜色一律以
DynamicResource引用宿主画刷 - 文本用三级 TextFill 刷:
TextFillColorPrimaryBrush/TextFillColorSecondaryBrush/TextFillColorTertiaryBrush - 状态用语义刷:
SystemFillColorSuccessBrush/SystemFillColorCautionBrush/SystemFillColorCriticalBrush,需要底色时用对应的*BackgroundBrush变体
插件默认不加阴影、不加动画/Storyboard。确有需求时只能引用宿主令牌,不得自造值。
- 长操作必须有进度反馈并禁用动作按钮:耗时可预期的用
ProgressBar确定性进度,不可预期的用ProgressRing(IsIndeterminate);操作期间动作按钮IsEnabled=false - 空列表必须提供三段式空态:图标 + 标题 + 描述
- 参考实现:
Plugins/ViveTool/ViveToolPage.xaml的_loadingPanel(:269)与_emptyStatePanel(:392)
BuildFallbackUi 可以简化布局,但视觉值必须与 XAML 同源:通过 TryFindResource 取令牌并给出字面回退值(模式见 Plugins/Shared/WpfFallbackHelper.cs:84-87),禁止魔法数字:
// ✅ 正确
Foreground = Application.Current?.TryFindResource("TextFillColorPrimaryBrush") as Brush
?? Brushes.Black;
// ❌ 错误:魔法数字与硬编码画刷
FontSize = 16;
Foreground = Brushes.Green;每个插件 UI 根元素必须合并 Shared 两个字典,且不得在本地重定义其中的键:
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/UniversalDeviceToolkit.Plugins.Shared;component/DesignTokens.xaml" />
<ResourceDictionary Source="pack://application:,,,/UniversalDeviceToolkit.Plugins.Shared;component/PluginUiStyles.xaml" />
</ResourceDictionary.MergedDictionaries>- 所有用户可见字符串必须通过资源文件
- 英文回退值作为默认值
- 禁止硬编码中文字符串
// ✅ 正确
public static class ViveToolText
{
private const string DefaultPluginName = "ViveTool Plugin";
public static string PluginName =>
Resources.Resource.ResourceManager.GetString(nameof(PluginName))
?? DefaultPluginName;
}
// ❌ 错误
public static string PluginName = "ViveTool 插件"; // 硬编码中文Resources/
├── Resource.resx # 英文回退值
├── Resource.zh.resx # 中文翻译
└── Resource.zh-Hant.resx # 繁体翻译
- 方法名:
<Method>_<Scenario>_<Expected> - 类名:
<Class>Tests
public class HttpClientManagerTests
{
[Fact]
public void GetSharedClient_ReturnsNonNullInstance()
{
var client = HttpClientManager.GetSharedClient();
Assert.NotNull(client);
}
[Theory]
[InlineData(30)]
[InlineData(60)]
public void RunAsync_WithValidTimeout_ReturnsSuccess(int timeout)
{
// 测试逻辑
}
}- 单元测试:每个公共方法至少一个测试
- 边界测试:验证边界值和异常情况
- 集成测试:关键路径覆盖
private readonly ILogger<ViveToolService> _logger;
public async Task ProcessAsync()
{
_logger.LogInformation("Starting process");
try
{
await ExecuteAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Process failed");
throw;
}
}- Error: 严重错误,影响功能
- Warning: 潜在问题,可恢复
- Information: 生命周期和重要流程
- Debug: 详细调试信息(仅在开发环境)
- Trace: 最详细的跟踪信息(仅在开发环境)
所有数值常量应定义在 Constants.cs 或类内部:
// ✅ 正确
public class Constants
{
public const int DefaultTimeoutSeconds = 30;
public const int DownloadTimeoutSeconds = 120;
public const int DefaultBufferSize = 8192;
}
// ❌ 错误
await client.GetAsync(url); // 缺少timeout
var buffer = new byte[8192]; // 魔法数字公共 API 必须提供 XML 文档:
/// <summary>
/// Manages shared HttpClient instances to prevent socket exhaustion.
/// </summary>
public static class HttpClientManager
{
/// <summary>
/// Gets a shared HttpClient instance with default timeout settings.
/// </summary>
/// <returns>A singleton HttpClient instance.</returns>
public static HttpClient GetSharedClient() => _sharedClient.Value;
}仅在复杂逻辑处添加注释,代码应自文档化:
// ✅ 正确 - 解释非显而易见的逻辑
// ViVeTool requires feature ID in format: "FeatureID:State"
var featureArg = $"{featureId}:{(enable ? "Enable" : "Disable")}";
// ❌ 错误 - 显而易见的逻辑
// Get the plugin name
var name = PluginName;每个类和方法只做一件事:
// ✅ 正确 - 拆分服务
ViveToolFeatureService // Feature操作
ViveToolDownloadService // 下载管理
ViveToolProcessService // 进程执行
// ❌ 错误 - 单一大类
ViveToolService // 1026行,包含所有功能消除重复代码,提取共享工具:
// ✅ 正确 - 使用共享库
WpfFallbackHelper.TryInitializeComponent(this, BuildFallbackUi);
// ❌ 错误 - 6处重复代码
try { InitializeComponent(); }
catch { BuildFallbackUi(); }- S: 单一职责原则
- O: 开闭原则(扩展开放,修改关闭)
- L: 里氏替换原则
- I: 接口隔离原则
- D: 依赖倒置原则
- 反射访问私有字段:破坏封装
- async void:异常无法捕获(除事件处理器)
- Task.Wait():阻塞调用
- 空 catch 块:吞掉异常
- 硬编码中文:本地化缺失
- 魔法数字:可读性差
- 手动 HttpClient 创建:socket耗尽
- 命令注入:安全漏洞
// ❌ 反模式集合
public class BadExample
{
// 反射破坏封装
private static readonly FieldInfo _tokenField =
typeof(PluginBase).GetField("RuntimeCancellationTokenSource");
// async void 异常无法捕获
public async void StartAsync() { ... }
// 阻塞调用
public void Run() { Task.Delay(1000).Wait(); }
// 空catch块
try { ... } catch { }
// 硬编码中文
public string Name = "插件名称";
// 魔法数字
await client.GetAsync(url); // 无timeout
// HttpClient实例泄漏
var client = new HttpClient();
// 命令注入
process.StartInfo.Arguments = userInput;
}dotnet build UniversalDeviceToolkit-Plugins.slndotnet test --collect:"XPlat Code Coverage"dotnet build .\UniversalDeviceToolkit-Plugins.sln -c Release
dotnet test .\UniversalDeviceToolkit-Plugins.sln -c ReleaseVisual Studio 或 Rider 内置分析器会自动检测大部分规则违反。