Files
ui/docs/architecture.md
2025-10-16 15:08:42 +08:00

70 KiB
Raw Blame History

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

开发平台

  • IDEVisual 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 GeneratorPost-MVP
│   ├── Example/                         # 示例应用
│   └── Penguin.AvaloniaUI.Tests/        # 单元测试
├── docs/                                # 文档
│   ├── prd.md
│   ├── architecture.md
│   └── ...
└── .bmad-core/                          # BMAD 框架配置

核心控件库内部结构(命名空间组织):

  • Penguin.AvaloniaUI.Controls - 业务场景控件PropertyGrid、UserGuide 等)
  • Penguin.AvaloniaUI.Layouts - 布局控件TwoColumnLayout 等)
  • Penguin.AvaloniaUI.Themes - 主题和样式系统
  • Penguin.AvaloniaUI.Utils - 工具类ThemeManager 等)

Rationale

  • Monorepo 简化控件库与示例应用的协同开发
  • 依赖版本统一管理,避免版本冲突
  • MVP 阶段项目数量少3-4 个),复杂度可控

High Level Architecture Diagram

graph TB
    subgraph "开发者应用"
        App[Avalonia Application]
    end

    subgraph "Penguin.AvaloniaUI 控件库"
        Controls[Controls Layer<br/>PropertyGrid, UserGuide]
        Layouts[Layouts Layer<br/>TwoColumnLayout, Overlay]
        Themes[Themes Layer<br/>ThemeManager, ColorSystem]
        Utils[Utils Layer<br/>LocalizationManager, Helpers]
    end

    subgraph "基础框架"
        Avalonia[Avalonia UI 11.3.7]
        ReactiveUI[ReactiveUI 11.3.0]
        NET[.NET 9.0]
    end

    App --> Controls
    App --> Layouts
    App --> Themes

    Controls --> Layouts
    Controls --> Themes
    Controls --> Utils

    Layouts --> Themes

    Themes --> Avalonia
    Controls --> ReactiveUI
    Avalonia --> NET
    ReactiveUI --> NET

    style Controls fill:#4A90E2
    style Layouts fill:#7ED321
    style Themes fill:#F5A623
    style Utils fill:#BD10E0

Architectural Patterns

  • MVVM (Model-View-ViewModel):所有控件遵循 MVVM 模式,使用 ReactiveUI 实现数据绑定和 Command

    • RationaleAvalonia 原生支持 MVVMReactiveUI 提供强大的响应式能力,降低复杂交互的实现难度
  • Templated Control Pattern:业务控件继承自 TemplatedControl,通过 ControlTemplate 分离逻辑和外观

    • Rationale:确保控件可定制,开发者可以通过 Style 和 Template 覆盖默认外观
  • Resource Dictionary 主题系统:主题通过 ResourceDictionary 定义,运行时通过替换 Application.Current.Resources 实现切换

    • RationaleAvalonia 标准机制,无需引入额外框架,主题切换自动传播到所有控件
  • Reactive Extensions (Rx):使用 ReactiveUI 的 Reactive 模式处理异步事件流和属性变化

    • Rationale:简化复杂的事件处理逻辑(如 PropertyGrid 的属性变化监听、UserGuide 的步骤流转)
  • Dependency Injection (可选)MVP 阶段不强制 DIThemeManager 等服务可通过静态类或单例访问

    • Rationale:控件库不需要复杂的 DI 容器保持简单Post-MVP 可根据需要引入
  • Attribute-Driven ConfigurationPropertyGrid 通过 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/CDPost-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 性能测量 验证 NFR60fps、100ms 主题切换、200ms PropertyGrid 生成)

统一包版本管理配置

为了避免多项目间的包版本冲突,项目使用 Central Package Management

需要在项目根目录创建 Directory.Packages.props

<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>

  <ItemGroup>
    <PackageVersion Include="Avalonia" Version="11.3.7" />
    <PackageVersion Include="ReactiveUI.Avalonia" Version="11.3.0" />
    <PackageVersion Include="xUnit" Version="2.6.6" />
    <PackageVersion Include="xUnit.runner.visualstudio" Version="2.5.6" />
  </ItemGroup>
</Project>

各项目的 .csproj 文件中引用包时不指定版本号

<ItemGroup>
  <PackageReference Include="Avalonia" />
  <PackageReference Include="ReactiveUI.Avalonia" />
</ItemGroup>

优势

  • 所有项目使用相同的包版本
  • 升级版本时只需修改一处
  • 避免版本冲突导致的编译或运行时错误

Data Models

核心数据模型定义了控件库中业务逻辑层的数据结构。这些模型在 UI 控件层和业务逻辑层之间共享。


PropertyItem

用途:表示 PropertyGrid 中的单个属性项,封装属性的元数据和值。

关键属性

  • Namestring- 属性名称,显示在左列标签
  • Valueobject- 属性当前值,支持双向绑定
  • PropertyTypeType- 属性的 .NET 类型,用于选择合适的编辑器
  • IsReadOnlybool- 是否只读,只读属性禁用编辑器
  • Categorystring?- 分组类别,用于属性分组显示
  • Descriptionstring?- 属性描述,可选的辅助说明
  • DisplayNamestring?- 显示名称,如果为空则使用 Name

C# 类定义

namespace Penguin.AvaloniaUI.Controls.PropertyGrid
{
    /// <summary>
    /// 表示 PropertyGrid 中的单个属性项
    /// </summary>
    public class PropertyItem : ReactiveObject
    {
        private object? _value;

        /// <summary>
        /// 属性名称
        /// </summary>
        public string Name { get; set; } = string.Empty;

        /// <summary>
        /// 属性显示名称(如果为空则使用 Name
        /// </summary>
        public string? DisplayName { get; set; }

        /// <summary>
        /// 属性值(支持双向绑定)
        /// </summary>
        public object? Value
        {
            get => _value;
            set => this.RaiseAndSetIfChanged(ref _value, value);
        }

        /// <summary>
        /// 属性的 .NET 类型
        /// </summary>
        public Type PropertyType { get; set; } = typeof(object);

        /// <summary>
        /// 是否只读
        /// </summary>
        public bool IsReadOnly { get; set; }

        /// <summary>
        /// 分组类别(可选)
        /// </summary>
        public string? Category { get; set; }

        /// <summary>
        /// 属性描述(可选)
        /// </summary>
        public string? Description { get; set; }

        /// <summary>
        /// 原始 PropertyInfo用于反射场景
        /// </summary>
        public PropertyInfo? PropertyInfo { get; set; }
    }
}

关系

  • PropertyGrid 包含多个 PropertyItemObservableCollection<PropertyItem>
  • PropertyItem 通过 PropertyType 决定使用哪个编辑器控件

GuideStep

用途:表示 UserGuide 引导流程中的单个步骤。

关键属性

  • TargetControlControl?- 引导目标控件Overlay 将聚焦此控件
  • Titlestring- 步骤标题,显示在引导提示框顶部
  • Contentstring- 步骤内容/提示文本,支持基础富文本
  • PositionTooltipPosition- 提示框相对于目标控件的位置
  • Orderint- 步骤顺序,用于排序

C# 类定义

namespace Penguin.AvaloniaUI.Controls.UserGuide
{
    /// <summary>
    /// 提示框位置枚举
    /// </summary>
    public enum TooltipPosition
    {
        Bottom,
        Top,
        Left,
        Right
    }

