From 6c17e027314c663b66defe4a62809fbf5d1797d0 Mon Sep 17 00:00:00 2001 From: chuan Date: Thu, 16 Oct 2025 15:08:42 +0800 Subject: [PATCH] docs: update architecture --- docs/architecture.md | 2439 +++++++++++++++++ docs/architecture/coding-standards.md | 109 + docs/architecture/components.md | 357 +++ docs/architecture/data-models.md | 207 ++ docs/architecture/development-workflow.md | 180 ++ docs/architecture/error-handling.md | 428 +++ docs/architecture/high-level-architecture.md | 144 + docs/architecture/index.md | 17 + docs/architecture/introduction.md | 42 + docs/architecture/performance-optimization.md | 415 +++ docs/architecture/tech-stack.md | 64 + docs/architecture/testing-strategy.md | 330 +++ .../architecture/unified-project-structure.md | 135 + src/Example/Program.cs | 35 +- .../Penguin.AvaloniaUI.csproj | 5 + 15 files changed, 4889 insertions(+), 18 deletions(-) create mode 100644 docs/architecture.md create mode 100644 docs/architecture/coding-standards.md create mode 100644 docs/architecture/components.md create mode 100644 docs/architecture/data-models.md create mode 100644 docs/architecture/development-workflow.md create mode 100644 docs/architecture/error-handling.md create mode 100644 docs/architecture/high-level-architecture.md create mode 100644 docs/architecture/index.md create mode 100644 docs/architecture/introduction.md create mode 100644 docs/architecture/performance-optimization.md create mode 100644 docs/architecture/tech-stack.md create mode 100644 docs/architecture/testing-strategy.md create mode 100644 docs/architecture/unified-project-structure.md diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..bf0e549 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,2439 @@ +# 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 + +```mermaid +graph TB + subgraph "开发者应用" + App[Avalonia Application] + end + + subgraph "Penguin.AvaloniaUI 控件库" + Controls[Controls Layer
PropertyGrid, UserGuide] + Layouts[Layouts Layer
TwoColumnLayout, Overlay] + Themes[Themes Layer
ThemeManager, ColorSystem] + Utils[Utils Layer
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`: + +```xml + + + true + + + + + + + + + +``` + +各项目的 `.csproj` 文件中引用包时**不指定版本号**: + +```xml + + + + +``` + +**优势**: +- 所有项目使用相同的包版本 +- 升级版本时只需修改一处 +- 避免版本冲突导致的编译或运行时错误 + +--- + +## Data Models + +核心数据模型定义了控件库中业务逻辑层的数据结构。这些模型在 UI 控件层和业务逻辑层之间共享。 + +--- + +### PropertyItem + +**用途**:表示 PropertyGrid 中的单个属性项,封装属性的元数据和值。 + +**关键属性**: +- `Name`(string)- 属性名称,显示在左列标签 +- `Value`(object)- 属性当前值,支持双向绑定 +- `PropertyType`(Type)- 属性的 .NET 类型,用于选择合适的编辑器 +- `IsReadOnly`(bool)- 是否只读,只读属性禁用编辑器 +- `Category`(string?)- 分组类别,用于属性分组显示 +- `Description`(string?)- 属性描述,可选的辅助说明 +- `DisplayName`(string?)- 显示名称,如果为空则使用 Name + +**C# 类定义**: + +```csharp +namespace Penguin.AvaloniaUI.Controls.PropertyGrid +{ + /// + /// 表示 PropertyGrid 中的单个属性项 + /// + public class PropertyItem : ReactiveObject + { + private object? _value; + + /// + /// 属性名称 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 属性显示名称(如果为空则使用 Name) + /// + public string? DisplayName { get; set; } + + /// + /// 属性值(支持双向绑定) + /// + public object? Value + { + get => _value; + set => this.RaiseAndSetIfChanged(ref _value, value); + } + + /// + /// 属性的 .NET 类型 + /// + public Type PropertyType { get; set; } = typeof(object); + + /// + /// 是否只读 + /// + public bool IsReadOnly { get; set; } + + /// + /// 分组类别(可选) + /// + public string? Category { get; set; } + + /// + /// 属性描述(可选) + /// + public string? Description { get; set; } + + /// + /// 原始 PropertyInfo(用于反射场景) + /// + public PropertyInfo? PropertyInfo { get; set; } + } +} +``` + +**关系**: +- PropertyGrid 包含多个 PropertyItem(`ObservableCollection`) +- PropertyItem 通过 PropertyType 决定使用哪个编辑器控件 + +--- + +### GuideStep + +**用途**:表示 UserGuide 引导流程中的单个步骤。 + +**关键属性**: +- `TargetControl`(Control?)- 引导目标控件,Overlay 将聚焦此控件 +- `Title`(string)- 步骤标题,显示在引导提示框顶部 +- `Content`(string)- 步骤内容/提示文本,支持基础富文本 +- `Position`(TooltipPosition)- 提示框相对于目标控件的位置 +- `Order`(int)- 步骤顺序,用于排序 + +**C# 类定义**: + +```csharp +namespace Penguin.AvaloniaUI.Controls.UserGuide +{ + /// + /// 提示框位置枚举 + /// + public enum TooltipPosition + { + Bottom, + Top, + Left, + Right + } + + /// + /// 表示 UserGuide 中的单个引导步骤 + /// + public class GuideStep : ReactiveObject + { + /// + /// 引导目标控件(可选,如果为空则显示全屏提示) + /// + public Control? TargetControl { get; set; } + + /// + /// 步骤标题 + /// + public string Title { get; set; } = string.Empty; + + /// + /// 步骤内容(支持基础富文本) + /// + public string Content { get; set; } = string.Empty; + + /// + /// 提示框位置 + /// + public TooltipPosition Position { get; set; } = TooltipPosition.Bottom; + + /// + /// 步骤顺序(用于排序) + /// + public int Order { get; set; } + + /// + /// 是否允许跳过此步骤 + /// + public bool Skippable { get; set; } = true; + } +} +``` + +**关系**: +- UserGuide 包含多个 GuideStep(`ObservableCollection`) +- GuideStep 通过 TargetControl 关联到具体的 UI 控件 +- Overlay 根据 TargetControl 的位置和大小进行挖空显示 + +--- + +### ThemeInfo(辅助模型) + +**用途**:表示主题信息,用于 ThemeManager 管理主题切换。 + +**关键属性**: +- `ThemeType`(ThemeType)- 主题类型枚举(Light、Dark) +- `ResourceUri`(Uri)- 主题资源字典的 URI + +**C# 类定义**: + +```csharp +namespace Penguin.AvaloniaUI.Themes +{ + /// + /// 主题类型枚举 + /// + public enum ThemeType + { + Light, + Dark + } + + /// + /// 主题信息 + /// + public class ThemeInfo + { + /// + /// 主题类型 + /// + public ThemeType Type { get; set; } + + /// + /// 主题资源字典 URI + /// + public Uri ResourceUri { get; set; } = null!; + + /// + /// 主题显示名称 + /// + 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()` 读取 Attribute(如 `[Category]`、`[Browsable]`) +- 编辑器选择逻辑:根据 `PropertyItem.PropertyType` 映射到对应控件 + +**实现示例**(核心逻辑): + +```csharp +public class PropertyGrid : TemplatedControl +{ + public static readonly StyledProperty SelectedObjectProperty = + AvaloniaProperty.Register(nameof(SelectedObject)); + + public object? SelectedObject + { + get => GetValue(SelectedObjectProperty); + set => SetValue(SelectedObjectProperty, value); + } + + public ObservableCollection 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) + +**核心逻辑**: + +```csharp +public class UserGuide : ContentControl +{ + public ObservableCollection Steps { get; } = new(); + private int _currentStepIndex; + + public ReactiveCommand NextStepCommand { get; } + public ReactiveCommand PreviousStepCommand { get; } + public ReactiveCommand 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请仔细阅读。" +输出: + + 注意: 这是一个重要提示。 + + 请仔细阅读。 + +``` + +--- + +### ThemeManager + +**职责**:管理主题切换,提供全局主题访问接口。 + +**关键接口**: +- `ApplyTheme(ThemeType type)`(方法)- 切换主题 +- `CurrentTheme`(ThemeType)- 当前主题类型 +- `ThemeChanged`(事件)- 主题变化事件 + +**依赖关系**: +- 被所有控件使用(通过静态方法或单例) +- 依赖 Avalonia 的 `Application.Current.Resources` + +**技术细节**: +- 静态类或单例模式 +- 主题切换通过替换 `ResourceDictionary` 实现 +- 支持动画过渡(可选,100ms 淡入淡出) + +**实现示例**: + +```csharp +public static class ThemeManager +{ + private static ThemeType _currentTheme = ThemeType.Light; + public static event EventHandler? 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 + +```mermaid +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 项目组织最佳实践,以下是详细的项目文件结构: + +```plaintext +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 + +**前置要求:** + +```bash +# 必需 +- .NET 9.0 SDK (https://dotnet.microsoft.com/download) +- Visual Studio 2022 (17.8+) +- Git 2.40+ + +# 推荐 +- Visual Studio Avalonia 扩展 (用于 XAML 预览) +``` + +**初始化项目:** + +```bash +# 克隆仓库 +git clone +cd 32_avalonia.ui + +# 还原 NuGet 包 +dotnet restore + +# 构建解决方案 +dotnet build + +# 运行示例应用 +dotnet run --project src/Example/Example.csproj +``` + +**Visual Studio 配置:** + +1. 打开 `Penguin.AvaloniaUI.sln` +2. 设置 `Example` 为启动项目 +3. 选择调试配置(Debug/Release) +4. 按 F5 启动调试 + +--- + +### Development Commands + +**常用命令:** + +```bash +# 启动示例应用(开发模式,支持热重载) +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` 中确保已启用: + +```xml + + true + +``` + +--- + +### Environment Configuration + +**控件库无需环境变量**(MVP 阶段),但示例应用可能需要: + +```bash +# Example/.env (可选,用于测试外部 API 集成) +# MVP 阶段不需要 +``` + +**调试配置(launchSettings.json):** + +```json +{ + "profiles": { + "Example": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} +``` + +--- + +### Git Workflow + +**Commit Message 格式(Conventional Commits):** + +``` +: + +[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`) + +**示例:** +```bash +git commit -m "feat: 实现PropertyGrid的属性分组功能" +git commit -m "fix: 修复TwoColumnLayout在窗口缩放时的对齐问题" +git commit -m "test: 添加ThemeManager的主题切换测试" +``` + +**分支命名规范:** +- `feat/` - 新功能分支(如 `feat/usercontrol`) +- `fix/` - Bug 修复分支(如 `fix/1123`) +- `refactor/` - 重构分支(如 `refactor/theme-system`) +- `test/` - 测试分支(如 `test/integration`) + +**日常开发流程:** + +1. **创建功能分支**:`git checkout -b feat/two-column-layout` +2. **编写代码**:在 `src/Penguin.AvaloniaUI/` 中实现控件 +3. **编写测试**:在 `src/Penguin.AvaloniaUI.Tests/` 中添加单元测试 +4. **更新示例**:在 `src/Example/Views/Pages/` 中添加演示页面 +5. **本地验证**: + - 运行单元测试:`dotnet test` + - 启动示例应用:`dotnet run --project src/Example` + - 手动测试主题切换和控件交互 +6. **提交代码**: + ```bash + 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 注释(`///`),包括 ``、``、``。示例: + ```csharp + /// + /// 应用指定的主题类型 + /// + /// 主题类型(Light 或 Dark) + 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`)与控件类分离,放在同级目录下。 + +- **文件格式规范**:命名空间单独放在文件顶部,减少缩进层级: + ```csharp + namespace Penguin.AvaloniaUI.Controls; + + public class PropertyGrid : TemplatedControl + { + // 类实现 + } + ``` + +--- + +### Code Style (基于 .editorconfig) + +项目使用 `.editorconfig` 强制执行以下规则: + +```ini +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;`) +- 命名空间单独一行,不使用文件范围命名空间的花括号嵌套 + +**运行格式化检查:** +```bash +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 的反射逻辑** + +```csharp +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** + +```csharp +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 步骤流转** + +```csharp +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 + +**创建测试辅助类减少重复代码:** + +```csharp +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 + +```bash +# 运行所有测试 +dotnet test + +# 运行特定测试类 +dotnet test --filter "FullyQualifiedName~PropertyGridTests" + +# 运行特定测试方法 +dotnet test --filter "Name~RefreshProperties_WithValidObject" + +# 生成代码覆盖率报告 +dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=html +``` + +**Visual Studio 测试运行器:** +1. 打开 Test Explorer(测试 → 测试资源管理器) +2. 点击"运行所有测试"或右键运行特定测试 +3. 查看测试结果和失败详情 + +--- + +## Error Handling + +本节定义控件库的错误处理策略,确保异常情况下的稳定性和良好的开发者体验。 + +--- + +### Error Handling Principles + +**核心原则:** + +1. **Fail Fast(快速失败)**:公开 API 在参数无效时立即抛出异常,不延迟到执行阶段 +2. **Clear Error Messages(清晰错误信息)**:异常消息应明确指出问题和解决方案 +3. **No Silent Failures(不吞没异常)**:禁止空 catch 块,必须记录或重新抛出异常 +4. **Defensive Programming(防御性编程)**:内部方法使用 `Debug.Assert` 验证前置条件 + +--- + +### Exception Handling Patterns + +**公开 API 参数验证:** + +```csharp +namespace Penguin.AvaloniaUI.Themes; + +public static class ThemeManager +{ + /// + /// 应用指定的主题类型 + /// + /// 主题类型 + /// 当主题类型无效时抛出 + 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 错误处理:** + +```csharp +namespace Penguin.AvaloniaUI.Controls.PropertyGrid; + +public class PropertyGrid : TemplatedControl +{ + public static readonly StyledProperty SelectedObjectProperty = + AvaloniaProperty.Register(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 错误处理:** + +```csharp +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 进行调试日志:** + +```csharp +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}"); + } + } +} +``` + +**使用示例:** + +```csharp +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 + +**好的错误消息示例:** + +```csharp +// ❌ 不好的错误消息 +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:** + +```csharp +// ✅ 捕获特定异常并提供上下文 +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 + +**创建参数验证辅助方法:** + +```csharp +namespace Penguin.AvaloniaUI.Utils; + +internal static class Guard +{ + public static void NotNull(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 线程:** + +```csharp +// 错误:在属性 getter 中执行反射 +public ObservableCollection Properties +{ + get + { + var items = new ObservableCollection(); + if (SelectedObject != null) + { + // 耗时操作会阻塞 UI 渲染 + var properties = SelectedObject.GetType().GetProperties(); + foreach (var prop in properties) + { + items.Add(CreatePropertyItem(SelectedObject, prop)); + } + } + return items; + } +} +``` + +**✅ 正确示例 - 异步加载:** + +```csharp +private ObservableCollection _properties = new(); +public ObservableCollection Properties => _properties; + +private async void RefreshProperties(object? obj) +{ + _properties.Clear(); + + if (obj == null) return; + + // 在后台线程执行反射 + var items = await Task.Run(() => + { + var result = new List(); + 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** + +```csharp +private static readonly ConcurrentDictionary _propertyCache = new(); + +private PropertyInfo[] GetCachedProperties(Type type) +{ + return _propertyCache.GetOrAdd(type, t => + t.GetProperties(BindingFlags.Public | BindingFlags.Instance) + ); +} +``` + +**优化策略 2:缓存 Attribute 读取** + +```csharp +private static readonly ConcurrentDictionary _categoryCache = new(); + +private string? GetCategory(PropertyInfo prop) +{ + var attr = _categoryCache.GetOrAdd(prop, p => + p.GetCustomAttribute() + ); + + return attr?.Category; +} +``` + +**优化策略 3:使用编译表达式替代反射调用(可选)** + +```csharp +// 反射调用(慢) +var value = propertyInfo.GetValue(obj); + +// 编译表达式(快 10-100 倍) +private static Func 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>(convertResult, instance).Compile(); +} +``` + +**性能对比:** +``` +反射 GetValue: ~1000 ns/调用 +编译表达式: ~10 ns/调用 +直接属性访问: ~1 ns/调用 +``` + +--- + +### Theme Switching Performance + +**优化主题切换速度(目标 < 100ms):** + +**❌ 反面示例 - 逐个替换资源:** + +```csharp +// 错误:逐个替换资源会触发多次 UI 刷新 +foreach (var key in themeColors.Keys) +{ + Application.Current.Resources[key] = themeColors[key]; +} +``` + +**✅ 正确示例 - 批量替换 ResourceDictionary:** + +```csharp +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); +} +``` + +**预加载主题资源(可选优化):** + +```csharp +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:事件订阅未取消** + +```csharp +// ❌ 可能导致内存泄漏 +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 过度使用** + +```csharp +// ❌ 每次都创建新集合 +public ObservableCollection Properties +{ + get => new ObservableCollection(GetProperties()); +} + +// ✅ 复用同一个集合 +private ObservableCollection _properties = new(); +public ObservableCollection Properties => _properties; + +private void RefreshProperties() +{ + _properties.Clear(); + foreach (var item in GetProperties()) + { + _properties.Add(item); + } +} +``` + +--- + +### Virtualization (Post-MVP) + +**如果 PropertyGrid 需要支持数百个属性,使用虚拟化:** + +```xml + + + + + + + + +``` + +**虚拟化性能对比:** +``` +100 个属性(非虚拟化): ~300ms 生成 + 50MB 内存 +100 个属性(虚拟化): ~100ms 生成 + 10MB 内存 +``` + +--- + +### Avoid Common Pitfalls + +**陷阱 1:在循环中触发属性变化通知** + +```csharp +// ❌ 每次循环触发 UI 刷新 +for (int i = 0; i < 1000; i++) +{ + Properties.Add(new PropertyItem()); // 触发 CollectionChanged +} + +// ✅ 批量添加后触发一次刷新 +var items = new List(); +for (int i = 0; i < 1000; i++) +{ + items.Add(new PropertyItem()); +} +Properties.Clear(); +foreach (var item in items) +{ + Properties.Add(item); +} +``` + +**陷阱 2:频繁的字符串拼接** + +```csharp +// ❌ 每次拼接创建新字符串 +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 中的复杂绑定** + +```xml + + + + + +``` + +--- + +### Performance Measurement + +**使用 Stopwatch 测量关键操作:** + +```csharp +#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:** + +1. 调试 → 性能探查器 +2. 选择"CPU 使用情况"或".NET 对象分配跟踪" +3. 启动分析 +4. 执行性能关键操作(如生成 50 个属性) +5. 停止分析并查看热点代码 + +**dotnet-counters 监控内存:** + +```bash +# 安装工具 +dotnet tool install --global dotnet-counters + +# 监控运行中的应用 +dotnet-counters monitor --process-id --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。 diff --git a/docs/architecture/coding-standards.md b/docs/architecture/coding-standards.md new file mode 100644 index 0000000..81ef373 --- /dev/null +++ b/docs/architecture/coding-standards.md @@ -0,0 +1,109 @@ +# 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 注释(`///`),包括 ``、``、``。示例: + ```csharp + /// + /// 应用指定的主题类型 + /// + /// 主题类型(Light 或 Dark) + 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`)与控件类分离,放在同级目录下。 + +- **文件格式规范**:命名空间单独放在文件顶部,减少缩进层级: + ```csharp + namespace Penguin.AvaloniaUI.Controls; + + public class PropertyGrid : TemplatedControl + { + // 类实现 + } + ``` + +--- + +### Code Style (基于 .editorconfig) + +项目使用 `.editorconfig` 强制执行以下规则: + +```ini +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;`) +- 命名空间单独一行,不使用文件范围命名空间的花括号嵌套 + +**运行格式化检查:** +```bash +dotnet format --verify-no-changes +``` + +--- + \ No newline at end of file diff --git a/docs/architecture/components.md b/docs/architecture/components.md new file mode 100644 index 0000000..0faadbf --- /dev/null +++ b/docs/architecture/components.md @@ -0,0 +1,357 @@ +# Components + +基于架构模式和数据模型,系统划分为以下核心组件。每个组件负责特定功能,并通过清晰的接口与其他组件交互。 + +--- + +### PropertyGrid + +**职责**:自动生成属性编辑 UI,支持多种属性类型、分组、只读属性和基础验证。 + +**关键接口**: +- `SelectedObject`(依赖属性)- 绑定的数据对象,变化时自动刷新属性列表 +- `Properties`(ObservableCollection)- 解析后的属性列表 +- `PropertyValueChanged`(事件)- 属性值变化时触发 + +**依赖关系**: +- 依赖 TwoColumnLayout 进行布局 +- 依赖 PropertyEditors(TextBox、NumericUpDown、ComboBox 等) +- 依赖 ThemeManager 获取当前主题颜色 +- 使用反射(System.Reflection)解析对象属性 + +**技术细节**: +- 继承自 `TemplatedControl` +- 使用 ReactiveUI 的 `WhenAnyValue` 监听 SelectedObject 变化 +- 通过 `PropertyInfo.GetCustomAttributes()` 读取 Attribute(如 `[Category]`、`[Browsable]`) +- 编辑器选择逻辑:根据 `PropertyItem.PropertyType` 映射到对应控件 + +**实现示例**(核心逻辑): + +```csharp +public class PropertyGrid : TemplatedControl +{ + public static readonly StyledProperty SelectedObjectProperty = + AvaloniaProperty.Register(nameof(SelectedObject)); + + public object? SelectedObject + { + get => GetValue(SelectedObjectProperty); + set => SetValue(SelectedObjectProperty, value); + } + + public ObservableCollection 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) + +**核心逻辑**: + +```csharp +public class UserGuide : ContentControl +{ + public ObservableCollection Steps { get; } = new(); + private int _currentStepIndex; + + public ReactiveCommand NextStepCommand { get; } + public ReactiveCommand PreviousStepCommand { get; } + public ReactiveCommand 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请仔细阅读。" +输出: + + 注意: 这是一个重要提示。 + + 请仔细阅读。 + +``` + +--- + +### ThemeManager + +**职责**:管理主题切换,提供全局主题访问接口。 + +**关键接口**: +- `ApplyTheme(ThemeType type)`(方法)- 切换主题 +- `CurrentTheme`(ThemeType)- 当前主题类型 +- `ThemeChanged`(事件)- 主题变化事件 + +**依赖关系**: +- 被所有控件使用(通过静态方法或单例) +- 依赖 Avalonia 的 `Application.Current.Resources` + +**技术细节**: +- 静态类或单例模式 +- 主题切换通过替换 `ResourceDictionary` 实现 +- 支持动画过渡(可选,100ms 淡入淡出) + +**实现示例**: + +```csharp +public static class ThemeManager +{ + private static ThemeType _currentTheme = ThemeType.Light; + public static event EventHandler? 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 + +```mermaid +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 +``` + +--- + \ No newline at end of file diff --git a/docs/architecture/data-models.md b/docs/architecture/data-models.md new file mode 100644 index 0000000..1e106b5 --- /dev/null +++ b/docs/architecture/data-models.md @@ -0,0 +1,207 @@ +# Data Models + +核心数据模型定义了控件库中业务逻辑层的数据结构。这些模型在 UI 控件层和业务逻辑层之间共享。 + +--- + +### PropertyItem + +**用途**:表示 PropertyGrid 中的单个属性项,封装属性的元数据和值。 + +**关键属性**: +- `Name`(string)- 属性名称,显示在左列标签 +- `Value`(object)- 属性当前值,支持双向绑定 +- `PropertyType`(Type)- 属性的 .NET 类型,用于选择合适的编辑器 +- `IsReadOnly`(bool)- 是否只读,只读属性禁用编辑器 +- `Category`(string?)- 分组类别,用于属性分组显示 +- `Description`(string?)- 属性描述,可选的辅助说明 +- `DisplayName`(string?)- 显示名称,如果为空则使用 Name + +**C# 类定义**: + +```csharp +namespace Penguin.AvaloniaUI.Controls.PropertyGrid +{ + /// + /// 表示 PropertyGrid 中的单个属性项 + /// + public class PropertyItem : ReactiveObject + { + private object? _value; + + /// + /// 属性名称 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 属性显示名称(如果为空则使用 Name) + /// + public string? DisplayName { get; set; } + + /// + /// 属性值(支持双向绑定) + /// + public object? Value + { + get => _value; + set => this.RaiseAndSetIfChanged(ref _value, value); + } + + /// + /// 属性的 .NET 类型 + /// + public Type PropertyType { get; set; } = typeof(object); + + /// + /// 是否只读 + /// + public bool IsReadOnly { get; set; } + + /// + /// 分组类别(可选) + /// + public string? Category { get; set; } + + /// + /// 属性描述(可选) + /// + public string? Description { get; set; } + + /// + /// 原始 PropertyInfo(用于反射场景) + /// + public PropertyInfo? PropertyInfo { get; set; } + } +} +``` + +**关系**: +- PropertyGrid 包含多个 PropertyItem(`ObservableCollection`) +- PropertyItem 通过 PropertyType 决定使用哪个编辑器控件 + +--- + +### GuideStep + +**用途**:表示 UserGuide 引导流程中的单个步骤。 + +**关键属性**: +- `TargetControl`(Control?)- 引导目标控件,Overlay 将聚焦此控件 +- `Title`(string)- 步骤标题,显示在引导提示框顶部 +- `Content`(string)- 步骤内容/提示文本,支持基础富文本 +- `Position`(TooltipPosition)- 提示框相对于目标控件的位置 +- `Order`(int)- 步骤顺序,用于排序 + +**C# 类定义**: + +```csharp +namespace Penguin.AvaloniaUI.Controls.UserGuide +{ + /// + /// 提示框位置枚举 + /// + public enum TooltipPosition + { + Bottom, + Top, + Left, + Right + } + + /// + /// 表示 UserGuide 中的单个引导步骤 + /// + public class GuideStep : ReactiveObject + { + /// + /// 引导目标控件(可选,如果为空则显示全屏提示) + /// + public Control? TargetControl { get; set; } + + /// + /// 步骤标题 + /// + public string Title { get; set; } = string.Empty; + + /// + /// 步骤内容(支持基础富文本) + /// + public string Content { get; set; } = string.Empty; + + /// + /// 提示框位置 + /// + public TooltipPosition Position { get; set; } = TooltipPosition.Bottom; + + /// + /// 步骤顺序(用于排序) + /// + public int Order { get; set; } + + /// + /// 是否允许跳过此步骤 + /// + public bool Skippable { get; set; } = true; + } +} +``` + +**关系**: +- UserGuide 包含多个 GuideStep(`ObservableCollection`) +- GuideStep 通过 TargetControl 关联到具体的 UI 控件 +- Overlay 根据 TargetControl 的位置和大小进行挖空显示 + +--- + +### ThemeInfo(辅助模型) + +**用途**:表示主题信息,用于 ThemeManager 管理主题切换。 + +**关键属性**: +- `ThemeType`(ThemeType)- 主题类型枚举(Light、Dark) +- `ResourceUri`(Uri)- 主题资源字典的 URI + +**C# 类定义**: + +```csharp +namespace Penguin.AvaloniaUI.Themes +{ + /// + /// 主题类型枚举 + /// + public enum ThemeType + { + Light, + Dark + } + + /// + /// 主题信息 + /// + public class ThemeInfo + { + /// + /// 主题类型 + /// + public ThemeType Type { get; set; } + + /// + /// 主题资源字典 URI + /// + public Uri ResourceUri { get; set; } = null!; + + /// + /// 主题显示名称 + /// + public string DisplayName { get; set; } = string.Empty; + } +} +``` + +**关系**: +- ThemeManager 使用 ThemeInfo 管理可用主题 +- 主题切换时通过 ResourceUri 加载对应的 ResourceDictionary + +--- + \ No newline at end of file diff --git a/docs/architecture/development-workflow.md b/docs/architecture/development-workflow.md new file mode 100644 index 0000000..dd1e609 --- /dev/null +++ b/docs/architecture/development-workflow.md @@ -0,0 +1,180 @@ +# Development Workflow + +本节定义控件库的开发环境配置和日常开发流程,确保开发者能够快速上手并保持高效开发。 + +--- + +### Local Development Setup + +**前置要求:** + +```bash +# 必需 +- .NET 9.0 SDK (https://dotnet.microsoft.com/download) +- Visual Studio 2022 (17.8+) +- Git 2.40+ + +# 推荐 +- Visual Studio Avalonia 扩展 (用于 XAML 预览) +``` + +**初始化项目:** + +```bash +# 克隆仓库 +git clone +cd 32_avalonia.ui + +# 还原 NuGet 包 +dotnet restore + +# 构建解决方案 +dotnet build + +# 运行示例应用 +dotnet run --project src/Example/Example.csproj +``` + +**Visual Studio 配置:** + +1. 打开 `Penguin.AvaloniaUI.sln` +2. 设置 `Example` 为启动项目 +3. 选择调试配置(Debug/Release) +4. 按 F5 启动调试 + +--- + +### Development Commands + +**常用命令:** + +```bash +# 启动示例应用(开发模式,支持热重载) +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` 中确保已启用: + +```xml + + true + +``` + +--- + +### Environment Configuration + +**控件库无需环境变量**(MVP 阶段),但示例应用可能需要: + +```bash +# Example/.env (可选,用于测试外部 API 集成) +# MVP 阶段不需要 +``` + +**调试配置(launchSettings.json):** + +```json +{ + "profiles": { + "Example": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} +``` + +--- + +### Git Workflow + +**Commit Message 格式(Conventional Commits):** + +``` +: + +[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`) + +**示例:** +```bash +git commit -m "feat: 实现PropertyGrid的属性分组功能" +git commit -m "fix: 修复TwoColumnLayout在窗口缩放时的对齐问题" +git commit -m "test: 添加ThemeManager的主题切换测试" +``` + +**分支命名规范:** +- `feat/` - 新功能分支(如 `feat/usercontrol`) +- `fix/` - Bug 修复分支(如 `fix/1123`) +- `refactor/` - 重构分支(如 `refactor/theme-system`) +- `test/` - 测试分支(如 `test/integration`) + +**日常开发流程:** + +1. **创建功能分支**:`git checkout -b feat/two-column-layout` +2. **编写代码**:在 `src/Penguin.AvaloniaUI/` 中实现控件 +3. **编写测试**:在 `src/Penguin.AvaloniaUI.Tests/` 中添加单元测试 +4. **更新示例**:在 `src/Example/Views/Pages/` 中添加演示页面 +5. **本地验证**: + - 运行单元测试:`dotnet test` + - 启动示例应用:`dotnet run --project src/Example` + - 手动测试主题切换和控件交互 +6. **提交代码**: + ```bash + git add . + git commit -m "feat: 实现TwoColumnLayout布局控件" + git push origin feat/two-column-layout + ``` + +--- + \ No newline at end of file diff --git a/docs/architecture/error-handling.md b/docs/architecture/error-handling.md new file mode 100644 index 0000000..affd421 --- /dev/null +++ b/docs/architecture/error-handling.md @@ -0,0 +1,428 @@ +# Error Handling + +本节定义控件库的错误处理策略,确保异常情况下的稳定性和良好的开发者体验。 + +--- + +### Error Handling Principles + +**核心原则:** + +1. **Fail Fast(快速失败)**:公开 API 在参数无效时立即抛出异常,不延迟到执行阶段 +2. **Clear Error Messages(清晰错误信息)**:异常消息应明确指出问题和解决方案 +3. **No Silent Failures(不吞没异常)**:禁止空 catch 块,必须记录或重新抛出异常 +4. **Defensive Programming(防御性编程)**:内部方法使用 `Debug.Assert` 验证前置条件 + +--- + +### Exception Handling Patterns + +**公开 API 参数验证:** + +```csharp +namespace Penguin.AvaloniaUI.Themes; + +public static class ThemeManager +{ + /// + /// 应用指定的主题类型 + /// + /// 主题类型 + /// 当主题类型无效时抛出 + 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 错误处理:** + +```csharp +namespace Penguin.AvaloniaUI.Controls.PropertyGrid; + +public class PropertyGrid : TemplatedControl +{ + public static readonly StyledProperty SelectedObjectProperty = + AvaloniaProperty.Register(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 错误处理:** + +```csharp +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 进行调试日志:** + +```csharp +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}"); + } + } +} +``` + +**使用示例:** + +```csharp +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 + +**好的错误消息示例:** + +```csharp +// ❌ 不好的错误消息 +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:** + +```csharp +// ✅ 捕获特定异常并提供上下文 +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 + +**创建参数验证辅助方法:** + +```csharp +namespace Penguin.AvaloniaUI.Utils; + +internal static class Guard +{ + public static void NotNull(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)); + + // 核心逻辑 +} +``` + +--- + \ No newline at end of file diff --git a/docs/architecture/high-level-architecture.md b/docs/architecture/high-level-architecture.md new file mode 100644 index 0000000..95d0c2c --- /dev/null +++ b/docs/architecture/high-level-architecture.md @@ -0,0 +1,144 @@ +# 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 + +```mermaid +graph TB + subgraph "开发者应用" + App[Avalonia Application] + end + + subgraph "Penguin.AvaloniaUI 控件库" + Controls[Controls Layer
PropertyGrid, UserGuide] + Layouts[Layouts Layer
TwoColumnLayout, Overlay] + Themes[Themes Layer
ThemeManager, ColorSystem] + Utils[Utils Layer
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_:提高代码复用性,避免深层继承带来的维护问题 + +--- + \ No newline at end of file diff --git a/docs/architecture/index.md b/docs/architecture/index.md new file mode 100644 index 0000000..7dc9be4 --- /dev/null +++ b/docs/architecture/index.md @@ -0,0 +1,17 @@ +# Penguin.AvaloniaUI Architecture Document + +## Table of Contents + +- [Penguin.AvaloniaUI Architecture Document](#table-of-contents) + - [Introduction](#introduction) + - [High Level Architecture](#high-level-architecture) + - [Tech Stack](#tech-stack) + - [Data Models](#data-models) + - [Components](#components) + - [Unified Project Structure](#unified-project-structure) + - [Development Workflow](#development-workflow) + - [Coding Standards](#coding-standards) + - [Testing Strategy](#testing-strategy) + - [Error Handling](#error-handling) + - [Performance Optimization](#performance-optimization) + - [文档完成](#文档完成) diff --git a/docs/architecture/introduction.md b/docs/architecture/introduction.md new file mode 100644 index 0000000..68bf3b2 --- /dev/null +++ b/docs/architecture/introduction.md @@ -0,0 +1,42 @@ +# 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 | + +--- + \ No newline at end of file diff --git a/docs/architecture/performance-optimization.md b/docs/architecture/performance-optimization.md new file mode 100644 index 0000000..e90288d --- /dev/null +++ b/docs/architecture/performance-optimization.md @@ -0,0 +1,415 @@ +# 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 线程:** + +```csharp +// 错误:在属性 getter 中执行反射 +public ObservableCollection Properties +{ + get + { + var items = new ObservableCollection(); + if (SelectedObject != null) + { + // 耗时操作会阻塞 UI 渲染 + var properties = SelectedObject.GetType().GetProperties(); + foreach (var prop in properties) + { + items.Add(CreatePropertyItem(SelectedObject, prop)); + } + } + return items; + } +} +``` + +**✅ 正确示例 - 异步加载:** + +```csharp +private ObservableCollection _properties = new(); +public ObservableCollection Properties => _properties; + +private async void RefreshProperties(object? obj) +{ + _properties.Clear(); + + if (obj == null) return; + + // 在后台线程执行反射 + var items = await Task.Run(() => + { + var result = new List(); + 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** + +```csharp +private static readonly ConcurrentDictionary _propertyCache = new(); + +private PropertyInfo[] GetCachedProperties(Type type) +{ + return _propertyCache.GetOrAdd(type, t => + t.GetProperties(BindingFlags.Public | BindingFlags.Instance) + ); +} +``` + +**优化策略 2:缓存 Attribute 读取** + +```csharp +private static readonly ConcurrentDictionary _categoryCache = new(); + +private string? GetCategory(PropertyInfo prop) +{ + var attr = _categoryCache.GetOrAdd(prop, p => + p.GetCustomAttribute() + ); + + return attr?.Category; +} +``` + +**优化策略 3:使用编译表达式替代反射调用(可选)** + +```csharp +// 反射调用(慢) +var value = propertyInfo.GetValue(obj); + +// 编译表达式(快 10-100 倍) +private static Func 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>(convertResult, instance).Compile(); +} +``` + +**性能对比:** +``` +反射 GetValue: ~1000 ns/调用 +编译表达式: ~10 ns/调用 +直接属性访问: ~1 ns/调用 +``` + +--- + +### Theme Switching Performance + +**优化主题切换速度(目标 < 100ms):** + +**❌ 反面示例 - 逐个替换资源:** + +```csharp +// 错误:逐个替换资源会触发多次 UI 刷新 +foreach (var key in themeColors.Keys) +{ + Application.Current.Resources[key] = themeColors[key]; +} +``` + +**✅ 正确示例 - 批量替换 ResourceDictionary:** + +```csharp +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); +} +``` + +**预加载主题资源(可选优化):** + +```csharp +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:事件订阅未取消** + +```csharp +// ❌ 可能导致内存泄漏 +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 过度使用** + +```csharp +// ❌ 每次都创建新集合 +public ObservableCollection Properties +{ + get => new ObservableCollection(GetProperties()); +} + +// ✅ 复用同一个集合 +private ObservableCollection _properties = new(); +public ObservableCollection Properties => _properties; + +private void RefreshProperties() +{ + _properties.Clear(); + foreach (var item in GetProperties()) + { + _properties.Add(item); + } +} +``` + +--- + +### Virtualization (Post-MVP) + +**如果 PropertyGrid 需要支持数百个属性,使用虚拟化:** + +```xml + + + + + + + + +``` + +**虚拟化性能对比:** +``` +100 个属性(非虚拟化): ~300ms 生成 + 50MB 内存 +100 个属性(虚拟化): ~100ms 生成 + 10MB 内存 +``` + +--- + +### Avoid Common Pitfalls + +**陷阱 1:在循环中触发属性变化通知** + +```csharp +// ❌ 每次循环触发 UI 刷新 +for (int i = 0; i < 1000; i++) +{ + Properties.Add(new PropertyItem()); // 触发 CollectionChanged +} + +// ✅ 批量添加后触发一次刷新 +var items = new List(); +for (int i = 0; i < 1000; i++) +{ + items.Add(new PropertyItem()); +} +Properties.Clear(); +foreach (var item in items) +{ + Properties.Add(item); +} +``` + +**陷阱 2:频繁的字符串拼接** + +```csharp +// ❌ 每次拼接创建新字符串 +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 中的复杂绑定** + +```xml + + + + + +``` + +--- + +### Performance Measurement + +**使用 Stopwatch 测量关键操作:** + +```csharp +#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:** + +1. 调试 → 性能探查器 +2. 选择"CPU 使用情况"或".NET 对象分配跟踪" +3. 启动分析 +4. 执行性能关键操作(如生成 50 个属性) +5. 停止分析并查看热点代码 + +**dotnet-counters 监控内存:** + +```bash +# 安装工具 +dotnet tool install --global dotnet-counters + +# 监控运行中的应用 +dotnet-counters monitor --process-id --counters System.Runtime +``` + +--- + +### Optimization Checklist + +**开发时检查清单:** + +- [ ] 反射操作已缓存(PropertyInfo、Attribute) +- [ ] 耗时操作(> 50ms)在后台线程执行 +- [ ] 事件订阅在控件销毁时取消 +- [ ] ObservableCollection 复用而非重建 +- [ ] 批量更新集合,避免频繁触发 CollectionChanged +- [ ] 字符串拼接使用 StringBuilder +- [ ] XAML 绑定避免复杂转换器 +- [ ] 主题切换批量替换 ResourceDictionary + +**发布前性能测试:** + +- [ ] PropertyGrid 50 属性生成 < 200ms +- [ ] 主题切换 < 100ms +- [ ] 应用启动 < 2s +- [ ] 空闲时内存 < 100MB +- [ ] 无明显的 UI 卡顿(帧率 > 50fps) + +--- + \ No newline at end of file diff --git a/docs/architecture/tech-stack.md b/docs/architecture/tech-stack.md new file mode 100644 index 0000000..62f4bfd --- /dev/null +++ b/docs/architecture/tech-stack.md @@ -0,0 +1,64 @@ +# 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`: + +```xml + + + true + + + + + + + + + +``` + +各项目的 `.csproj` 文件中引用包时**不指定版本号**: + +```xml + + + + +``` + +**优势**: +- 所有项目使用相同的包版本 +- 升级版本时只需修改一处 +- 避免版本冲突导致的编译或运行时错误 + +--- + \ No newline at end of file diff --git a/docs/architecture/testing-strategy.md b/docs/architecture/testing-strategy.md new file mode 100644 index 0000000..9d23049 --- /dev/null +++ b/docs/architecture/testing-strategy.md @@ -0,0 +1,330 @@ +# 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 的反射逻辑** + +```csharp +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** + +```csharp +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 步骤流转** + +```csharp +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 + +**创建测试辅助类减少重复代码:** + +```csharp +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 + +```bash +# 运行所有测试 +dotnet test + +# 运行特定测试类 +dotnet test --filter "FullyQualifiedName~PropertyGridTests" + +# 运行特定测试方法 +dotnet test --filter "Name~RefreshProperties_WithValidObject" + +# 生成代码覆盖率报告 +dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=html +``` + +**Visual Studio 测试运行器:** +1. 打开 Test Explorer(测试 → 测试资源管理器) +2. 点击"运行所有测试"或右键运行特定测试 +3. 查看测试结果和失败详情 + +--- + \ No newline at end of file diff --git a/docs/architecture/unified-project-structure.md b/docs/architecture/unified-project-structure.md new file mode 100644 index 0000000..341060b --- /dev/null +++ b/docs/architecture/unified-project-structure.md @@ -0,0 +1,135 @@ +# Unified Project Structure + +基于 Monorepo 策略和 .NET 项目组织最佳实践,以下是详细的项目文件结构: + +```plaintext +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` | + +--- + \ No newline at end of file diff --git a/src/Example/Program.cs b/src/Example/Program.cs index cb8025d..02ad7f6 100644 --- a/src/Example/Program.cs +++ b/src/Example/Program.cs @@ -1,22 +1,21 @@ -using System; +using System; using Avalonia; -namespace Example -{ - internal class Program - { - // Initialization code. Don't use any Avalonia, third-party APIs or any - // SynchronizationContext-reliant code before AppMain is called: things aren't initialized - // yet and stuff might break. - [STAThread] - public static void Main(string[] args) => BuildAvaloniaApp() - .StartWithClassicDesktopLifetime(args); +namespace Example; - // Avalonia configuration, don't remove; also used by visual designer. - public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .UsePlatformDetect() - .WithInterFont() - .LogToTrace(); - } +internal class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); } diff --git a/src/Penguin.AvaloniaUI/Penguin.AvaloniaUI.csproj b/src/Penguin.AvaloniaUI/Penguin.AvaloniaUI.csproj index 125f4c9..f82a033 100644 --- a/src/Penguin.AvaloniaUI/Penguin.AvaloniaUI.csproj +++ b/src/Penguin.AvaloniaUI/Penguin.AvaloniaUI.csproj @@ -6,4 +6,9 @@ enable + + + + +