70 KiB
Penguin.AvaloniaUI Architecture Document
Version: 1.0 Date: 2025-10-16 Status: In Progress
Introduction
本文档定义了 Penguin.AvaloniaUI 控件库的完整架构,作为 AI 驱动开发的技术蓝图。
项目基于以下核心技术:
- UI 框架: Avalonia 11.3.7
- MVVM 和响应式: ReactiveUI.Avalonia 11.3.0
- 运行时: .NET 9.0
- 目标平台: Windows(主要)、Linux、macOS(次要)
架构范围涵盖:
- 控件组件设计(PropertyGrid、UserGuide 等)
- 主题和样式系统
- 项目结构和开发工作流
- 编码规范和测试策略
Starter Template or Existing Project
项目类型: 现有代码基础上的开发
当前状态:
- 项目结构已建立(
src/Penguin.AvaloniaUI/) - 核心依赖已配置(Avalonia 11.3.7、ReactiveUI 11.3.0)
- PRD 已完成,定义了 3 个 Epic 和 16 个 Story
- Monorepo 结构(控件库 + 示例应用 + 测试)
待评估项:
- Semi.Avalonia 样式库的可用性(Story 1.1 前置任务)
- 如不可用,将采用自定义样式系统
Change Log
| Date | Version | Description | Author |
|---|---|---|---|
| 2025-10-16 | 1.0 | 初始架构文档创建(基于 PRD v1.0) | Architect |
High Level Architecture
Technical Summary
Penguin.AvaloniaUI 是一个基于 Avalonia 11.3.7 的桌面控件库,专注于业务场景控件(PropertyGrid、UserGuide 等)。采用 ReactiveUI 实现 MVVM 模式和响应式交互,目标是为上位机和 AI 桌面应用提供开箱即用的复合控件。
核心特性:
- 业务场景控件:PropertyGrid(属性编辑)、UserGuide(新手引导)等高级控件
- 多主题系统:基于 Semi Design 和苹果颜色系统,支持浅色/暗色主题运行时切换
- 响应式架构:基于 ReactiveUI,支持 Command、Reactive、Event 混合使用
- 国际化预留:架构支持后续多语言扩展,MVP 阶段使用单一语言
- AOT 友好设计:架构考虑未来 AOT 编译支持,MVP 阶段允许使用反射
架构目标:
- 降低上位机和 AI 桌面应用的开发成本(减少 30-50% 重复工作)
- 提供专业级 UI 体验(信息密度优先、暗色模式友好)
- 确保良好的扩展性(开发者可通过 Template 和 Style 自定义外观)
Platform and Infrastructure Choice
开发平台:
- IDE:Visual Studio 2022 或 JetBrains Rider
- 运行时:.NET 9.0
- 版本控制:Git(本地或私有仓库)
- 包管理:NuGet
目标平台:
- 主要:Windows 10/11(桌面应用)
- 次要:Linux 桌面(Ubuntu 20.04+、Debian 11+,主要是工业平板场景)
- 可选:macOS(架构不排斥,但非 MVP 测试平台)
最小支持分辨率:1366x768 推荐分辨率:1920x1080 及以上
输入方式:
- 主要:鼠标 + 键盘
- 次要:触控笔(Linux Pad 场景)
- 不支持:多点触控手势
Repository Structure
结构类型:Monorepo
组织策略:
D:\32_avalonia.ui/
├── src/
│ ├── Penguin.AvaloniaUI/ # 核心控件库
│ ├── Penguin.AvaloniaUI.SourceGenerators/ # Source Generator(Post-MVP)
│ ├── Example/ # 示例应用
│ └── Penguin.AvaloniaUI.Tests/ # 单元测试
├── docs/ # 文档
│ ├── prd.md
│ ├── architecture.md
│ └── ...
└── .bmad-core/ # BMAD 框架配置
核心控件库内部结构(命名空间组织):
Penguin.AvaloniaUI.Controls- 业务场景控件(PropertyGrid、UserGuide 等)Penguin.AvaloniaUI.Layouts- 布局控件(TwoColumnLayout 等)Penguin.AvaloniaUI.Themes- 主题和样式系统Penguin.AvaloniaUI.Utils- 工具类(ThemeManager 等)
Rationale:
- Monorepo 简化控件库与示例应用的协同开发
- 依赖版本统一管理,避免版本冲突
- MVP 阶段项目数量少(3-4 个),复杂度可控
High Level Architecture Diagram
graph TB
subgraph "开发者应用"
App[Avalonia Application]
end
subgraph "Penguin.AvaloniaUI 控件库"
Controls[Controls Layer<br/>PropertyGrid, UserGuide]
Layouts[Layouts Layer<br/>TwoColumnLayout, Overlay]
Themes[Themes Layer<br/>ThemeManager, ColorSystem]
Utils[Utils Layer<br/>LocalizationManager, Helpers]
end
subgraph "基础框架"
Avalonia[Avalonia UI 11.3.7]
ReactiveUI[ReactiveUI 11.3.0]
NET[.NET 9.0]
end
App --> Controls
App --> Layouts
App --> Themes
Controls --> Layouts
Controls --> Themes
Controls --> Utils
Layouts --> Themes
Themes --> Avalonia
Controls --> ReactiveUI
Avalonia --> NET
ReactiveUI --> NET
style Controls fill:#4A90E2
style Layouts fill:#7ED321
style Themes fill:#F5A623
style Utils fill:#BD10E0
Architectural Patterns
-
MVVM (Model-View-ViewModel):所有控件遵循 MVVM 模式,使用 ReactiveUI 实现数据绑定和 Command
- Rationale:Avalonia 原生支持 MVVM,ReactiveUI 提供强大的响应式能力,降低复杂交互的实现难度
-
Templated Control Pattern:业务控件继承自
TemplatedControl,通过ControlTemplate分离逻辑和外观- Rationale:确保控件可定制,开发者可以通过 Style 和 Template 覆盖默认外观
-
Resource Dictionary 主题系统:主题通过
ResourceDictionary定义,运行时通过替换Application.Current.Resources实现切换- Rationale:Avalonia 标准机制,无需引入额外框架,主题切换自动传播到所有控件
-
Reactive Extensions (Rx):使用 ReactiveUI 的 Reactive 模式处理异步事件流和属性变化
- Rationale:简化复杂的事件处理逻辑(如 PropertyGrid 的属性变化监听、UserGuide 的步骤流转)
-
Dependency Injection (可选):MVP 阶段不强制 DI,ThemeManager 等服务可通过静态类或单例访问
- Rationale:控件库不需要复杂的 DI 容器,保持简单;Post-MVP 可根据需要引入
-
Attribute-Driven Configuration:PropertyGrid 通过 Attribute(如
[Category]、[Browsable])配置属性显示- Rationale:符合 .NET 生态习惯(类似 WinForms PropertyGrid),降低学习成本
-
Composition over Inheritance:复杂控件通过组合基础控件实现(如 UserGuide 组合 Overlay + RichTooltip)
- Rationale:提高代码复用性,避免深层继承带来的维护问题
Tech Stack
本表是项目的单一技术事实来源,所有开发必须使用这些确定的技术和版本。
Technology Stack Table
| Category | Technology | Version | Purpose | Rationale |
|---|---|---|---|---|
| 运行时 | .NET | 9.0 | 应用运行时环境 | 最新版本,支持最新 C# 特性,性能优异 |
| UI 框架 | Avalonia | 11.3.7 | 跨平台桌面 UI 框架 | 现代化、跨平台、活跃社区,不向后兼容旧版本 |
| MVVM 框架 | ReactiveUI.Avalonia | 11.3.0 | 响应式 MVVM 实现 | 强大的响应式能力,简化复杂交互逻辑,与 Avalonia 深度集成 |
| 状态管理 | ReactiveUI Observable | Built-in | 属性变化监听和数据流 | ReactiveUI 内置,无需额外依赖,统一状态管理方式 |
| 样式系统 | Semi.Avalonia + 自定义 | Latest | UI 样式和主题 | 基于 Semi Design,结合苹果颜色系统的自定义实现 |
| 颜色系统 | 苹果颜色系统(自定义实现) | N/A | 语义化颜色定义 | Primary/Secondary/Success/Warning/Error 等语义化颜色,支持浅色/暗色主题 |
| 布局系统 | Avalonia Layouts + 自定义 | Built-in + Custom | 控件布局 | Avalonia 内置 Panel 系统 + 自定义 TwoColumnLayout |
| 数据绑定 | Avalonia Binding | Built-in | XAML 数据绑定 | Avalonia 原生支持,与 ReactiveUI 无缝集成 |
| 单元测试 | xUnit | 2.6+ | 单元测试框架 | .NET 生态主流选择,简洁易用 |
| 包管理 | NuGet + 统一包版本管理 | Built-in | 依赖包管理 | 使用 Directory.Packages.props 统一管理包版本,避免版本冲突 |
| 版本控制 | Git | 2.40+ | 代码版本管理 | 行业标准 |
| CI/CD | 手动构建(MVP) | N/A | 持续集成/部署 | MVP 阶段不强制 CI/CD,Post-MVP 可引入 GitHub Actions |
| 代码格式化 | .editorconfig | Built-in | 代码风格统一 | 定义缩进、换行等规则 |
| 文档生成 | XML 文档注释 | Built-in | API 文档 | 所有 public API 必须有 XML 注释 |
| 国际化 | Avalonia IResourceProvider(预留) | Built-in | 多语言支持 | MVP 单一语言,架构预留扩展点 |
| 性能分析 | Visual Studio Profiler / dotnet-trace | Built-in | 性能测量 | 验证 NFR(60fps、100ms 主题切换、200ms PropertyGrid 生成) |
统一包版本管理配置
为了避免多项目间的包版本冲突,项目使用 Central Package Management。
需要在项目根目录创建 Directory.Packages.props:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="11.3.7" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.3.0" />
<PackageVersion Include="xUnit" Version="2.6.6" />
<PackageVersion Include="xUnit.runner.visualstudio" Version="2.5.6" />
</ItemGroup>
</Project>
各项目的 .csproj 文件中引用包时不指定版本号:
<ItemGroup>
<PackageReference Include="Avalonia" />
<PackageReference Include="ReactiveUI.Avalonia" />
</ItemGroup>
优势:
- 所有项目使用相同的包版本
- 升级版本时只需修改一处
- 避免版本冲突导致的编译或运行时错误
Data Models
核心数据模型定义了控件库中业务逻辑层的数据结构。这些模型在 UI 控件层和业务逻辑层之间共享。
PropertyItem
用途:表示 PropertyGrid 中的单个属性项,封装属性的元数据和值。
关键属性:
Name(string)- 属性名称,显示在左列标签Value(object)- 属性当前值,支持双向绑定PropertyType(Type)- 属性的 .NET 类型,用于选择合适的编辑器IsReadOnly(bool)- 是否只读,只读属性禁用编辑器Category(string?)- 分组类别,用于属性分组显示Description(string?)- 属性描述,可选的辅助说明DisplayName(string?)- 显示名称,如果为空则使用 Name
C# 类定义:
namespace Penguin.AvaloniaUI.Controls.PropertyGrid
{
/// <summary>
/// 表示 PropertyGrid 中的单个属性项
/// </summary>
public class PropertyItem : ReactiveObject
{
private object? _value;
/// <summary>
/// 属性名称
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 属性显示名称(如果为空则使用 Name)
/// </summary>
public string? DisplayName { get; set; }
/// <summary>
/// 属性值(支持双向绑定)
/// </summary>
public object? Value
{
get => _value;
set => this.RaiseAndSetIfChanged(ref _value, value);
}
/// <summary>
/// 属性的 .NET 类型
/// </summary>
public Type PropertyType { get; set; } = typeof(object);
/// <summary>
/// 是否只读
/// </summary>
public bool IsReadOnly { get; set; }
/// <summary>
/// 分组类别(可选)
/// </summary>
public string? Category { get; set; }
/// <summary>
/// 属性描述(可选)
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 原始 PropertyInfo(用于反射场景)
/// </summary>
public PropertyInfo? PropertyInfo { get; set; }
}
}
关系:
- PropertyGrid 包含多个 PropertyItem(
ObservableCollection<PropertyItem>) - PropertyItem 通过 PropertyType 决定使用哪个编辑器控件
GuideStep
用途:表示 UserGuide 引导流程中的单个步骤。
关键属性:
TargetControl(Control?)- 引导目标控件,Overlay 将聚焦此控件Title(string)- 步骤标题,显示在引导提示框顶部Content(string)- 步骤内容/提示文本,支持基础富文本Position(TooltipPosition)- 提示框相对于目标控件的位置Order(int)- 步骤顺序,用于排序
C# 类定义:
namespace Penguin.AvaloniaUI.Controls.UserGuide
{
/// <summary>
/// 提示框位置枚举
/// </summary>
public enum TooltipPosition
{
Bottom,
Top,
Left,
Right
}
/// <summary>
/// 表示 UserGuide 中的单个引导步骤
/// </summary>
public class GuideStep : ReactiveObject
{
/// <summary>
/// 引导目标控件(可选,如果为空则显示全屏提示)
/// </summary>
public Control? TargetControl { get; set; }
/// <summary>
/// 步骤标题
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// 步骤内容(支持基础富文本)
/// </summary>
public string Content { get; set; } = string.Empty;
/// <summary>
/// 提示框位置
/// </summary>
public TooltipPosition Position { get; set; } = TooltipPosition.Bottom;
/// <summary>
/// 步骤顺序(用于排序)
/// </summary>
public int Order { get; set; }
/// <summary>
/// 是否允许跳过此步骤
/// </summary>
public bool Skippable { get; set; } = true;
}
}
关系:
- UserGuide 包含多个 GuideStep(
ObservableCollection<GuideStep>) - GuideStep 通过 TargetControl 关联到具体的 UI 控件
- Overlay 根据 TargetControl 的位置和大小进行挖空显示
ThemeInfo(辅助模型)
用途:表示主题信息,用于 ThemeManager 管理主题切换。
关键属性:
ThemeType(ThemeType)- 主题类型枚举(Light、Dark)ResourceUri(Uri)- 主题资源字典的 URI
C# 类定义:
namespace Penguin.AvaloniaUI.Themes
{
/// <summary>
/// 主题类型枚举
/// </summary>
public enum ThemeType
{
Light,
Dark
}
/// <summary>
/// 主题信息
/// </summary>
public class ThemeInfo
{
/// <summary>
/// 主题类型
/// </summary>
public ThemeType Type { get; set; }
/// <summary>
/// 主题资源字典 URI
/// </summary>
public Uri ResourceUri { get; set; } = null!;
/// <summary>
/// 主题显示名称
/// </summary>
public string DisplayName { get; set; } = string.Empty;
}
}
关系:
- ThemeManager 使用 ThemeInfo 管理可用主题
- 主题切换时通过 ResourceUri 加载对应的 ResourceDictionary
Components
基于架构模式和数据模型,系统划分为以下核心组件。每个组件负责特定功能,并通过清晰的接口与其他组件交互。
PropertyGrid
职责:自动生成属性编辑 UI,支持多种属性类型、分组、只读属性和基础验证。
关键接口:
SelectedObject(依赖属性)- 绑定的数据对象,变化时自动刷新属性列表Properties(ObservableCollection)- 解析后的属性列表PropertyValueChanged(事件)- 属性值变化时触发
依赖关系:
- 依赖 TwoColumnLayout 进行布局
- 依赖 PropertyEditors(TextBox、NumericUpDown、ComboBox 等)
- 依赖 ThemeManager 获取当前主题颜色
- 使用反射(System.Reflection)解析对象属性
技术细节:
- 继承自
TemplatedControl - 使用 ReactiveUI 的
WhenAnyValue监听 SelectedObject 变化 - 通过
PropertyInfo.GetCustomAttributes<T>()读取 Attribute(如[Category]、[Browsable]) - 编辑器选择逻辑:根据
PropertyItem.PropertyType映射到对应控件
实现示例(核心逻辑):
public class PropertyGrid : TemplatedControl
{
public static readonly StyledProperty<object?> SelectedObjectProperty =
AvaloniaProperty.Register<PropertyGrid, object?>(nameof(SelectedObject));
public object? SelectedObject
{
get => GetValue(SelectedObjectProperty);
set => SetValue(SelectedObjectProperty, value);
}
public ObservableCollection<PropertyItem> Properties { get; } = new();
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
this.GetObservable(SelectedObjectProperty)
.Subscribe(obj => RefreshProperties(obj));
}
private void RefreshProperties(object? obj)
{
Properties.Clear();
if (obj == null) return;
var properties = obj.GetType()
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead)
.Select(p => CreatePropertyItem(obj, p))
.OrderBy(p => p.Category)
.ThenBy(p => p.Order);
foreach (var prop in properties)
{
Properties.Add(prop);
}
}
}
TwoColumnLayout
职责:提供"标签-编辑器"配对的布局控件,左列固定或自适应宽度,右列占据剩余空间。
关键接口:
Items(ObservableCollection)- 行集合LabelWidth(double)- 左列宽度(默认 Auto)RowSpacing(double)- 行间距(默认 8px)
依赖关系:
- 无外部依赖(纯布局控件)
- 被 PropertyGrid 使用
技术细节:
- 继承自
Panel或内部使用Grid - 重写
MeasureOverride和ArrangeOverride实现自定义布局 - 响应主题系统(标签文本颜色使用
TextSecondary)
布局逻辑:
┌─────────────────────────────────┐
│ Label 1 │ Editor 1 │
│ Label 2 │ Editor 2 │
│ Label 3 │ Editor 3 │
└─────────────────────────────────┘
← LabelWidth → ← 剩余空间 →
PropertyEditors(编辑器集合)
职责:为不同属性类型提供专用编辑器控件。
组件列表:
| 属性类型 | 编辑器控件 | 说明 |
|---|---|---|
| string | TextBox | Avalonia 内置 |
| int, double | NumericUpDown | Avalonia 内置或自定义 |
| bool | CheckBox | Avalonia 内置 |
| enum | ComboBox | 自动填充枚举值 |
| DateTime | DatePicker + TimePicker | 组合控件 |
关键接口:
- 所有编辑器实现双向绑定(TwoWay Binding)
- 支持 IsEnabled 控制只读状态
- 支持 Avalonia 的 DataValidation 机制
依赖关系:
- 被 PropertyGrid 使用
- 依赖 ThemeManager 获取主题颜色
扩展性:
- 开发者可通过
PropertyGrid.EditorTemplateSelector注册自定义编辑器
UserGuide
职责:管理多步引导流程,协调 Overlay 和引导提示框的显示。
关键接口:
Steps(ObservableCollection)- 引导步骤集合CurrentStepIndex(int)- 当前步骤索引NextStepCommand(ICommand)- 下一步命令PreviousStepCommand(ICommand)- 上一步命令SkipCommand(ICommand)- 跳过命令Completed(事件)- 引导完成事件
依赖关系:
- 依赖 Overlay 控件显示遮罩
- 依赖 RichTooltip 显示引导提示框
- 依赖 GuideStep 数据模型
技术细节:
- 继承自
ContentControl - 使用 ReactiveUI 的
ReactiveCommand实现步骤流转 - 步骤切换时触发动画(淡入淡出,200ms)
核心逻辑:
public class UserGuide : ContentControl
{
public ObservableCollection<GuideStep> Steps { get; } = new();
private int _currentStepIndex;
public ReactiveCommand<Unit, Unit> NextStepCommand { get; }
public ReactiveCommand<Unit, Unit> PreviousStepCommand { get; }
public ReactiveCommand<Unit, Unit> SkipCommand { get; }
public UserGuide()
{
NextStepCommand = ReactiveCommand.Create(() =>
{
if (_currentStepIndex < Steps.Count - 1)
{
_currentStepIndex++;
ShowCurrentStep();
}
else
{
Complete();
}
});
// ... 其他 Command 实现
}
}
Overlay
职责:在窗口上层显示半透明遮罩,可选地挖空特定控件区域以聚焦目标。
关键接口:
IsVisible(bool)- 显示/隐藏遮罩TargetControl(Control?)- 挖空目标控件BackgroundOpacity(double)- 不透明度(默认 0.5)BackgroundColor(Color)- 遮罩颜色
依赖关系:
- 被 UserGuide 使用
- 依赖 ThemeManager(暗色模式下调整遮罩颜色)
技术细节:
- 继承自
ContentControl或Panel - 使用绝对定位覆盖整个窗口
- 挖空逻辑:通过
Clip或多个Rectangle实现
视觉效果:
┌─────────────────────────────────┐
│████████████████████████████████│ ← 半透明遮罩
│████████┌──────────┐████████████│
│████████│ Target │████████████│ ← 挖空区域清晰可见
│████████└──────────┘████████████│
│████████████████████████████████│
└─────────────────────────────────┘
RichTooltip
职责:显示支持基础富文本的增强 Tooltip。
关键接口:
Content(string)- 提示内容,支持简单 Markdown 语法(**粗体**、*斜体*、\n换行)MaxWidth(double)- 最大宽度(默认 300px)
依赖关系:
- 扩展 Avalonia 的
ToolTip - 被 UserGuide 使用
技术细节:
- 内部使用
TextBlock配合Inlines(Run、Bold、Italic) - 简单的 Markdown 解析器(不依赖第三方库)
- 响应主题系统(背景色
Surface,文本色TextPrimary)
解析示例:
输入:"**注意**: 这是一个重要提示。\n请仔细阅读。"
输出:
<TextBlock>
<Bold>注意</Bold>: 这是一个重要提示。
<LineBreak />
请仔细阅读。
</TextBlock>
ThemeManager
职责:管理主题切换,提供全局主题访问接口。
关键接口:
ApplyTheme(ThemeType type)(方法)- 切换主题CurrentTheme(ThemeType)- 当前主题类型ThemeChanged(事件)- 主题变化事件
依赖关系:
- 被所有控件使用(通过静态方法或单例)
- 依赖 Avalonia 的
Application.Current.Resources
技术细节:
- 静态类或单例模式
- 主题切换通过替换
ResourceDictionary实现 - 支持动画过渡(可选,100ms 淡入淡出)
实现示例:
public static class ThemeManager
{
private static ThemeType _currentTheme = ThemeType.Light;
public static event EventHandler<ThemeType>? ThemeChanged;
public static void ApplyTheme(ThemeType type)
{
if (_currentTheme == type) return;
var app = Application.Current;
if (app == null) return;
// 移除旧主题
var oldTheme = app.Resources.MergedDictionaries
.FirstOrDefault(d => d.Source?.ToString().Contains("Theme") == true);
if (oldTheme != null)
app.Resources.MergedDictionaries.Remove(oldTheme);
// 加载新主题
var uri = type == ThemeType.Light
? new Uri("avares://Penguin.AvaloniaUI/Themes/LightTheme.axaml")
: new Uri("avares://Penguin.AvaloniaUI/Themes/DarkTheme.axaml");
var newTheme = new ResourceDictionary { Source = uri };
app.Resources.MergedDictionaries.Add(newTheme);
_currentTheme = type;
ThemeChanged?.Invoke(null, type);
}
}
Component Diagrams
graph TB
subgraph "业务控件层"
PropertyGrid[PropertyGrid]
UserGuide[UserGuide]
end
subgraph "基础控件层"
TwoColumnLayout[TwoColumnLayout]
Overlay[Overlay]
RichTooltip[RichTooltip]
Editors[PropertyEditors]
end
subgraph "工具层"
ThemeManager[ThemeManager]
end
subgraph "数据模型"
PropertyItem[PropertyItem]
GuideStep[GuideStep]
end
PropertyGrid --> TwoColumnLayout
PropertyGrid --> Editors
PropertyGrid --> PropertyItem
PropertyGrid --> ThemeManager
UserGuide --> Overlay
UserGuide --> RichTooltip
UserGuide --> GuideStep
UserGuide --> ThemeManager
TwoColumnLayout --> ThemeManager
Overlay --> ThemeManager
RichTooltip --> ThemeManager
Editors --> ThemeManager
style PropertyGrid fill:#4A90E2
style UserGuide fill:#4A90E2
style TwoColumnLayout fill:#7ED321
style Overlay fill:#7ED321
style RichTooltip fill:#7ED321
style ThemeManager fill:#F5A623
Unified Project Structure
基于 Monorepo 策略和 .NET 项目组织最佳实践,以下是详细的项目文件结构:
D:\32_avalonia.ui/
├── .github/ # GitHub 工作流(Post-MVP)
│ └── workflows/
│ ├── ci.yml # 持续集成
│ └── release.yml # 发布流程
│
├── src/ # 源代码目录
│ ├── Penguin.AvaloniaUI/ # 核心控件库项目
│ │ ├── Controls/ # 业务场景控件
│ │ │ ├── PropertyGrid/
│ │ │ │ ├── PropertyGrid.cs # 主控件类
│ │ │ │ ├── PropertyItem.cs # 数据模型
│ │ │ │ ├── PropertyEditorFactory.cs # 编辑器工厂
│ │ │ │ └── Themes/
│ │ │ │ ├── PropertyGrid.axaml # 默认模板
│ │ │ │ └── PropertyGrid.axaml.cs
│ │ │ ├── UserGuide/
│ │ │ │ ├── UserGuide.cs
│ │ │ │ ├── GuideStep.cs
│ │ │ │ ├── Overlay.cs
│ │ │ │ ├── RichTooltip.cs
│ │ │ │ └── Themes/
│ │ │ │ ├── UserGuide.axaml
│ │ │ │ ├── Overlay.axaml
│ │ │ │ └── RichTooltip.axaml
│ │ │ └── ... (其他控件)
│ │ │
│ │ ├── Layouts/ # 布局控件
│ │ │ ├── TwoColumnLayout.cs
│ │ │ └── Themes/
│ │ │ └── TwoColumnLayout.axaml
│ │ │
│ │ ├── Themes/ # 主题系统
│ │ │ ├── ThemeManager.cs # 主题管理器
│ │ │ ├── ThemeInfo.cs # 主题信息模型
│ │ │ ├── ColorSystem/ # 颜色系统定义
│ │ │ │ ├── LightColors.axaml # 浅色主题颜色
│ │ │ │ └── DarkColors.axaml # 暗色主题颜色
│ │ │ ├── LightTheme.axaml # 浅色主题资源
│ │ │ └── DarkTheme.axaml # 暗色主题资源
│ │ │
│ │ ├── Utils/ # 工具类
│ │ │ ├── Converters/ # 值转换器
│ │ │ │ ├── BoolToVisibilityConverter.cs
│ │ │ │ └── TypeToEditorConverter.cs
│ │ │ ├── Helpers/ # 辅助类
│ │ │ │ ├── ReflectionHelper.cs # 反射工具
│ │ │ │ └── ValidationHelper.cs # 验证工具
│ │ │ └── Extensions/ # 扩展方法
│ │ │ └── ObservableExtensions.cs
│ │ │
│ │ ├── Assets/ # 资源文件
│ │ │ └── Icons/ # 图标资源
│ │ │
│ │ ├── Penguin.AvaloniaUI.csproj # 项目文件
│ │ └── AssemblyInfo.cs # 程序集信息
│ │
│ ├── Penguin.AvaloniaUI.SourceGenerators/ # Source Generator(Post-MVP)
│ │ ├── PropertyGridGenerator.cs
│ │ └── Penguin.AvaloniaUI.SourceGenerators.csproj
│ │
│ ├── Example/ # 示例应用项目
│ │ ├── App.axaml # 应用程序资源
│ │ ├── App.axaml.cs
│ │ ├── ViewModels/ # 视图模型
│ │ │ ├── MainWindowViewModel.cs
│ │ │ ├── PropertyGridDemoViewModel.cs
│ │ │ └── UserGuideDemoViewModel.cs
│ │ ├── Views/ # 视图/页面
│ │ │ ├── MainWindow.axaml
│ │ │ ├── MainWindow.axaml.cs
│ │ │ ├── Pages/
│ │ │ │ ├── ColorSystemPage.axaml # 颜色系统演示
│ │ │ │ ├── PropertyGridPage.axaml # PropertyGrid 演示
│ │ │ │ ├── UserGuidePage.axaml # UserGuide 演示
│ │ │ │ └── ComprehensiveDemoPage.axaml # 综合演示
│ │ │ └── ...
│ │ ├── Models/ # 测试数据模型
│ │ │ ├── DemoSettings.cs # PropertyGrid 测试类
│ │ │ └── LargeSettings.cs # 50 属性测试类
│ │ ├── Assets/ # 示例应用资源
│ │ ├── Example.csproj
│ │ └── Program.cs
│ │
│ └── Penguin.AvaloniaUI.Tests/ # 单元测试项目
│ ├── Controls/
│ │ ├── PropertyGridTests.cs
│ │ ├── UserGuideTests.cs
│ │ └── ...
│ ├── Layouts/
│ │ └── TwoColumnLayoutTests.cs
│ ├── Themes/
│ │ └── ThemeManagerTests.cs
│ ├── Utils/
│ │ └── ReflectionHelperTests.cs
│ └── Penguin.AvaloniaUI.Tests.csproj
│
├── docs/ # 项目文档
│ ├── prd.md # 产品需求文档
│ ├── architecture.md # 架构文档(本文档)
│ ├── brief.md # 项目简介
│ └── brainstorm.md # 头脑风暴
│
├── .bmad-core/ # BMAD 框架配置
│ ├── core-config.yaml
│ ├── agents/
│ ├── tasks/
│ └── templates/
│
├── .editorconfig # 编辑器配置
├── .gitignore # Git 忽略规则
├── Directory.Packages.props # 统一包版本管理
├── README.md # 项目说明
├── LICENSE # 许可证(Post-MVP)
└── Penguin.AvaloniaUI.sln # 解决方案文件
关键目录说明:
| 目录 | 用途 | 命名空间 |
|---|---|---|
Controls/ |
业务场景控件实现 | Penguin.AvaloniaUI.Controls.* |
Layouts/ |
布局控件实现 | Penguin.AvaloniaUI.Layouts |
Themes/ |
主题和样式系统 | Penguin.AvaloniaUI.Themes |
Utils/ |
工具类和辅助方法 | Penguin.AvaloniaUI.Utils.* |
Example/Views/Pages/ |
示例应用页面 | Example.Views.Pages |
Example/ViewModels/ |
示例应用 ViewModel | Example.ViewModels |
Development Workflow
本节定义控件库的开发环境配置和日常开发流程,确保开发者能够快速上手并保持高效开发。
Local Development Setup
前置要求:
# 必需
- .NET 9.0 SDK (https://dotnet.microsoft.com/download)
- Visual Studio 2022 (17.8+)
- Git 2.40+
# 推荐
- Visual Studio Avalonia 扩展 (用于 XAML 预览)
初始化项目:
# 克隆仓库
git clone <repository-url>
cd 32_avalonia.ui
# 还原 NuGet 包
dotnet restore
# 构建解决方案
dotnet build
# 运行示例应用
dotnet run --project src/Example/Example.csproj
Visual Studio 配置:
- 打开
Penguin.AvaloniaUI.sln - 设置
Example为启动项目 - 选择调试配置(Debug/Release)
- 按 F5 启动调试
Development Commands
常用命令:
# 启动示例应用(开发模式,支持热重载)
dotnet watch --project src/Example/Example.csproj
# 运行所有单元测试
dotnet test
# 运行特定测试类
dotnet test --filter "FullyQualifiedName~PropertyGridTests"
# 生成代码覆盖率报告
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=html
# 清理构建产物
dotnet clean
# 格式化代码(基于 .editorconfig)
dotnet format
# 打包 NuGet 包(Post-MVP)
dotnet pack src/Penguin.AvaloniaUI/Penguin.AvaloniaUI.csproj -c Release
Hot Reload 配置
Avalonia 11.x 支持 .NET Hot Reload,但有限制:
支持的修改:
- C# 方法体内的代码修改
- XAML 资源字典的修改(颜色、样式)
- 部分 XAML 控件属性修改
不支持的修改(需要重启):
- 新增或删除控件
- 修改控件模板结构
- 修改依赖属性定义
- 修改 ViewModel 属性签名
启用 Hot Reload:
在 Example.csproj 中确保已启用:
<PropertyGroup>
<AvaloniaUseHotReload>true</AvaloniaUseHotReload>
</PropertyGroup>
Environment Configuration
控件库无需环境变量(MVP 阶段),但示例应用可能需要:
# Example/.env (可选,用于测试外部 API 集成)
# MVP 阶段不需要
调试配置(launchSettings.json):
{
"profiles": {
"Example": {
"commandName": "Project",
"dotnetRunMessages": true,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}
Git Workflow
Commit Message 格式(Conventional Commits):
<type>: <description>
[optional body]
Type 类型:
feat: 新功能(如feat: 增加了UserControl的DarkStyle)fix: Bug 修复(如fix: 修复PropertyGrid在主题切换时崩溃)refactor: 重构(如refactor: 优化ThemeManager的资源加载逻辑)test: 测试相关(如test: 添加PropertyGrid的分组功能测试)docs: 文档更新(如docs: 更新README的快速开始指南)style: 代码格式调整(如style: 统一缩进为4空格)chore: 构建/工具相关(如chore: 升级Avalonia到11.3.8)
示例:
git commit -m "feat: 实现PropertyGrid的属性分组功能"
git commit -m "fix: 修复TwoColumnLayout在窗口缩放时的对齐问题"
git commit -m "test: 添加ThemeManager的主题切换测试"
分支命名规范:
feat/<feature-name>- 新功能分支(如feat/usercontrol)fix/<issue-number>- Bug 修复分支(如fix/1123)refactor/<description>- 重构分支(如refactor/theme-system)test/<description>- 测试分支(如test/integration)
日常开发流程:
- 创建功能分支:
git checkout -b feat/two-column-layout - 编写代码:在
src/Penguin.AvaloniaUI/中实现控件 - 编写测试:在
src/Penguin.AvaloniaUI.Tests/中添加单元测试 - 更新示例:在
src/Example/Views/Pages/中添加演示页面 - 本地验证:
- 运行单元测试:
dotnet test - 启动示例应用:
dotnet run --project src/Example - 手动测试主题切换和控件交互
- 运行单元测试:
- 提交代码:
git add . git commit -m "feat: 实现TwoColumnLayout布局控件" git push origin feat/two-column-layout
Coding Standards
本节定义最小但关键的编码规范,专注于项目特定的规则,防止常见错误。这些规范将被开发者和 AI Agent 遵循。
Critical Rules
以下规则是强制性的,违反将导致代码审查不通过:
-
命名空间组织: 所有控件必须放在
Penguin.AvaloniaUI.Controls.*命名空间下,布局控件在Penguin.AvaloniaUI.Layouts,主题在Penguin.AvaloniaUI.Themes。不得跨命名空间引用内部实现细节。 -
依赖属性定义: 所有公开的可绑定属性必须定义为
StyledProperty,使用AvaloniaProperty.Register<>注册,不得使用字段或普通属性。 -
XAML 资源引用: 所有颜色、字体、间距必须从主题资源字典引用,禁止硬编码颜色值(如
#FFFFFF)。使用{DynamicResource TextPrimary}而非{StaticResource},确保主题切换生效。 -
ReactiveUI 集成: ViewModel 必须继承
ReactiveObject,属性变化使用this.RaiseAndSetIfChanged(ref _field, value),不得直接触发PropertyChanged事件。 -
错误处理: 公开 API 方法必须验证参数(如
ArgumentNullException),内部方法可使用Debug.Assert。不得吞没异常(空 catch 块)。 -
XML 文档注释: 所有
public和protected成员必须有 XML 注释(///),包括<summary>、<param>、<returns>。示例:/// <summary> /// 应用指定的主题类型 /// </summary> /// <param name="type">主题类型(Light 或 Dark)</param> public static void ApplyTheme(ThemeType type) -
禁止反射动态创建控件: PropertyGrid 可以使用反射解析属性,但不得使用
Activator.CreateInstance动态创建编辑器控件。使用工厂模式或字典映射。 -
主题资源命名约定: 语义化颜色必须使用统一前缀:
TextPrimary、TextSecondary、BackgroundPrimary、SurfaceElevated。不得使用Color1、MyBlue等非语义化命名。
Naming Conventions
| 元素类型 | 命名规则 | 示例 |
|---|---|---|
| 控件类 | PascalCase,功能名称 + 控件类型后缀(可选) | PropertyGrid, TwoColumnLayout |
| 依赖属性 | PascalCase,属性名 + Property 后缀 |
SelectedObjectProperty |
| 私有字段 | camelCase,下划线前缀 | _currentTheme |
| 事件 | PascalCase,动词过去式 | ThemeChanged, PropertyValueChanged |
| 方法 | PascalCase,动词开头 | ApplyTheme(), RefreshProperties() |
| XAML 文件 | PascalCase,与类名一致 | PropertyGrid.axaml |
| 测试方法 | PascalCase,MethodName_Scenario_ExpectedResult | ApplyTheme_WithDarkTheme_ShouldUpdateResources() |
File Organization Rules
-
一个文件一个类:每个控件类单独一个
.cs文件,不得在同一文件中定义多个公开类(嵌套私有类除外)。 -
XAML 与代码分离:控件的 XAML 模板放在
Themes/子目录下,与类定义分离:Controls/PropertyGrid/PropertyGrid.cs Controls/PropertyGrid/Themes/PropertyGrid.axaml -
数据模型独立文件:数据模型类(如
PropertyItem,GuideStep)与控件类分离,放在同级目录下。 -
文件格式规范:命名空间单独放在文件顶部,减少缩进层级:
namespace Penguin.AvaloniaUI.Controls; public class PropertyGrid : TemplatedControl { // 类实现 }
Code Style (基于 .editorconfig)
项目使用 .editorconfig 强制执行以下规则:
root = true
[*.cs]
indent_style = space
indent_size = 4
end_of_line = crlf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
# C# 规则
csharp_new_line_before_open_brace = all
csharp_prefer_braces = true:warning
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
[*.axaml]
indent_size = 2
代码风格要点:
- 使用
var声明局部变量(如var count = 100;) - 命名空间单独一行,不使用文件范围命名空间的花括号嵌套
运行格式化检查:
dotnet format --verify-no-changes
Testing Strategy
本节定义控件库的测试方法和组织策略,确保核心功能的稳定性和可靠性。
Testing Pyramid
E2E Tests (手动测试)
/ \
Integration Tests (可选)
/ \
Unit Tests (核心逻辑)
测试重点分配:
- Unit Tests (70%):核心业务逻辑、数据模型、工具类
- Integration Tests (可选):控件与 ReactiveUI 的集成、主题系统
- Manual Tests (必须):UI 渲染、主题切换、用户交互
MVP 阶段不包括:
- UI 自动化测试(Avalonia 提供 Headless 测试包,但 MVP 阶段依赖手动测试)
- 性能基准测试(除非发现明显性能问题)
- 跨平台兼容性测试(仅在 Windows 上测试)
Test Organization
测试项目结构:
Penguin.AvaloniaUI.Tests/
├── Controls/
│ ├── PropertyGridTests.cs
│ ├── UserGuideTests.cs
│ └── ...
├── Layouts/
│ └── TwoColumnLayoutTests.cs
├── Themes/
│ └── ThemeManagerTests.cs
├── Utils/
│ ├── ReflectionHelperTests.cs
│ └── Converters/
│ └── TypeToEditorConverterTests.cs
└── TestHelpers/
├── MockObjects.cs
└── TestBase.cs
命名约定:
- 测试类:
{ClassName}Tests - 测试方法:
{MethodName}_{Scenario}_{ExpectedResult}
Unit Test Examples
示例 1:测试 PropertyGrid 的反射逻辑
namespace Penguin.AvaloniaUI.Tests.Controls;
public class PropertyGridTests
{
[Fact]
public void RefreshProperties_WithValidObject_ShouldParseAllPublicProperties()
{
// Arrange
var testObject = new TestSettings
{
Name = "Test",
Age = 25,
IsEnabled = true
};
var propertyGrid = new PropertyGrid
{
SelectedObject = testObject
};
// Act
var properties = propertyGrid.Properties;
// Assert
Assert.Equal(3, properties.Count);
Assert.Contains(properties, p => p.Name == "Name" && p.PropertyType == typeof(string));
Assert.Contains(properties, p => p.Name == "Age" && p.PropertyType == typeof(int));
Assert.Contains(properties, p => p.Name == "IsEnabled" && p.PropertyType == typeof(bool));
}
[Fact]
public void RefreshProperties_WithCategoryAttribute_ShouldGroupByCategory()
{
// Arrange
var testObject = new CategorizedSettings();
var propertyGrid = new PropertyGrid
{
SelectedObject = testObject
};
// Act
var properties = propertyGrid.Properties;
var generalCategory = properties.Where(p => p.Category == "General").ToList();
var advancedCategory = properties.Where(p => p.Category == "Advanced").ToList();
// Assert
Assert.NotEmpty(generalCategory);
Assert.NotEmpty(advancedCategory);
}
[Fact]
public void RefreshProperties_WithNullObject_ShouldClearProperties()
{
// Arrange
var propertyGrid = new PropertyGrid
{
SelectedObject = new TestSettings()
};
// Act
propertyGrid.SelectedObject = null;
// Assert
Assert.Empty(propertyGrid.Properties);
}
}
示例 2:测试 ThemeManager
namespace Penguin.AvaloniaUI.Tests.Themes;
public class ThemeManagerTests
{
[Fact]
public void ApplyTheme_WithLightTheme_ShouldLoadLightResources()
{
// Arrange
var app = CreateTestApplication();
// Act
ThemeManager.ApplyTheme(ThemeType.Light);
// Assert
var textPrimary = app.Resources["TextPrimary"] as SolidColorBrush;
Assert.NotNull(textPrimary);
// 浅色主题的主要文本应为深色
Assert.True(textPrimary.Color.R < 128);
}
[Fact]
public void ApplyTheme_SwitchingThemes_ShouldTriggerThemeChangedEvent()
{
// Arrange
var eventTriggered = false;
ThemeManager.ThemeChanged += (sender, type) => eventTriggered = true;
// Act
ThemeManager.ApplyTheme(ThemeType.Dark);
// Assert
Assert.True(eventTriggered);
}
private Application CreateTestApplication()
{
// 创建测试用的 Application 实例
return new Application();
}
}
示例 3:测试 UserGuide 步骤流转
namespace Penguin.AvaloniaUI.Tests.Controls;
public class UserGuideTests
{
[Fact]
public void NextStepCommand_WhenNotLastStep_ShouldIncrementIndex()
{
// Arrange
var userGuide = new UserGuide();
userGuide.Steps.Add(new GuideStep { Title = "Step 1", Order = 0 });
userGuide.Steps.Add(new GuideStep { Title = "Step 2", Order = 1 });
userGuide.Steps.Add(new GuideStep { Title = "Step 3", Order = 2 });
// Act
userGuide.NextStepCommand.Execute(null);
// Assert
Assert.Equal(1, userGuide.CurrentStepIndex);
}
[Fact]
public void NextStepCommand_WhenLastStep_ShouldTriggerCompleteEvent()
{
// Arrange
var userGuide = new UserGuide();
userGuide.Steps.Add(new GuideStep { Title = "Step 1", Order = 0 });
var completedTriggered = false;
userGuide.Completed += (sender, args) => completedTriggered = true;
// Act
userGuide.NextStepCommand.Execute(null);
// Assert
Assert.True(completedTriggered);
}
[Fact]
public void PreviousStepCommand_WhenFirstStep_ShouldNotExecute()
{
// Arrange
var userGuide = new UserGuide();
userGuide.Steps.Add(new GuideStep { Title = "Step 1", Order = 0 });
// Act
var canExecute = userGuide.PreviousStepCommand.CanExecute(null);
// Assert
Assert.False(canExecute);
}
}
Manual Testing Checklist
以下场景必须通过示例应用手动测试:
PropertyGrid:
- 绑定包含 6 种基础类型的对象,所有编辑器正确显示
- 修改属性值,SelectedObject 对象同步更新
- 只读属性禁用编辑器或显示为灰色
- 属性按 Category 分组显示,分组标题清晰
- 主题切换时,PropertyGrid 颜色正确更新
- 50 个属性的生成时间 < 200ms(使用秒表或 Stopwatch 测量)
UserGuide:
- 引导流程正确显示 Overlay 遮罩
- 目标控件区域挖空显示(清晰可见)
- 提示框在目标控件附近正确定位(Top/Bottom/Left/Right)
- "下一步"/"上一步"按钮功能正常
- 步骤进度显示正确(如 "2/5")
- "跳过"和"完成"按钮触发完成事件
主题系统:
- 主题切换在 100ms 内完成,无闪烁
- 所有控件的颜色正确响应主题变化
- 浅色和暗色主题的对比度舒适(文本清晰可读)
- 主题切换时无控件错位或布局异常
跨功能集成:
- 在 PropertyGrid 编辑时切换主题,无崩溃
- 在 UserGuide 进行中修改 PropertyGrid,无异常
- 窗口缩放时,所有控件布局正确调整
Test Helpers
创建测试辅助类减少重复代码:
namespace Penguin.AvaloniaUI.Tests.TestHelpers;
public static class MockObjects
{
public class TestSettings
{
public string Name { get; set; } = "Default";
public int Age { get; set; } = 0;
public bool IsEnabled { get; set; } = false;
}
public class CategorizedSettings
{
[Category("General")]
public string Name { get; set; } = "Default";
[Category("Advanced")]
public int Timeout { get; set; } = 5000;
}
}
public class TestBase
{
protected Application CreateTestApplication()
{
var app = new Application();
// 加载测试用主题资源
return app;
}
}
Running Tests
# 运行所有测试
dotnet test
# 运行特定测试类
dotnet test --filter "FullyQualifiedName~PropertyGridTests"
# 运行特定测试方法
dotnet test --filter "Name~RefreshProperties_WithValidObject"
# 生成代码覆盖率报告
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=html
Visual Studio 测试运行器:
- 打开 Test Explorer(测试 → 测试资源管理器)
- 点击"运行所有测试"或右键运行特定测试
- 查看测试结果和失败详情
Error Handling
本节定义控件库的错误处理策略,确保异常情况下的稳定性和良好的开发者体验。
Error Handling Principles
核心原则:
- Fail Fast(快速失败):公开 API 在参数无效时立即抛出异常,不延迟到执行阶段
- Clear Error Messages(清晰错误信息):异常消息应明确指出问题和解决方案
- No Silent Failures(不吞没异常):禁止空 catch 块,必须记录或重新抛出异常
- Defensive Programming(防御性编程):内部方法使用
Debug.Assert验证前置条件
Exception Handling Patterns
公开 API 参数验证:
namespace Penguin.AvaloniaUI.Themes;
public static class ThemeManager
{
/// <summary>
/// 应用指定的主题类型
/// </summary>
/// <param name="type">主题类型</param>
/// <exception cref="ArgumentException">当主题类型无效时抛出</exception>
public static void ApplyTheme(ThemeType type)
{
// 参数验证
if (!Enum.IsDefined(typeof(ThemeType), type))
{
throw new ArgumentException(
$"Invalid theme type: {type}. Valid values are: {string.Join(", ", Enum.GetNames(typeof(ThemeType)))}",
nameof(type));
}
// 核心逻辑
try
{
LoadThemeResources(type);
_currentTheme = type;
ThemeChanged?.Invoke(null, type);
}
catch (Exception ex)
{
throw new InvalidOperationException(
$"Failed to apply theme '{type}'. Ensure theme resources are available.",
ex);
}
}
}
PropertyGrid 错误处理:
namespace Penguin.AvaloniaUI.Controls.PropertyGrid;
public class PropertyGrid : TemplatedControl
{
public static readonly StyledProperty<object?> SelectedObjectProperty =
AvaloniaProperty.Register<PropertyGrid, object?>(nameof(SelectedObject));
public object? SelectedObject
{
get => GetValue(SelectedObjectProperty);
set => SetValue(SelectedObjectProperty, value);
}
private void RefreshProperties(object? obj)
{
Properties.Clear();
if (obj == null)
{
// 空对象是合法的,清空属性列表即可
return;
}
try
{
var properties = obj.GetType()
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead);
foreach (var prop in properties)
{
try
{
var item = CreatePropertyItem(obj, prop);
Properties.Add(item);
}
catch (Exception ex)
{
// 单个属性解析失败不应影响其他属性
Debug.WriteLine($"[PropertyGrid] Failed to create property item for '{prop.Name}': {ex.Message}");
// 可选:添加错误占位符
Properties.Add(new PropertyItem
{
Name = prop.Name,
DisplayName = $"{prop.Name} (Error)",
IsReadOnly = true,
Value = $"Error: {ex.Message}"
});
}
}
}
catch (Exception ex)
{
throw new InvalidOperationException(
$"Failed to parse properties for object of type '{obj.GetType().Name}'.",
ex);
}
}
private PropertyItem CreatePropertyItem(object obj, PropertyInfo prop)
{
Debug.Assert(obj != null, "Object should not be null");
Debug.Assert(prop != null, "PropertyInfo should not be null");
try
{
var value = prop.GetValue(obj);
return new PropertyItem
{
Name = prop.Name,
DisplayName = GetDisplayName(prop),
Value = value,
PropertyType = prop.PropertyType,
IsReadOnly = !prop.CanWrite,
Category = GetCategory(prop),
PropertyInfo = prop
};
}
catch (TargetInvocationException ex)
{
throw new InvalidOperationException(
$"Property '{prop.Name}' getter threw an exception.",
ex.InnerException ?? ex);
}
}
}
UserGuide 错误处理:
namespace Penguin.AvaloniaUI.Controls.UserGuide;
public class UserGuide : ContentControl
{
private void ShowCurrentStep()
{
if (CurrentStepIndex < 0 || CurrentStepIndex >= Steps.Count)
{
Debug.WriteLine($"[UserGuide] Invalid step index: {CurrentStepIndex}");
return;
}
var step = Steps[CurrentStepIndex];
// 验证目标控件是否可用
if (step.TargetControl == null)
{
Debug.WriteLine($"[UserGuide] Step '{step.Title}' has no target control, showing full-screen overlay.");
// 显示全屏提示
ShowFullScreenGuide(step);
return;
}
// 检查目标控件是否在可视化树中
if (!step.TargetControl.IsVisible || !IsControlInVisualTree(step.TargetControl))
{
Debug.WriteLine($"[UserGuide] Target control for step '{step.Title}' is not visible or not in visual tree. Skipping step.");
// 自动跳到下一步
if (CurrentStepIndex < Steps.Count - 1)
{
CurrentStepIndex++;
ShowCurrentStep();
}
else
{
Complete();
}
return;
}
// 正常显示引导
ShowGuideForControl(step);
}
private bool IsControlInVisualTree(Control control)
{
try
{
return control.GetVisualRoot() != null;
}
catch
{
return false;
}
}
}
Logging Strategy
使用 Debug.WriteLine 进行调试日志:
using System.Diagnostics;
namespace Penguin.AvaloniaUI.Utils;
internal static class Logger
{
[Conditional("DEBUG")]
public static void Debug(string message)
{
System.Diagnostics.Debug.WriteLine($"[Penguin.AvaloniaUI] {message}");
}
[Conditional("DEBUG")]
public static void Warning(string message)
{
System.Diagnostics.Debug.WriteLine($"[WARNING] {message}");
}
public static void Error(string message, Exception? ex = null)
{
System.Diagnostics.Debug.WriteLine($"[ERROR] {message}");
if (ex != null)
{
System.Diagnostics.Debug.WriteLine($"Exception: {ex}");
}
}
}
使用示例:
private void RefreshProperties(object? obj)
{
Logger.Debug($"RefreshProperties called with object type: {obj?.GetType().Name ?? "null"}");
try
{
// 核心逻辑
}
catch (Exception ex)
{
Logger.Error("Failed to refresh properties", ex);
throw;
}
}
Error Messages Guidelines
好的错误消息示例:
// ❌ 不好的错误消息
throw new Exception("Error");
throw new ArgumentException("Invalid parameter");
// ✅ 好的错误消息
throw new ArgumentNullException(nameof(selectedObject),
"SelectedObject cannot be null. Please provide a valid object instance.");
throw new InvalidOperationException(
"Cannot apply theme: Application.Current is null. Ensure ThemeManager is called after App initialization.");
throw new NotSupportedException(
$"Property type '{propertyType.Name}' is not supported by PropertyGrid. " +
$"Supported types: string, int, double, bool, enum, DateTime.");
错误消息模板:
{What happened}: {Why it happened}. {How to fix it}.
Exception Types
使用合适的异常类型:
| 场景 | 异常类型 | 示例 |
|---|---|---|
| 参数为 null | ArgumentNullException |
throw new ArgumentNullException(nameof(obj)) |
| 参数值无效 | ArgumentException |
throw new ArgumentException("Invalid theme type", nameof(type)) |
| 参数超出范围 | ArgumentOutOfRangeException |
throw new ArgumentOutOfRangeException(nameof(index)) |
| 操作在当前状态无效 | InvalidOperationException |
throw new InvalidOperationException("Theme resources not loaded") |
| 功能未实现 | NotImplementedException |
throw new NotImplementedException("Custom editors not supported in MVP") |
| 功能不支持 | NotSupportedException |
throw new NotSupportedException($"Type {type} not supported") |
Try-Catch Guidelines
何时使用 try-catch:
// ✅ 捕获特定异常并提供上下文
try
{
var value = property.GetValue(obj);
}
catch (TargetInvocationException ex)
{
throw new InvalidOperationException(
$"Property '{property.Name}' getter threw an exception.",
ex.InnerException ?? ex);
}
// ✅ 记录异常并继续处理
try
{
LoadThemeResource(uri);
}
catch (Exception ex)
{
Logger.Error($"Failed to load theme resource: {uri}", ex);
// 使用默认主题
LoadDefaultTheme();
}
// ❌ 不要捕获所有异常并吞没
try
{
DoSomething();
}
catch
{
// 什么都不做 - 这是错误的!
}
// ❌ 不要捕获异常后重新抛出同一个异常
try
{
DoSomething();
}
catch (Exception ex)
{
throw ex; // 错误:会丢失调用栈
}
// ✅ 如果需要记录后重新抛出,使用 throw;
try
{
DoSomething();
}
catch (Exception ex)
{
Logger.Error("Operation failed", ex);
throw; // 正确:保留调用栈
}
Validation Helpers
创建参数验证辅助方法:
namespace Penguin.AvaloniaUI.Utils;
internal static class Guard
{
public static void NotNull<T>(T value, string paramName) where T : class
{
if (value == null)
{
throw new ArgumentNullException(paramName);
}
}
public static void NotNullOrEmpty(string value, string paramName)
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("Value cannot be null or empty.", paramName);
}
}
public static void InRange(int value, int min, int max, string paramName)
{
if (value < min || value > max)
{
throw new ArgumentOutOfRangeException(paramName,
$"Value must be between {min} and {max}, but was {value}.");
}
}
}
// 使用示例
public void SetProperty(string name, object value)
{
Guard.NotNullOrEmpty(name, nameof(name));
Guard.NotNull(value, nameof(value));
// 核心逻辑
}
Performance Optimization
本节定义性能优化策略和最佳实践,确保控件库满足 NFR 性能要求。
Performance Targets (from NFR)
| 指标 | 目标值 | 测量方法 |
|---|---|---|
| UI 渲染帧率 | 60fps (16ms/frame) | Visual Studio Performance Profiler |
| 主题切换时间 | < 100ms | Stopwatch 测量 |
| PropertyGrid 生成(50 属性) | < 200ms | Stopwatch 测量 |
| 应用启动时间 | < 2s | 从启动到窗口显示 |
| 内存占用 | < 100MB (空闲时) | Task Manager / dotnet-counters |
UI Rendering Performance
关键原则:避免在 UI 线程执行耗时操作
❌ 反面示例 - 阻塞 UI 线程:
// 错误:在属性 getter 中执行反射
public ObservableCollection<PropertyItem> Properties
{
get
{
var items = new ObservableCollection<PropertyItem>();
if (SelectedObject != null)
{
// 耗时操作会阻塞 UI 渲染
var properties = SelectedObject.GetType().GetProperties();
foreach (var prop in properties)
{
items.Add(CreatePropertyItem(SelectedObject, prop));
}
}
return items;
}
}
✅ 正确示例 - 异步加载:
private ObservableCollection<PropertyItem> _properties = new();
public ObservableCollection<PropertyItem> Properties => _properties;
private async void RefreshProperties(object? obj)
{
_properties.Clear();
if (obj == null) return;
// 在后台线程执行反射
var items = await Task.Run(() =>
{
var result = new List<PropertyItem>();
var properties = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var prop in properties)
{
result.Add(CreatePropertyItem(obj, prop));
}
return result;
});
// 回到 UI 线程更新集合
foreach (var item in items)
{
_properties.Add(item);
}
}
Reflection Optimization
问题:反射是 PropertyGrid 的性能瓶颈
优化策略 1:缓存 PropertyInfo
private static readonly ConcurrentDictionary<Type, PropertyInfo[]> _propertyCache = new();
private PropertyInfo[] GetCachedProperties(Type type)
{
return _propertyCache.GetOrAdd(type, t =>
t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
);
}
优化策略 2:缓存 Attribute 读取
private static readonly ConcurrentDictionary<PropertyInfo, CategoryAttribute?> _categoryCache = new();
private string? GetCategory(PropertyInfo prop)
{
var attr = _categoryCache.GetOrAdd(prop, p =>
p.GetCustomAttribute<CategoryAttribute>()
);
return attr?.Category;
}
优化策略 3:使用编译表达式替代反射调用(可选)
// 反射调用(慢)
var value = propertyInfo.GetValue(obj);
// 编译表达式(快 10-100 倍)
private static Func<object, object> CreateGetter(PropertyInfo prop)
{
var instance = Expression.Parameter(typeof(object), "instance");
var convert = Expression.Convert(instance, prop.DeclaringType!);
var property = Expression.Property(convert, prop);
var convertResult = Expression.Convert(property, typeof(object));
return Expression.Lambda<Func<object, object>>(convertResult, instance).Compile();
}
性能对比:
反射 GetValue: ~1000 ns/调用
编译表达式: ~10 ns/调用
直接属性访问: ~1 ns/调用
Theme Switching Performance
优化主题切换速度(目标 < 100ms):
❌ 反面示例 - 逐个替换资源:
// 错误:逐个替换资源会触发多次 UI 刷新
foreach (var key in themeColors.Keys)
{
Application.Current.Resources[key] = themeColors[key];
}
✅ 正确示例 - 批量替换 ResourceDictionary:
public static void ApplyTheme(ThemeType type)
{
var app = Application.Current;
if (app == null) return;
// 移除旧主题
var oldTheme = app.Resources.MergedDictionaries
.FirstOrDefault(d => d.Source?.ToString().Contains("Theme") == true);
if (oldTheme != null)
app.Resources.MergedDictionaries.Remove(oldTheme);
// 一次性加载新主题(单次 UI 刷新)
var uri = type == ThemeType.Light
? new Uri("avares://Penguin.AvaloniaUI/Themes/LightTheme.axaml")
: new Uri("avares://Penguin.AvaloniaUI/Themes/DarkTheme.axaml");
var newTheme = new ResourceDictionary { Source = uri };
app.Resources.MergedDictionaries.Add(newTheme);
_currentTheme = type;
ThemeChanged?.Invoke(null, type);
}
预加载主题资源(可选优化):
private static ResourceDictionary? _cachedLightTheme;
private static ResourceDictionary? _cachedDarkTheme;
static ThemeManager()
{
// 应用启动时预加载主题
_cachedLightTheme = LoadThemeResource(ThemeType.Light);
_cachedDarkTheme = LoadThemeResource(ThemeType.Dark);
}
public static void ApplyTheme(ThemeType type)
{
var theme = type == ThemeType.Light ? _cachedLightTheme : _cachedDarkTheme;
// 直接使用缓存的主题,无需重新加载
}
Memory Management
避免内存泄漏的常见陷阱:
问题 1:事件订阅未取消
// ❌ 可能导致内存泄漏
public class PropertyGrid : TemplatedControl
{
public PropertyGrid()
{
ThemeManager.ThemeChanged += OnThemeChanged;
// 没有取消订阅!
}
private void OnThemeChanged(object? sender, ThemeType type)
{
RefreshTheme();
}
}
// ✅ 正确实现 IDisposable
public class PropertyGrid : TemplatedControl, IDisposable
{
public PropertyGrid()
{
ThemeManager.ThemeChanged += OnThemeChanged;
}
public void Dispose()
{
ThemeManager.ThemeChanged -= OnThemeChanged;
}
private void OnThemeChanged(object? sender, ThemeType type)
{
RefreshTheme();
}
}
问题 2:ObservableCollection 过度使用
// ❌ 每次都创建新集合
public ObservableCollection<PropertyItem> Properties
{
get => new ObservableCollection<PropertyItem>(GetProperties());
}
// ✅ 复用同一个集合
private ObservableCollection<PropertyItem> _properties = new();
public ObservableCollection<PropertyItem> Properties => _properties;
private void RefreshProperties()
{
_properties.Clear();
foreach (var item in GetProperties())
{
_properties.Add(item);
}
}
Virtualization (Post-MVP)
如果 PropertyGrid 需要支持数百个属性,使用虚拟化:
<ItemsControl Items="{Binding Properties}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<!-- VirtualizingStackPanel 仅渲染可见项 -->
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
虚拟化性能对比:
100 个属性(非虚拟化): ~300ms 生成 + 50MB 内存
100 个属性(虚拟化): ~100ms 生成 + 10MB 内存
Avoid Common Pitfalls
陷阱 1:在循环中触发属性变化通知
// ❌ 每次循环触发 UI 刷新
for (int i = 0; i < 1000; i++)
{
Properties.Add(new PropertyItem()); // 触发 CollectionChanged
}
// ✅ 批量添加后触发一次刷新
var items = new List<PropertyItem>();
for (int i = 0; i < 1000; i++)
{
items.Add(new PropertyItem());
}
Properties.Clear();
foreach (var item in items)
{
Properties.Add(item);
}
陷阱 2:频繁的字符串拼接
// ❌ 每次拼接创建新字符串
string result = "";
for (int i = 0; i < 100; i++)
{
result += properties[i].Name + ", ";
}
// ✅ 使用 StringBuilder
var sb = new StringBuilder();
for (int i = 0; i < 100; i++)
{
sb.Append(properties[i].Name).Append(", ");
}
var result = sb.ToString();
陷阱 3:XAML 中的复杂绑定
<!-- ❌ 每次渲染都执行转换 -->
<TextBlock Text="{Binding PropertyType, Converter={StaticResource ComplexConverter}}" />
<!-- ✅ 在 ViewModel 中预计算 -->
<TextBlock Text="{Binding PropertyTypeDisplayName}" />
Performance Measurement
使用 Stopwatch 测量关键操作:
#if DEBUG
private void RefreshProperties(object? obj)
{
var sw = Stopwatch.StartNew();
// 核心逻辑
_properties.Clear();
var items = GetProperties(obj);
foreach (var item in items)
{
_properties.Add(item);
}
sw.Stop();
Debug.WriteLine($"[PropertyGrid] RefreshProperties took {sw.ElapsedMilliseconds}ms for {items.Count} properties");
}
#endif
Visual Studio Performance Profiler:
- 调试 → 性能探查器
- 选择"CPU 使用情况"或".NET 对象分配跟踪"
- 启动分析
- 执行性能关键操作(如生成 50 个属性)
- 停止分析并查看热点代码
dotnet-counters 监控内存:
# 安装工具
dotnet tool install --global dotnet-counters
# 监控运行中的应用
dotnet-counters monitor --process-id <pid> --counters System.Runtime
Optimization Checklist
开发时检查清单:
- 反射操作已缓存(PropertyInfo、Attribute)
- 耗时操作(> 50ms)在后台线程执行
- 事件订阅在控件销毁时取消
- ObservableCollection 复用而非重建
- 批量更新集合,避免频繁触发 CollectionChanged
- 字符串拼接使用 StringBuilder
- XAML 绑定避免复杂转换器
- 主题切换批量替换 ResourceDictionary
发布前性能测试:
- PropertyGrid 50 属性生成 < 200ms
- 主题切换 < 100ms
- 应用启动 < 2s
- 空闲时内存 < 100MB
- 无明显的 UI 卡顿(帧率 > 50fps)
文档完成
文档状态:✅ 完成(100%) 最后更新:2025-10-16
本架构文档涵盖了 Penguin.AvaloniaUI 控件库的完整技术架构,包括:
✅ 高层架构设计和技术选型 ✅ 核心数据模型定义 ✅ 组件设计和依赖关系 ✅ 项目结构和文件组织 ✅ 开发工作流和环境配置 ✅ 编码规范和最佳实践 ✅ 测试策略和示例 ✅ 错误处理机制 ✅ 性能优化指导
下一步: 基于本架构文档开始实施 PRD 中定义的 Epic 和 Story。