    /// <summary>
    /// 表示 UserGuide 中的单个引导步骤
    /// </summary>
    public class GuideStep : ReactiveObject
    {
        /// <summary>
        /// 引导目标控件(可选,如果为空则显示全屏提示)
        /// </summary>
        public Control? TargetControl { get; set; }

        /// <summary>
        /// 步骤标题
        /// </summary>
        public string Title { get; set; } = string.Empty;

        /// <summary>
        /// 步骤内容(支持基础富文本)
        /// </summary>
        public string Content { get; set; } = string.Empty;

        /// <summary>
        /// 提示框位置
        /// </summary>
        public TooltipPosition Position { get; set; } = TooltipPosition.Bottom;

        /// <summary>
        /// 步骤顺序(用于排序)
        /// </summary>
        public int Order { get; set; }

        /// <summary>
        /// 是否允许跳过此步骤
        /// </summary>
        public bool Skippable { get; set; } = true;
    }
}

关系

  • UserGuide 包含多个 GuideStepObservableCollection<GuideStep>
  • GuideStep 通过 TargetControl 关联到具体的 UI 控件
  • Overlay 根据 TargetControl 的位置和大小进行挖空显示

ThemeInfo辅助模型

用途:表示主题信息,用于 ThemeManager 管理主题切换。

关键属性

  • ThemeTypeThemeType- 主题类型枚举Light、Dark
  • ResourceUriUri- 主题资源字典的 URI

C# 类定义

namespace Penguin.AvaloniaUI.Themes
{
    /// <summary>
    /// 主题类型枚举
    /// </summary>
    public enum ThemeType
    {
        Light,
        Dark
    }

    /// <summary>
    /// 主题信息
    /// </summary>
    public class ThemeInfo
    {
        /// <summary>
        /// 主题类型
        /// </summary>
        public ThemeType Type { get; set; }

        /// <summary>
        /// 主题资源字典 URI
        /// </summary>
        public Uri ResourceUri { get; set; } = null!;

        /// <summary>
        /// 主题显示名称
        /// </summary>
        public string DisplayName { get; set; } = string.Empty;
    }
}

关系

  • ThemeManager 使用 ThemeInfo 管理可用主题
  • 主题切换时通过 ResourceUri 加载对应的 ResourceDictionary

Components

基于架构模式和数据模型,系统划分为以下核心组件。每个组件负责特定功能,并通过清晰的接口与其他组件交互。


PropertyGrid

职责:自动生成属性编辑 UI支持多种属性类型、分组、只读属性和基础验证。

关键接口

  • SelectedObject(依赖属性)- 绑定的数据对象,变化时自动刷新属性列表
  • PropertiesObservableCollection- 解析后的属性列表
  • PropertyValueChanged(事件)- 属性值变化时触发

依赖关系

  • 依赖 TwoColumnLayout 进行布局
  • 依赖 PropertyEditorsTextBox、NumericUpDown、ComboBox 等)
  • 依赖 ThemeManager 获取当前主题颜色
  • 使用反射System.Reflection解析对象属性

技术细节

  • 继承自 TemplatedControl
  • 使用 ReactiveUI 的 WhenAnyValue 监听 SelectedObject 变化
  • 通过 PropertyInfo.GetCustomAttributes<T>() 读取 Attribute[Category][Browsable]
  • 编辑器选择逻辑:根据 PropertyItem.PropertyType 映射到对应控件

实现示例(核心逻辑):

public class PropertyGrid : TemplatedControl
{
    public static readonly StyledProperty<object?> SelectedObjectProperty =
        AvaloniaProperty.Register<PropertyGrid, object?>(nameof(SelectedObject));

    public object? SelectedObject
    {
        get => GetValue(SelectedObjectProperty);
        set => SetValue(SelectedObjectProperty, value);
    }

    public ObservableCollection<PropertyItem> Properties { get; } = new();

    protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
    {
        base.OnApplyTemplate(e);

        this.GetObservable(SelectedObjectProperty)
            .Subscribe(obj => RefreshProperties(obj));
    }

    private void RefreshProperties(object? obj)
    {
        Properties.Clear();
        if (obj == null) return;

        var properties = obj.GetType()
            .GetProperties(BindingFlags.Public | BindingFlags.Instance)
            .Where(p => p.CanRead)
            .Select(p => CreatePropertyItem(obj, p))
            .OrderBy(p => p.Category)
            .ThenBy(p => p.Order);

        foreach (var prop in properties)
        {
            Properties.Add(prop);
        }
    }
}

TwoColumnLayout

职责:提供"标签-编辑器"配对的布局控件,左列固定或自适应宽度,右列占据剩余空间。

关键接口

  • ItemsObservableCollection- 行集合
  • LabelWidthdouble- 左列宽度(默认 Auto
  • RowSpacingdouble- 行间距(默认 8px

依赖关系

  • 无外部依赖(纯布局控件)
  • 被 PropertyGrid 使用

技术细节

  • 继承自 Panel 或内部使用 Grid
  • 重写 MeasureOverrideArrangeOverride 实现自定义布局
  • 响应主题系统(标签文本颜色使用 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 和引导提示框的显示。

关键接口

  • StepsObservableCollection- 引导步骤集合
  • CurrentStepIndexint- 当前步骤索引
  • NextStepCommandICommand- 下一步命令
  • PreviousStepCommandICommand- 上一步命令
  • SkipCommandICommand- 跳过命令
  • Completed(事件)- 引导完成事件

依赖关系

  • 依赖 Overlay 控件显示遮罩
  • 依赖 RichTooltip 显示引导提示框
  • 依赖 GuideStep 数据模型

技术细节

  • 继承自 ContentControl
  • 使用 ReactiveUI 的 ReactiveCommand 实现步骤流转
  • 步骤切换时触发动画淡入淡出200ms

核心逻辑

public class UserGuide : ContentControl
{
    public ObservableCollection<GuideStep> Steps { get; } = new();
    private int _currentStepIndex;

    public ReactiveCommand<Unit, Unit> NextStepCommand { get; }
    public ReactiveCommand<Unit, Unit> PreviousStepCommand { get; }
    public ReactiveCommand<Unit, Unit> SkipCommand { get; }

    public UserGuide()
    {
        NextStepCommand = ReactiveCommand.Create(() =>
        {
            if (_currentStepIndex < Steps.Count - 1)
            {
                _currentStepIndex++;
                ShowCurrentStep();
            }
            else
            {
                Complete();
            }
        });

        // ... 其他 Command 实现
    }
}

Overlay

职责:在窗口上层显示半透明遮罩,可选地挖空特定控件区域以聚焦目标。

关键接口

  • IsVisiblebool- 显示/隐藏遮罩
  • TargetControlControl?- 挖空目标控件
  • BackgroundOpacitydouble- 不透明度(默认 0.5
  • BackgroundColorColor- 遮罩颜色

依赖关系

  • 被 UserGuide 使用
  • 依赖 ThemeManager暗色模式下调整遮罩颜色

技术细节

  • 继承自 ContentControlPanel
  • 使用绝对定位覆盖整个窗口
  • 挖空逻辑:通过 Clip 或多个 Rectangle 实现

视觉效果

┌─────────────────────────────────┐
│████████████████████████████████│ ← 半透明遮罩
│████████┌──────────┐████████████│
│████████│ Target   │████████████│ ← 挖空区域清晰可见
│████████└──────────┘████████████│
│████████████████████████████████│
└─────────────────────────────────┘

RichTooltip

职责:显示支持基础富文本的增强 Tooltip。

关键接口

  • Contentstring- 提示内容,支持简单 Markdown 语法(**粗体***斜体*\n 换行)
  • MaxWidthdouble- 最大宽度(默认 300px

依赖关系

  • 扩展 Avalonia 的 ToolTip
  • 被 UserGuide 使用

技术细节

  • 内部使用 TextBlock 配合 InlinesRun、Bold、Italic
  • 简单的 Markdown 解析器(不依赖第三方库)
  • 响应主题系统(背景色 Surface,文本色 TextPrimary

解析示例

输入:"**注意**: 这是一个重要提示。\n请仔细阅读。"
输出:
  <TextBlock>
    <Bold>注意</Bold>: 这是一个重要提示。
    <LineBreak />
    请仔细阅读。
  </TextBlock>

ThemeManager

职责:管理主题切换,提供全局主题访问接口。

关键接口

  • ApplyTheme(ThemeType type)(方法)- 切换主题
  • CurrentThemeThemeType- 当前主题类型
  • ThemeChanged(事件)- 主题变化事件

依赖关系

  • 被所有控件使用(通过静态方法或单例)
  • 依赖 Avalonia 的 Application.Current.Resources

技术细节

  • 静态类或单例模式
  • 主题切换通过替换 ResourceDictionary 实现
  • 支持动画过渡可选100ms 淡入淡出)

实现示例

public static class ThemeManager
{
    private static ThemeType _currentTheme = ThemeType.Light;
    public static event EventHandler<ThemeType>? ThemeChanged;

    public static void ApplyTheme(ThemeType type)
    {
        if (_currentTheme == type) return;

        var app = Application.Current;
        if (app == null) return;

        // 移除旧主题
        var oldTheme = app.Resources.MergedDictionaries
            .FirstOrDefault(d => d.Source?.ToString().Contains("Theme") == true);
        if (oldTheme != null)
            app.Resources.MergedDictionaries.Remove(oldTheme);

        // 加载新主题
        var uri = type == ThemeType.Light
            ? new Uri("avares://Penguin.AvaloniaUI/Themes/LightTheme.axaml")
            : new Uri("avares://Penguin.AvaloniaUI/Themes/DarkTheme.axaml");

        var newTheme = new ResourceDictionary { Source = uri };
        app.Resources.MergedDictionaries.Add(newTheme);

        _currentTheme = type;
        ThemeChanged?.Invoke(null, type);
    }
}

Component Diagrams

graph TB
    subgraph "业务控件层"
        PropertyGrid[PropertyGrid]
        UserGuide[UserGuide]
    end

    subgraph "基础控件层"
        TwoColumnLayout[TwoColumnLayout]
        Overlay[Overlay]
        RichTooltip[RichTooltip]
        Editors[PropertyEditors]
    end

    subgraph "工具层"
        ThemeManager[ThemeManager]
    end

    subgraph "数据模型"
        PropertyItem[PropertyItem]
        GuideStep[GuideStep]
    end

    PropertyGrid --> TwoColumnLayout
    PropertyGrid --> Editors
    PropertyGrid --> PropertyItem
    PropertyGrid --> ThemeManager

    UserGuide --> Overlay
    UserGuide --> RichTooltip
    UserGuide --> GuideStep
    UserGuide --> ThemeManager

    TwoColumnLayout --> ThemeManager
    Overlay --> ThemeManager
    RichTooltip --> ThemeManager
    Editors --> ThemeManager

    style PropertyGrid fill:#4A90E2
    style UserGuide fill:#4A90E2
    style TwoColumnLayout fill:#7ED321
    style Overlay fill:#7ED321
    style RichTooltip fill:#7ED321
    style ThemeManager fill:#F5A623

Unified Project Structure

基于 Monorepo 策略和 .NET 项目组织最佳实践,以下是详细的项目文件结构:

D:\32_avalonia.ui/
├── .github/                              # GitHub 工作流Post-MVP
│   └── workflows/
│       ├── ci.yml                        # 持续集成
│       └── release.yml                   # 发布流程
│
├── src/                                  # 源代码目录
│   ├── Penguin.AvaloniaUI/              # 核心控件库项目
│   │   ├── Controls/                     # 业务场景控件
│   │   │   ├── PropertyGrid/
│   │   │   │   ├── PropertyGrid.cs       # 主控件类
│   │   │   │   ├── PropertyItem.cs       # 数据模型
│   │   │   │   ├── PropertyEditorFactory.cs  # 编辑器工厂
│   │   │   │   └── Themes/
│   │   │   │       ├── PropertyGrid.axaml     # 默认模板
│   │   │   │       └── PropertyGrid.axaml.cs
│   │   │   ├── UserGuide/
│   │   │   │   ├── UserGuide.cs
│   │   │   │   ├── GuideStep.cs
│   │   │   │   ├── Overlay.cs
│   │   │   │   ├── RichTooltip.cs
│   │   │   │   └── Themes/
│   │   │   │       ├── UserGuide.axaml
│   │   │   │       ├── Overlay.axaml
│   │   │   │       └── RichTooltip.axaml
│   │   │   └── ... (其他控件)
│   │   │
│   │   ├── Layouts/                      # 布局控件
│   │   │   ├── TwoColumnLayout.cs
│   │   │   └── Themes/
│   │   │       └── TwoColumnLayout.axaml
│   │   │
│   │   ├── Themes/                       # 主题系统
│   │   │   ├── ThemeManager.cs           # 主题管理器
│   │   │   ├── ThemeInfo.cs              # 主题信息模型
│   │   │   ├── ColorSystem/              # 颜色系统定义
│   │   │   │   ├── LightColors.axaml     # 浅色主题颜色
│   │   │   │   └── DarkColors.axaml      # 暗色主题颜色
│   │   │   ├── LightTheme.axaml          # 浅色主题资源
│   │   │   └── DarkTheme.axaml           # 暗色主题资源
│   │   │
│   │   ├── Utils/                        # 工具类
│   │   │   ├── Converters/               # 值转换器
│   │   │   │   ├── BoolToVisibilityConverter.cs
│   │   │   │   └── TypeToEditorConverter.cs
│   │   │   ├── Helpers/                  # 辅助类
│   │   │   │   ├── ReflectionHelper.cs   # 反射工具
│   │   │   │   └── ValidationHelper.cs   # 验证工具
│   │   │   └── Extensions/               # 扩展方法
│   │   │       └── ObservableExtensions.cs
│   │   │
│   │   ├── Assets/                       # 资源文件
│   │   │   └── Icons/                    # 图标资源
│   │   │
│   │   ├── Penguin.AvaloniaUI.csproj     # 项目文件
│   │   └── AssemblyInfo.cs               # 程序集信息
│   │
│   ├── Penguin.AvaloniaUI.SourceGenerators/  # Source GeneratorPost-MVP
│   │   ├── PropertyGridGenerator.cs
│   │   └── Penguin.AvaloniaUI.SourceGenerators.csproj
│   │
│   ├── Example/                          # 示例应用项目
│   │   ├── App.axaml                     # 应用程序资源
│   │   ├── App.axaml.cs
│   │   ├── ViewModels/                   # 视图模型
│   │   │   ├── MainWindowViewModel.cs
│   │   │   ├── PropertyGridDemoViewModel.cs
│   │   │   └── UserGuideDemoViewModel.cs
│   │   ├── Views/                        # 视图/页面
│   │   │   ├── MainWindow.axaml
│   │   │   ├── MainWindow.axaml.cs
│   │   │   ├── Pages/
│   │   │   │   ├── ColorSystemPage.axaml       # 颜色系统演示
│   │   │   │   ├── PropertyGridPage.axaml     # PropertyGrid 演示
│   │   │   │   ├── UserGuidePage.axaml        # UserGuide 演示
│   │   │   │   └── ComprehensiveDemoPage.axaml  # 综合演示
│   │   │   └── ...
│   │   ├── Models/                       # 测试数据模型
│   │   │   ├── DemoSettings.cs           # PropertyGrid 测试类
│   │   │   └── LargeSettings.cs          # 50 属性测试类
│   │   ├── Assets/                       # 示例应用资源
│   │   ├── Example.csproj
│   │   └── Program.cs
│   │
│   └── Penguin.AvaloniaUI.Tests/        # 单元测试项目
│       ├── Controls/
│       │   ├── PropertyGridTests.cs
│       │   ├── UserGuideTests.cs
│       │   └── ...
│       ├── Layouts/
│       │   └── TwoColumnLayoutTests.cs
│       ├── Themes/
│       │   └── ThemeManagerTests.cs
│       ├── Utils/
│       │   └── ReflectionHelperTests.cs
│       └── Penguin.AvaloniaUI.Tests.csproj
│
├── docs/                                 # 项目文档
│   ├── prd.md                            # 产品需求文档
│   ├── architecture.md                   # 架构文档(本文档)
│   ├── brief.md                          # 项目简介
│   └── brainstorm.md                     # 头脑风暴
│
├── .bmad-core/                          # BMAD 框架配置
│   ├── core-config.yaml
│   ├── agents/
│   ├── tasks/
│   └── templates/
│
├── .editorconfig                        # 编辑器配置
├── .gitignore                           # Git 忽略规则
├── Directory.Packages.props             # 统一包版本管理
├── README.md                            # 项目说明
├── LICENSE                              # 许可证Post-MVP
└── Penguin.AvaloniaUI.sln              # 解决方案文件

关键目录说明:

目录 用途 命名空间
Controls/ 业务场景控件实现 Penguin.AvaloniaUI.Controls.*
Layouts/ 布局控件实现 Penguin.AvaloniaUI.Layouts
Themes/ 主题和样式系统 Penguin.AvaloniaUI.Themes
Utils/ 工具类和辅助方法 Penguin.AvaloniaUI.Utils.*
Example/Views/Pages/ 示例应用页面 Example.Views.Pages
Example/ViewModels/ 示例应用 ViewModel Example.ViewModels

Development Workflow

本节定义控件库的开发环境配置和日常开发流程,确保开发者能够快速上手并保持高效开发。


Local Development Setup

前置要求:

# 必需
- .NET 9.0 SDK (https://dotnet.microsoft.com/download)
- Visual Studio 2022 (17.8+)
- Git 2.40+

# 推荐
- Visual Studio Avalonia 扩展 (用于 XAML 预览)

初始化项目:

# 克隆仓库
git clone <repository-url>
cd 32_avalonia.ui

# 还原 NuGet 包
dotnet restore

# 构建解决方案
dotnet build

# 运行示例应用
dotnet run --project src/Example/Example.csproj

Visual Studio 配置:

  1. 打开 Penguin.AvaloniaUI.sln
  2. 设置 Example 为启动项目
  3. 选择调试配置Debug/Release
  4. 按 F5 启动调试

Development Commands

常用命令:

# 启动示例应用(开发模式,支持热重载)
dotnet watch --project src/Example/Example.csproj

# 运行所有单元测试
dotnet test

# 运行特定测试类
dotnet test --filter "FullyQualifiedName~PropertyGridTests"

# 生成代码覆盖率报告
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=html

# 清理构建产物
dotnet clean

# 格式化代码(基于 .editorconfig
dotnet format

# 打包 NuGet 包Post-MVP
dotnet pack src/Penguin.AvaloniaUI/Penguin.AvaloniaUI.csproj -c Release

Hot Reload 配置

Avalonia 11.x 支持 .NET Hot Reload但有限制

支持的修改:

  • C# 方法体内的代码修改
  • XAML 资源字典的修改(颜色、样式)
  • 部分 XAML 控件属性修改

不支持的修改(需要重启):

  • 新增或删除控件
  • 修改控件模板结构
  • 修改依赖属性定义
  • 修改 ViewModel 属性签名

启用 Hot Reload

Example.csproj 中确保已启用:

<PropertyGroup>
  <AvaloniaUseHotReload>true</AvaloniaUseHotReload>
</PropertyGroup>

Environment Configuration

控件库无需环境变量MVP 阶段),但示例应用可能需要:

# Example/.env (可选,用于测试外部 API 集成)
# MVP 阶段不需要

调试配置launchSettings.json

{
  "profiles": {
    "Example": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "environmentVariables": {
        "DOTNET_ENVIRONMENT": "Development"
      }
    }
  }
}

Git Workflow

Commit Message 格式Conventional Commits

<type>: <description>

[optional body]

Type 类型:

  • feat: 新功能(如 feat: 增加了UserControl的DarkStyle
  • fix: Bug 修复(如 fix: 修复PropertyGrid在主题切换时崩溃
  • refactor: 重构(如 refactor: 优化ThemeManager的资源加载逻辑
  • test: 测试相关(如 test: 添加PropertyGrid的分组功能测试
  • docs: 文档更新(如 docs: 更新README的快速开始指南
  • style: 代码格式调整(如 style: 统一缩进为4空格
  • chore: 构建/工具相关(如 chore: 升级Avalonia到11.3.8

示例:

git commit -m "feat: 实现PropertyGrid的属性分组功能"
git commit -m "fix: 修复TwoColumnLayout在窗口缩放时的对齐问题"
git commit -m "test: 添加ThemeManager的主题切换测试"

分支命名规范:

  • feat/<feature-name> - 新功能分支(如 feat/usercontrol
  • fix/<issue-number> - Bug 修复分支(如 fix/1123
  • refactor/<description> - 重构分支(如 refactor/theme-system
  • test/<description> - 测试分支(如 test/integration

日常开发流程:

  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. 提交代码
    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 文档注释: 所有 publicprotected 成员必须有 XML 注释(///),包括 <summary><param><returns>。示例:

    /// <summary>
    /// 应用指定的主题类型
    /// </summary>
    /// <param name="type">主题类型Light 或 Dark</param>
    public static void ApplyTheme(ThemeType type)
    
  • 禁止反射动态创建控件: PropertyGrid 可以使用反射解析属性,但不得使用 Activator.CreateInstance 动态创建编辑器控件。使用工厂模式或字典映射。

  • 主题资源命名约定: 语义化颜色必须使用统一前缀:TextPrimaryTextSecondaryBackgroundPrimarySurfaceElevated。不得使用 Color1MyBlue 等非语义化命名。


Naming Conventions

元素类型 命名规则 示例
控件类 PascalCase功能名称 + 控件类型后缀(可选) PropertyGrid, TwoColumnLayout
依赖属性 PascalCase属性名 + Property 后缀 SelectedObjectProperty
私有字段 camelCase下划线前缀 _currentTheme
事件 PascalCase动词过去式 ThemeChanged, PropertyValueChanged
方法 PascalCase动词开头 ApplyTheme(), RefreshProperties()
XAML 文件 PascalCase与类名一致 PropertyGrid.axaml
测试方法 PascalCaseMethodName_Scenario_ExpectedResult ApplyTheme_WithDarkTheme_ShouldUpdateResources()

File Organization Rules

  • 一个文件一个类:每个控件类单独一个 .cs 文件,不得在同一文件中定义多个公开类(嵌套私有类除外)。

  • XAML 与代码分离:控件的 XAML 模板放在 Themes/ 子目录下,与类定义分离:

    Controls/PropertyGrid/PropertyGrid.cs
    Controls/PropertyGrid/Themes/PropertyGrid.axaml
    
  • 数据模型独立文件:数据模型类(如 PropertyItem, GuideStep)与控件类分离,放在同级目录下。

  • 文件格式规范:命名空间单独放在文件顶部,减少缩进层级:

    namespace Penguin.AvaloniaUI.Controls;
    
    public class PropertyGrid : TemplatedControl
    {
        // 类实现
    }
    

Code Style (基于 .editorconfig)

项目使用 .editorconfig 强制执行以下规则:

root = true

[*.cs]
indent_style = space
indent_size = 4
end_of_line = crlf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

# C# 规则
csharp_new_line_before_open_brace = all
csharp_prefer_braces = true:warning
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion

[*.axaml]
indent_size = 2

代码风格要点:

  • 使用 var 声明局部变量(如 var count = 100;
  • 命名空间单独一行,不使用文件范围命名空间的花括号嵌套

运行格式化检查:

dotnet format --verify-no-changes

Testing Strategy

本节定义控件库的测试方法和组织策略,确保核心功能的稳定性和可靠性。


Testing Pyramid

       E2E Tests (手动测试)
      /                    \
     Integration Tests (可选)
    /                          \
   Unit Tests (核心逻辑)

测试重点分配:

  • Unit Tests (70%):核心业务逻辑、数据模型、工具类
  • Integration Tests (可选):控件与 ReactiveUI 的集成、主题系统
  • Manual Tests (必须)UI 渲染、主题切换、用户交互

MVP 阶段不包括:

  • UI 自动化测试Avalonia 提供 Headless 测试包,但 MVP 阶段依赖手动测试)
  • 性能基准测试(除非发现明显性能问题)
  • 跨平台兼容性测试(仅在 Windows 上测试)

Test Organization

测试项目结构:

Penguin.AvaloniaUI.Tests/
├── Controls/
│   ├── PropertyGridTests.cs
│   ├── UserGuideTests.cs
│   └── ...
├── Layouts/
│   └── TwoColumnLayoutTests.cs
├── Themes/
│   └── ThemeManagerTests.cs
├── Utils/
│   ├── ReflectionHelperTests.cs
│   └── Converters/
│       └── TypeToEditorConverterTests.cs
└── TestHelpers/
    ├── MockObjects.cs
    └── TestBase.cs

命名约定:

  • 测试类:{ClassName}Tests
  • 测试方法:{MethodName}_{Scenario}_{ExpectedResult}

Unit Test Examples

示例 1测试 PropertyGrid 的反射逻辑

namespace Penguin.AvaloniaUI.Tests.Controls;

public class PropertyGridTests
{
    [Fact]
    public void RefreshProperties_WithValidObject_ShouldParseAllPublicProperties()
    {
        // Arrange
        var testObject = new TestSettings
        {
            Name = "Test",
            Age = 25,
            IsEnabled = true
        };
        var propertyGrid = new PropertyGrid
        {
            SelectedObject = testObject
        };

        // Act
        var properties = propertyGrid.Properties;

        // Assert
        Assert.Equal(3, properties.Count);
        Assert.Contains(properties, p => p.Name == "Name" && p.PropertyType == typeof(string));
        Assert.Contains(properties, p => p.Name == "Age" && p.PropertyType == typeof(int));
        Assert.Contains(properties, p => p.Name == "IsEnabled" && p.PropertyType == typeof(bool));
    }

    [Fact]
    public void RefreshProperties_WithCategoryAttribute_ShouldGroupByCategory()
    {
        // Arrange
        var testObject = new CategorizedSettings();
        var propertyGrid = new PropertyGrid
        {
            SelectedObject = testObject
        };

        // Act
        var properties = propertyGrid.Properties;
        var generalCategory = properties.Where(p => p.Category == "General").ToList();
        var advancedCategory = properties.Where(p => p.Category == "Advanced").ToList();

        // Assert
        Assert.NotEmpty(generalCategory);
        Assert.NotEmpty(advancedCategory);
    }

    [Fact]
    public void RefreshProperties_WithNullObject_ShouldClearProperties()
    {
        // Arrange
        var propertyGrid = new PropertyGrid
        {
            SelectedObject = new TestSettings()
        };

        // Act
        propertyGrid.SelectedObject = null;

        // Assert
        Assert.Empty(propertyGrid.Properties);
    }
}

示例 2测试 ThemeManager

namespace Penguin.AvaloniaUI.Tests.Themes;

public class ThemeManagerTests
{
    [Fact]
    public void ApplyTheme_WithLightTheme_ShouldLoadLightResources()
    {
        // Arrange
        var app = CreateTestApplication();

        // Act
        ThemeManager.ApplyTheme(ThemeType.Light);

        // Assert
        var textPrimary = app.Resources["TextPrimary"] as SolidColorBrush;
        Assert.NotNull(textPrimary);
        // 浅色主题的主要文本应为深色
        Assert.True(textPrimary.Color.R < 128);
    }

    [Fact]
    public void ApplyTheme_SwitchingThemes_ShouldTriggerThemeChangedEvent()
    {
        // Arrange
        var eventTriggered = false;
        ThemeManager.ThemeChanged += (sender, type) => eventTriggered = true;

        // Act
        ThemeManager.ApplyTheme(ThemeType.Dark);

        // Assert
        Assert.True(eventTriggered);
    }

    private Application CreateTestApplication()
    {
        // 创建测试用的 Application 实例
        return new Application();
    }
}

示例 3测试 UserGuide 步骤流转

namespace Penguin.AvaloniaUI.Tests.Controls;

public class UserGuideTests
{
    [Fact]
    public void NextStepCommand_WhenNotLastStep_ShouldIncrementIndex()
    {
        // Arrange
        var userGuide = new UserGuide();
        userGuide.Steps.Add(new GuideStep { Title = "Step 1", Order = 0 });
        userGuide.Steps.Add(new GuideStep { Title = "Step 2", Order = 1 });
        userGuide.Steps.Add(new GuideStep { Title = "Step 3", Order = 2 });

        // Act
        userGuide.NextStepCommand.Execute(null);

        // Assert
        Assert.Equal(1, userGuide.CurrentStepIndex);
    }

    [Fact]
    public void NextStepCommand_WhenLastStep_ShouldTriggerCompleteEvent()
    {
        // Arrange
        var userGuide = new UserGuide();
        userGuide.Steps.Add(new GuideStep { Title = "Step 1", Order = 0 });
        var completedTriggered = false;
        userGuide.Completed += (sender, args) => completedTriggered = true;

        // Act
        userGuide.NextStepCommand.Execute(null);

        // Assert
        Assert.True(completedTriggered);
    }

    [Fact]
    public void PreviousStepCommand_WhenFirstStep_ShouldNotExecute()
    {
        // Arrange
        var userGuide = new UserGuide();
        userGuide.Steps.Add(new GuideStep { Title = "Step 1", Order = 0 });

        // Act
        var canExecute = userGuide.PreviousStepCommand.CanExecute(null);

        // Assert
        Assert.False(canExecute);
    }
}

Manual Testing Checklist

以下场景必须通过示例应用手动测试

PropertyGrid

  • 绑定包含 6 种基础类型的对象,所有编辑器正确显示
  • 修改属性值SelectedObject 对象同步更新
  • 只读属性禁用编辑器或显示为灰色
  • 属性按 Category 分组显示,分组标题清晰
  • 主题切换时PropertyGrid 颜色正确更新
  • 50 个属性的生成时间 < 200ms使用秒表或 Stopwatch 测量)

UserGuide

  • 引导流程正确显示 Overlay 遮罩
  • 目标控件区域挖空显示(清晰可见)
  • 提示框在目标控件附近正确定位Top/Bottom/Left/Right
  • "下一步"/"上一步"按钮功能正常
  • 步骤进度显示正确(如 "2/5"
  • "跳过"和"完成"按钮触发完成事件

主题系统:

  • 主题切换在 100ms 内完成,无闪烁
  • 所有控件的颜色正确响应主题变化
  • 浅色和暗色主题的对比度舒适(文本清晰可读)
  • 主题切换时无控件错位或布局异常

跨功能集成:

  • 在 PropertyGrid 编辑时切换主题,无崩溃
  • 在 UserGuide 进行中修改 PropertyGrid无异常
  • 窗口缩放时,所有控件布局正确调整

Test Helpers

创建测试辅助类减少重复代码:

namespace Penguin.AvaloniaUI.Tests.TestHelpers;

public static class MockObjects
{
    public class TestSettings
    {
        public string Name { get; set; } = "Default";
        public int Age { get; set; } = 0;
        public bool IsEnabled { get; set; } = false;
    }

    public class CategorizedSettings
    {
        [Category("General")]
        public string Name { get; set; } = "Default";

        [Category("Advanced")]
        public int Timeout { get; set; } = 5000;
    }
}

public class TestBase
{
    protected Application CreateTestApplication()
    {
        var app = new Application();
        // 加载测试用主题资源
        return app;
    }
}

Running Tests

# 运行所有测试
dotnet test

# 运行特定测试类
dotnet test --filter "FullyQualifiedName~PropertyGridTests"

# 运行特定测试方法
dotnet test --filter "Name~RefreshProperties_WithValidObject"

# 生成代码覆盖率报告
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=html

Visual Studio 测试运行器:

  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 参数验证:

namespace Penguin.AvaloniaUI.Themes;

public static class ThemeManager
{
    /// <summary>
    /// 应用指定的主题类型
    /// </summary>
    /// <param name="type">主题类型</param>
    /// <exception cref="ArgumentException">当主题类型无效时抛出</exception>
    public static void ApplyTheme(ThemeType type)
    {
        // 参数验证
        if (!Enum.IsDefined(typeof(ThemeType), type))
        {
            throw new ArgumentException(
                $"Invalid theme type: {type}. Valid values are: {string.Join(", ", Enum.GetNames(typeof(ThemeType)))}",
                nameof(type));
        }

        // 核心逻辑
        try
        {
            LoadThemeResources(type);
            _currentTheme = type;
            ThemeChanged?.Invoke(null, type);
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException(
                $"Failed to apply theme '{type}'. Ensure theme resources are available.",
                ex);
        }
    }
}

PropertyGrid 错误处理:

namespace Penguin.AvaloniaUI.Controls.PropertyGrid;

public class PropertyGrid : TemplatedControl
{
    public static readonly StyledProperty<object?> SelectedObjectProperty =
        AvaloniaProperty.Register<PropertyGrid, object?>(nameof(SelectedObject));

    public object? SelectedObject
    {
        get => GetValue(SelectedObjectProperty);
        set => SetValue(SelectedObjectProperty, value);
    }

    private void RefreshProperties(object? obj)
    {
        Properties.Clear();

        if (obj == null)
        {
            // 空对象是合法的,清空属性列表即可
            return;
        }

        try
        {
            var properties = obj.GetType()
                .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                .Where(p => p.CanRead);

            foreach (var prop in properties)
            {
                try
                {
                    var item = CreatePropertyItem(obj, prop);
                    Properties.Add(item);
                }
                catch (Exception ex)
                {
                    // 单个属性解析失败不应影响其他属性
                    Debug.WriteLine($"[PropertyGrid] Failed to create property item for '{prop.Name}': {ex.Message}");

                    // 可选:添加错误占位符
                    Properties.Add(new PropertyItem
                    {
                        Name = prop.Name,
                        DisplayName = $"{prop.Name} (Error)",
                        IsReadOnly = true,
                        Value = $"Error: {ex.Message}"
                    });
                }
            }
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException(
                $"Failed to parse properties for object of type '{obj.GetType().Name}'.",
                ex);
        }
    }

    private PropertyItem CreatePropertyItem(object obj, PropertyInfo prop)
    {
        Debug.Assert(obj != null, "Object should not be null");
        Debug.Assert(prop != null, "PropertyInfo should not be null");

        try
        {
            var value = prop.GetValue(obj);

            return new PropertyItem
            {
                Name = prop.Name,
                DisplayName = GetDisplayName(prop),
                Value = value,
                PropertyType = prop.PropertyType,
                IsReadOnly = !prop.CanWrite,
                Category = GetCategory(prop),
                PropertyInfo = prop
            };
        }
        catch (TargetInvocationException ex)
        {
            throw new InvalidOperationException(
                $"Property '{prop.Name}' getter threw an exception.",
                ex.InnerException ?? ex);
        }
    }
}

UserGuide 错误处理:

namespace Penguin.AvaloniaUI.Controls.UserGuide;

public class UserGuide : ContentControl
{
    private void ShowCurrentStep()
    {
        if (CurrentStepIndex < 0 || CurrentStepIndex >= Steps.Count)
        {
            Debug.WriteLine($"[UserGuide] Invalid step index: {CurrentStepIndex}");
            return;
        }

        var step = Steps[CurrentStepIndex];

        // 验证目标控件是否可用
        if (step.TargetControl == null)
        {
            Debug.WriteLine($"[UserGuide] Step '{step.Title}' has no target control, showing full-screen overlay.");
            // 显示全屏提示
            ShowFullScreenGuide(step);
            return;
        }

        // 检查目标控件是否在可视化树中
        if (!step.TargetControl.IsVisible || !IsControlInVisualTree(step.TargetControl))
        {
            Debug.WriteLine($"[UserGuide] Target control for step '{step.Title}' is not visible or not in visual tree. Skipping step.");

            // 自动跳到下一步
            if (CurrentStepIndex < Steps.Count - 1)
            {
                CurrentStepIndex++;
                ShowCurrentStep();
            }
            else
            {
                Complete();
            }
            return;
        }

        // 正常显示引导
        ShowGuideForControl(step);
    }

    private bool IsControlInVisualTree(Control control)
    {
        try
        {
            return control.GetVisualRoot() != null;
        }
        catch
        {
            return false;
        }
    }
}

Logging Strategy

使用 Debug.WriteLine 进行调试日志:

using System.Diagnostics;

namespace Penguin.AvaloniaUI.Utils;

internal static class Logger
{
    [Conditional("DEBUG")]
    public static void Debug(string message)
    {
        System.Diagnostics.Debug.WriteLine($"[Penguin.AvaloniaUI] {message}");
    }

    [Conditional("DEBUG")]
    public static void Warning(string message)
    {
        System.Diagnostics.Debug.WriteLine($"[WARNING] {message}");
    }

    public static void Error(string message, Exception? ex = null)
    {
        System.Diagnostics.Debug.WriteLine($"[ERROR] {message}");
        if (ex != null)
        {
            System.Diagnostics.Debug.WriteLine($"Exception: {ex}");
        }
    }
}

使用示例:

private void RefreshProperties(object? obj)
{
    Logger.Debug($"RefreshProperties called with object type: {obj?.GetType().Name ?? "null"}");

    try
    {
        // 核心逻辑
    }
    catch (Exception ex)
    {
        Logger.Error("Failed to refresh properties", ex);
        throw;
    }
}

Error Messages Guidelines

好的错误消息示例:

// ❌ 不好的错误消息
throw new Exception("Error");
throw new ArgumentException("Invalid parameter");

// ✅ 好的错误消息
throw new ArgumentNullException(nameof(selectedObject),
    "SelectedObject cannot be null. Please provide a valid object instance.");

throw new InvalidOperationException(
    "Cannot apply theme: Application.Current is null. Ensure ThemeManager is called after App initialization.");

throw new NotSupportedException(
    $"Property type '{propertyType.Name}' is not supported by PropertyGrid. " +
    $"Supported types: string, int, double, bool, enum, DateTime.");

错误消息模板:

{What happened}: {Why it happened}. {How to fix it}.

Exception Types

使用合适的异常类型:

场景 异常类型 示例
参数为 null ArgumentNullException throw new ArgumentNullException(nameof(obj))
参数值无效 ArgumentException throw new ArgumentException("Invalid theme type", nameof(type))
参数超出范围 ArgumentOutOfRangeException throw new ArgumentOutOfRangeException(nameof(index))
操作在当前状态无效 InvalidOperationException throw new InvalidOperationException("Theme resources not loaded")
功能未实现 NotImplementedException throw new NotImplementedException("Custom editors not supported in MVP")
功能不支持 NotSupportedException throw new NotSupportedException($"Type {type} not supported")

Try-Catch Guidelines

何时使用 try-catch

// ✅ 捕获特定异常并提供上下文
try
{
    var value = property.GetValue(obj);
}
catch (TargetInvocationException ex)
{
    throw new InvalidOperationException(
        $"Property '{property.Name}' getter threw an exception.",
        ex.InnerException ?? ex);
}

// ✅ 记录异常并继续处理
try
{
    LoadThemeResource(uri);
}
catch (Exception ex)
{
    Logger.Error($"Failed to load theme resource: {uri}", ex);
    // 使用默认主题
    LoadDefaultTheme();
}

// ❌ 不要捕获所有异常并吞没
try
{
    DoSomething();
}
catch
{
    // 什么都不做 - 这是错误的!
}

// ❌ 不要捕获异常后重新抛出同一个异常
try
{
    DoSomething();
}
catch (Exception ex)
{
    throw ex; // 错误:会丢失调用栈
}

// ✅ 如果需要记录后重新抛出,使用 throw;
try
{
    DoSomething();
}
catch (Exception ex)
{
    Logger.Error("Operation failed", ex);
    throw; // 正确:保留调用栈
}

Validation Helpers

创建参数验证辅助方法:

namespace Penguin.AvaloniaUI.Utils;

internal static class Guard
{
    public static void NotNull<T>(T value, string paramName) where T : class
    {
        if (value == null)
        {
            throw new ArgumentNullException(paramName);
        }
    }

    public static void NotNullOrEmpty(string value, string paramName)
    {
        if (string.IsNullOrEmpty(value))
        {
            throw new ArgumentException("Value cannot be null or empty.", paramName);
        }
    }

    public static void InRange(int value, int min, int max, string paramName)
    {
        if (value < min || value > max)
        {
            throw new ArgumentOutOfRangeException(paramName,
                $"Value must be between {min} and {max}, but was {value}.");
        }
    }
}

// 使用示例
public void SetProperty(string name, object value)
{
    Guard.NotNullOrEmpty(name, nameof(name));
    Guard.NotNull(value, nameof(value));

    // 核心逻辑
}

Performance Optimization

本节定义性能优化策略和最佳实践,确保控件库满足 NFR 性能要求。


Performance Targets (from NFR)

指标 目标值 测量方法
UI 渲染帧率 60fps (16ms/frame) Visual Studio Performance Profiler
主题切换时间 < 100ms Stopwatch 测量
PropertyGrid 生成50 属性) < 200ms Stopwatch 测量
应用启动时间 < 2s 从启动到窗口显示
内存占用 < 100MB (空闲时) Task Manager / dotnet-counters

UI Rendering Performance

关键原则:避免在 UI 线程执行耗时操作

反面示例 - 阻塞 UI 线程:

// 错误:在属性 getter 中执行反射
public ObservableCollection<PropertyItem> Properties
{
    get
    {
        var items = new ObservableCollection<PropertyItem>();
        if (SelectedObject != null)
        {
            // 耗时操作会阻塞 UI 渲染
            var properties = SelectedObject.GetType().GetProperties();
            foreach (var prop in properties)
            {
                items.Add(CreatePropertyItem(SelectedObject, prop));
            }
        }
        return items;
    }
}

正确示例 - 异步加载:

private ObservableCollection<PropertyItem> _properties = new();
public ObservableCollection<PropertyItem> Properties => _properties;

private async void RefreshProperties(object? obj)
{
    _properties.Clear();

    if (obj == null) return;

    // 在后台线程执行反射
    var items = await Task.Run(() =>
    {
        var result = new List<PropertyItem>();
        var properties = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);

        foreach (var prop in properties)
        {
            result.Add(CreatePropertyItem(obj, prop));
        }

        return result;
    });

    // 回到 UI 线程更新集合
    foreach (var item in items)
    {
        _properties.Add(item);
    }
}

Reflection Optimization

问题:反射是 PropertyGrid 的性能瓶颈

优化策略 1缓存 PropertyInfo

private static readonly ConcurrentDictionary<Type, PropertyInfo[]> _propertyCache = new();

private PropertyInfo[] GetCachedProperties(Type type)
{
    return _propertyCache.GetOrAdd(type, t =>
        t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
    );
}

优化策略 2缓存 Attribute 读取

private static readonly ConcurrentDictionary<PropertyInfo, CategoryAttribute?> _categoryCache = new();

private string? GetCategory(PropertyInfo prop)
{
    var attr = _categoryCache.GetOrAdd(prop, p =>
        p.GetCustomAttribute<CategoryAttribute>()
    );

    return attr?.Category;
}

优化策略 3使用编译表达式替代反射调用可选

// 反射调用(慢)
var value = propertyInfo.GetValue(obj);

// 编译表达式(快 10-100 倍)
private static Func<object, object> CreateGetter(PropertyInfo prop)
{
    var instance = Expression.Parameter(typeof(object), "instance");
    var convert = Expression.Convert(instance, prop.DeclaringType!);
    var property = Expression.Property(convert, prop);
    var convertResult = Expression.Convert(property, typeof(object));

    return Expression.Lambda<Func<object, object>>(convertResult, instance).Compile();
}

性能对比:

反射 GetValue:       ~1000 ns/调用
编译表达式:          ~10 ns/调用
直接属性访问:        ~1 ns/调用

Theme Switching Performance

优化主题切换速度(目标 < 100ms

反面示例 - 逐个替换资源:

// 错误:逐个替换资源会触发多次 UI 刷新
foreach (var key in themeColors.Keys)
{
    Application.Current.Resources[key] = themeColors[key];
}

正确示例 - 批量替换 ResourceDictionary

public static void ApplyTheme(ThemeType type)
{
    var app = Application.Current;
    if (app == null) return;

    // 移除旧主题
    var oldTheme = app.Resources.MergedDictionaries
        .FirstOrDefault(d => d.Source?.ToString().Contains("Theme") == true);
    if (oldTheme != null)
        app.Resources.MergedDictionaries.Remove(oldTheme);

    // 一次性加载新主题(单次 UI 刷新)
    var uri = type == ThemeType.Light
        ? new Uri("avares://Penguin.AvaloniaUI/Themes/LightTheme.axaml")
        : new Uri("avares://Penguin.AvaloniaUI/Themes/DarkTheme.axaml");

    var newTheme = new ResourceDictionary { Source = uri };
    app.Resources.MergedDictionaries.Add(newTheme);

    _currentTheme = type;
    ThemeChanged?.Invoke(null, type);
}

预加载主题资源(可选优化):

private static ResourceDictionary? _cachedLightTheme;
private static ResourceDictionary? _cachedDarkTheme;

static ThemeManager()
{
    // 应用启动时预加载主题
    _cachedLightTheme = LoadThemeResource(ThemeType.Light);
    _cachedDarkTheme = LoadThemeResource(ThemeType.Dark);
}

public static void ApplyTheme(ThemeType type)
{
    var theme = type == ThemeType.Light ? _cachedLightTheme : _cachedDarkTheme;
    // 直接使用缓存的主题,无需重新加载
}

Memory Management

避免内存泄漏的常见陷阱:

问题 1事件订阅未取消

// ❌ 可能导致内存泄漏
public class PropertyGrid : TemplatedControl
{
    public PropertyGrid()
    {
        ThemeManager.ThemeChanged += OnThemeChanged;
        // 没有取消订阅!
    }

    private void OnThemeChanged(object? sender, ThemeType type)
    {
        RefreshTheme();
    }
}

// ✅ 正确实现 IDisposable
public class PropertyGrid : TemplatedControl, IDisposable
{
    public PropertyGrid()
    {
        ThemeManager.ThemeChanged += OnThemeChanged;
    }

    public void Dispose()
    {
        ThemeManager.ThemeChanged -= OnThemeChanged;
    }

    private void OnThemeChanged(object? sender, ThemeType type)
    {
        RefreshTheme();
    }
}

问题 2ObservableCollection 过度使用

// ❌ 每次都创建新集合
public ObservableCollection<PropertyItem> Properties
{
    get => new ObservableCollection<PropertyItem>(GetProperties());
}

// ✅ 复用同一个集合
private ObservableCollection<PropertyItem> _properties = new();
public ObservableCollection<PropertyItem> Properties => _properties;

private void RefreshProperties()
{
    _properties.Clear();
    foreach (var item in GetProperties())
    {
        _properties.Add(item);
    }
}

Virtualization (Post-MVP)

如果 PropertyGrid 需要支持数百个属性,使用虚拟化:

<ItemsControl Items="{Binding Properties}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <!-- VirtualizingStackPanel 仅渲染可见项 -->
            <VirtualizingStackPanel />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>

虚拟化性能对比:

100 个属性(非虚拟化): ~300ms 生成 + 50MB 内存
100 个属性(虚拟化):   ~100ms 生成 + 10MB 内存

Avoid Common Pitfalls

陷阱 1在循环中触发属性变化通知

// ❌ 每次循环触发 UI 刷新
for (int i = 0; i < 1000; i++)
{
    Properties.Add(new PropertyItem()); // 触发 CollectionChanged
}

// ✅ 批量添加后触发一次刷新
var items = new List<PropertyItem>();
for (int i = 0; i < 1000; i++)
{
    items.Add(new PropertyItem());
}
Properties.Clear();
foreach (var item in items)
{
    Properties.Add(item);
}

陷阱 2频繁的字符串拼接

// ❌ 每次拼接创建新字符串
string result = "";
for (int i = 0; i < 100; i++)
{
    result += properties[i].Name + ", ";
}

// ✅ 使用 StringBuilder
var sb = new StringBuilder();
for (int i = 0; i < 100; i++)
{
    sb.Append(properties[i].Name).Append(", ");
}
var result = sb.ToString();

陷阱 3XAML 中的复杂绑定

<!-- ❌ 每次渲染都执行转换 -->
<TextBlock Text="{Binding PropertyType, Converter={StaticResource ComplexConverter}}" />

<!-- ✅ 在 ViewModel 中预计算 -->
<TextBlock Text="{Binding PropertyTypeDisplayName}" />

Performance Measurement

使用 Stopwatch 测量关键操作:

#if DEBUG
private void RefreshProperties(object? obj)
{
    var sw = Stopwatch.StartNew();

    // 核心逻辑
    _properties.Clear();
    var items = GetProperties(obj);
    foreach (var item in items)
    {
        _properties.Add(item);
    }

    sw.Stop();
    Debug.WriteLine($"[PropertyGrid] RefreshProperties took {sw.ElapsedMilliseconds}ms for {items.Count} properties");
}
#endif

Visual Studio Performance Profiler

  1. 调试 → 性能探查器
  2. 选择"CPU 使用情况"或".NET 对象分配跟踪"
  3. 启动分析
  4. 执行性能关键操作(如生成 50 个属性)
  5. 停止分析并查看热点代码

dotnet-counters 监控内存:

# 安装工具
dotnet tool install --global dotnet-counters

# 监控运行中的应用
dotnet-counters monitor --process-id <pid> --counters System.Runtime

Optimization Checklist

开发时检查清单:

  • 反射操作已缓存PropertyInfo、Attribute
  • 耗时操作(> 50ms在后台线程执行
  • 事件订阅在控件销毁时取消
  • ObservableCollection 复用而非重建
  • 批量更新集合,避免频繁触发 CollectionChanged
  • 字符串拼接使用 StringBuilder
  • XAML 绑定避免复杂转换器
  • 主题切换批量替换 ResourceDictionary

发布前性能测试:

  • PropertyGrid 50 属性生成 < 200ms
  • 主题切换 < 100ms
  • 应用启动 < 2s
  • 空闲时内存 < 100MB
  • 无明显的 UI 卡顿(帧率 > 50fps

文档完成

文档状态 完成100% 最后更新2025-10-16


本架构文档涵盖了 Penguin.AvaloniaUI 控件库的完整技术架构,包括:

高层架构设计和技术选型 核心数据模型定义 组件设计和依赖关系 项目结构和文件组织 开发工作流和环境配置 编码规范和最佳实践 测试策略和示例 错误处理机制 性能优化指导

下一步: 基于本架构文档开始实施 PRD 中定义的 Epic 和 Story。