# 编码规范 > 本文档规定 AtomUI 中所有 Avalonia 内部成员反射访问代码的编写标准。 --- ## 1. 文件命名与组织 ### 1.1 命名规则 反射扩展类的文件和类名 **必须** 遵循以下格式: ``` {Avalonia目标类型名}ReflectionExtensions.cs ``` 示例: | Avalonia 目标类型 | 文件名 | 类名 | |---|---|---| | `Visual` | `VisualReflectionExtensions.cs` | `VisualReflectionExtensions` | | `StyledElement` | `StyledElementReflectionExtensions.cs` | `StyledElementReflectionExtensions` | | `TextBox` | `TextBoxReflectionExtensions.cs` | `TextBoxReflectionExtensions` | | `WindowBase` | `WindowBaseReflectionExtensions.cs` | `WindowBaseReflectionExtensions` | | `SelectionModel<>` | `SelectionModelReflectionExtensions.cs` | `SelectionModelReflectionExtensions` | ### 1.2 文件位置 - **通用反射扩展**(多个控件共用的 Avalonia 基础类型)放在 `AtomUI.Core` 的对应目录下: - `Reflection/` — `StyledElement`、`Visual` 等基础类型 - `Controls/` — `ItemCollection`、`ILayoutRoot` 等控件相关类型 - `Animations/` — `Animatable` 等动画相关类型 - `Data/` — `DynamicResourceExtension` 等数据绑定类型 - `Media/` — `TextParagraphProperties` 等媒体类型 - `Utils/` — `ManagedPopupPositioner`、`AvaloniaProperty` 等工具类型 - `Input/` — `DispatcherPriority` 等输入相关类型 - **特定控件的反射扩展** 放在 `AtomUI.Desktop.Controls`(或对应平台控件项目)中该控件的文件夹下: - `TextBlock/TextBlockReflectionExtensions.cs` - `Input/Utils/TextBoxReflectionExtensions.cs` - `Window/WindowBaseReflectionExtensions.cs` - `ComboBox/ComboBoxReflectionExtensions.cs` - `Menu/MenuReflectionExtensions.cs` - `Popup/PopupReflectionExtensions.cs` - `Primitives/SelectingItemsControlReflectionExtensions.cs` ### 1.3 就近放置原则 如果一个反射扩展只被某个特定控件使用,应放在该控件的文件夹内。如果被多个控件跨模块使用,应下沉到 `AtomUI.Core`。 --- ## 2. 类结构标准模板 每个反射扩展类 **必须** 遵循以下结构: ```csharp using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using AtomUI.Reflection; // ... 其他必要的 using namespace AtomUI.Desktop.Controls; // 或 AtomUI.Controls / AtomUI.Xxx // ① 如有命名冲突,使用 using 别名 using AvaloniaTextBox = Avalonia.Controls.TextBox; // ② 类声明:internal static class,命名为 {目标类型}ReflectionExtensions internal static class TextBoxReflectionExtensions { // ③ 反射信息定义区域(用 #region 包裹) #region 反射信息定义 // ④ 每个反射缓存字段都必须标注 [DynamicDependency] [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicFields, typeof(AvaloniaTextBox))] private static readonly Lazy ScrollViewerFieldInfo = new Lazy(() => typeof(AvaloniaTextBox).GetFieldInfoOrThrow("_scrollViewer", BindingFlags.Instance | BindingFlags.NonPublic)); [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, typeof(AvaloniaTextBox))] private static readonly Lazy HandleTextInputMethodInfo = new Lazy(() => typeof(AvaloniaTextBox).GetMethodInfoOrThrow("HandleTextInput", BindingFlags.Instance | BindingFlags.NonPublic)); #endregion // ⑤ 公开的扩展方法(强类型包装) public static ScrollViewer? GetScrollViewer(this AvaloniaTextBox textBox) { return ScrollViewerFieldInfo.Value.GetValue(textBox) as ScrollViewer; } public static void HandleTextInput(this AvaloniaTextBox textBox, string? input) { HandleTextInputMethodInfo.Value.Invoke(textBox, [input]); } } ``` --- ## 3. 反射缓存字段规范 ### 3.1 声明规则 | 规则 | 说明 | |---|---| | 访问修饰符 | `private static readonly`(仅在极少数跨类共享场景下使用 `internal static readonly`) | | 类型 | `Lazy` / `Lazy` / `Lazy` / `Lazy` | | 命名 | `{成员名}FieldInfo` / `{成员名}PropertyInfo` / `{成员名}MethodInfo` / `{成员名}EventInfo` | | 注解 | **必须** 标注 `[DynamicDependency]` | | 初始化 | 使用 `TypeMemberExtension` 中的 `GetXxxInfoOrThrow` 方法 | ### 3.2 `[DynamicDependency]` 标注规则 根据访问的成员类型选择正确的 `DynamicallyAccessedMemberTypes`: | 反射访问目标 | `DynamicallyAccessedMemberTypes` 值 | |---|---| | 私有/内部字段 | `NonPublicFields` | | 私有/内部属性 | `NonPublicProperties` | | 私有/内部方法 | `NonPublicMethods` | | 私有/内部事件 | `NonPublicEvents` | ```csharp // ✅ 正确:访问私有字段,使用 NonPublicFields [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicFields, typeof(AvaloniaComboBox))] private static readonly Lazy PopupFieldInfo = ...; // ✅ 正确:访问私有方法,使用 NonPublicMethods [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, typeof(Animatable))] private static readonly Lazy EnableTransitionsMethodInfo = ...; // ✅ 正确:访问私有事件,使用 NonPublicEvents [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicEvents, typeof(Popup))] private static readonly Lazy ClosingEventInfo = ...; // ❌ 错误:缺少 [DynamicDependency] 注解 private static readonly Lazy SomeFieldInfo = new Lazy(() => typeof(SomeType).GetFieldInfoOrThrow("_someField", ...)); // ❌ 错误:DynamicallyAccessedMemberTypes 与实际访问的成员类型不匹配 [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, typeof(SomeType))] // 访问的是字段! private static readonly Lazy SomeFieldInfo = ...; ``` ### 3.3 `BindingFlags` 选择指南 | 成员特征 | `BindingFlags` | |---|---| | 实例私有成员 | `BindingFlags.Instance \| BindingFlags.NonPublic` | | 静态私有成员 | `BindingFlags.Static \| BindingFlags.NonPublic` | | 公开成员(偶尔需要) | `BindingFlags.Instance \| BindingFlags.Public` | | 需要搜索继承层次 | 追加 `BindingFlags.FlattenHierarchy` | > **注意**:`GetXxxInfoOrThrow` 方法的默认 `flags` 已包含 `FlattenHierarchy`,通常只需显式指定 `Instance | NonPublic` 即可。但如果目标成员定义在基类中且需要明确搜索继承链,应显式添加 `FlattenHierarchy`。 ### 3.4 命名约定 反射缓存字段命名遵循 `{Avalonia成员名}{Info类型}` 模式: ```csharp // 字段:_scrollViewer → ScrollViewerFieldInfo private static readonly Lazy ScrollViewerFieldInfo = ...; // 属性:LogicalChildren → LogicalChildrenPropertyInfo private static readonly Lazy LogicalChildrenPropertyInfo = ...; // 方法:SetVisualParent → SetVisualParentMethodInfo private static readonly Lazy SetVisualParentMethodInfo = ...; // 事件:Closing → ClosingEventInfo private static readonly Lazy ClosingEventInfo = ...; ``` 字段名中 **不包含** 目标类型名称前缀(因为所属类名已经包含了目标类型信息)。 例外:如果一个扩展类中有来自不同 Avalonia 类型的反射成员(极少见),可用类型名前缀区分。 --- ## 4. 扩展方法封装规范 ### 4.1 可见性 | 场景 | 可见性 | |---|---| | 被控件外部直接调用 | `public` 或 `internal` | | 仅在扩展类内部组合使用 | `private` | | 复杂流程中的多步骤方法 | 入口方法 `public`,子步骤 `private` | 参考 `WindowBaseReflectionExtensions`: ```csharp // 入口方法:public public static void ShowWithoutActive(this WindowBase window) { ... } // 子步骤:private private static IDisposable FreezeVisibilityChangeHandling(this WindowBase window) { ... } private static void EnsureInitialized(this WindowBase window) { ... } private static void StartRendering(this WindowBase window) { ... } ``` ### 4.2 返回值处理 根据返回值类型选择不同的处理策略: **值类型返回值 —— 使用 `as T?` + `Debug.Assert`** ```csharp public static Size GetMaxSizeFromConstraint(this AvaloniaTextBlock textBlock) { var size = GetMaxSizeFromConstraintMethodInfo.Value.Invoke(textBlock, []) as Size?; Debug.Assert(size != null); return size.Value; } ``` **引用类型返回值(非空预期)—— 使用 `as T` + `Debug.Assert`** ```csharp public static IAvaloniaList GetLogicalChildrenList(this StyledElement styledElement) { var children = LogicalChildrenPropertyInfo.Value.GetValue(styledElement) as IAvaloniaList; Debug.Assert(children != null); return children; } ``` **引用类型返回值(可空)—— 直接 `as T`** ```csharp public static ScrollViewer? GetScrollViewer(this AvaloniaTextBox textBox) { return ScrollViewerFieldInfo.Value.GetValue(textBox) as ScrollViewer; } ``` **无返回值(方法调用 / 设值操作)—— 直接调用** ```csharp public static void SetPopup(this AvaloniaComboBox comboBox, Popup? popup) { PopupFieldInfo.Value.SetValue(comboBox, popup); } public static void HandleTextInput(this AvaloniaTextBox textBox, string? input) { HandleTextInputMethodInfo.Value.Invoke(textBox, [input]); } ``` ### 4.3 `this` 参数类型 扩展方法的 `this` 参数类型 **必须** 是被 Hook 的 Avalonia 类型(或其别名),不能是 AtomUI 自定义子类: ```csharp // ✅ 正确:this 参数是 Avalonia 类型 public static void SetVisualParent(this Visual visual, Control? parent) { ... } public static void SetPopup(this AvaloniaComboBox comboBox, Popup? popup) { ... } // ❌ 错误:this 参数不应该是 AtomUI 子类(除非扩展确实只适用于子类) public static void SetPopup(this ComboBox comboBox, Popup? popup) { ... } ``` --- ## 5. `using` 类型别名规范 当 AtomUI 自定义控件与 Avalonia 内置控件同名时,**必须** 在反射扩展类文件顶部声明 `using` 别名: ```csharp using AvaloniaTextBox = Avalonia.Controls.TextBox; using AvaloniaTextBlock = Avalonia.Controls.TextBlock; using AvaloniaComboBox = Avalonia.Controls.ComboBox; using AvaloniaContextMenu = Avalonia.Controls.ContextMenu; using AvaloniaPopup = Avalonia.Controls.Primitives.Popup; ``` 别名统一使用 `Avalonia` 前缀 + 原始类型名,即 `Avalonia{ClassName}`。 --- ## 6. 泛型目标类型 当反射目标是泛型类型时,使用开放泛型 `typeof(SelectionModel<>)` 而非具体闭合类型: ```csharp // ✅ 正确:使用开放泛型 [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, typeof(SelectionModel<>))] public static void SetSource(this SelectionModel model, IEnumerable? value) { var targetType = typeof(SelectionModel); var method = _setSourceMethodCache.GetOrAdd(targetType, t => t.GetMethodInfoOrThrow("SetSource", BindingFlags.Instance | BindingFlags.NonPublic)); method.Invoke(model, [value]); } ``` 对于泛型类型,由于闭合类型的 `MethodInfo` 各不相同,应使用 `ConcurrentDictionary` 缓存替代 `Lazy`: ```csharp private static readonly ConcurrentDictionary _setSourceMethodCache = new(); ``` --- ## 7. 事件 Hook 规范 对于需要反射访问的私有事件,使用 `EventInfo` + `GetAddMethod()` / `GetRemoveMethod()`: ```csharp #region 反射信息定义 [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicEvents, typeof(Popup))] private static readonly Lazy ClosingEventInfo = new Lazy(() => typeof(Popup).GetEventInfoOrThrow("Closing", BindingFlags.NonPublic | BindingFlags.Instance)); #endregion public static void AddClosingEventHandler(this Popup popup, EventHandler handler) { var closingEventAddMethod = ClosingEventInfo.Value.GetAddMethod(); closingEventAddMethod?.Invoke(popup, [handler]); } public static void RemoveClosingEventHandler(this Popup popup, EventHandler handler) { var closingEventRemoveMethod = ClosingEventInfo.Value.GetRemoveMethod(); closingEventRemoveMethod?.Invoke(popup, [handler]); } ``` --- ## 8. 禁止事项 | ❌ 禁止 | 说明 | |---|---| | 在业务代码中直接使用 `typeof(X).GetMethod(...)` | 所有反射访问必须封装在 `XxxReflectionExtensions` 中 | | 缺少 `[DynamicDependency]` 注解 | 会导致 Trimming 后运行时异常 | | 使用 `GetMethod` / `GetField` / `GetProperty` 而非 `OrThrow` 版本 | 丢失快速失败能力,可能出现 `NullReferenceException` | | 在反射缓存中使用非 `Lazy` 的静态字段 | 丧失延迟初始化和线程安全性 | | 将反射扩展类标记为 `public` | 反射 Hook 是内部实现细节,不应暴露给外部消费者 | | 反射扩展类中包含非反射的业务逻辑 | 扩展类应保持纯粹,仅包含反射访问封装 | --- ## 9. 完整示例 以下是一个符合所有规范的反射扩展类完整示例: ```csharp using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using AtomUI.Reflection; using Avalonia; using Avalonia.Controls; namespace AtomUI.Desktop.Controls; using AvaloniaComboBox = Avalonia.Controls.ComboBox; /// /// 提供对 Avalonia ComboBox 私有成员的反射访问。 /// /// /// 此类 Hook 的成员不受 Avalonia 公开 API 契约保护, /// 升级 Avalonia 版本时需要验证兼容性。 /// /// Hook 目标(Avalonia v11.3.x): /// - _popup (PrivateField): Avalonia.Controls.Primitives.Popup /// internal static class ComboBoxReflectionExtensions { #region 反射信息定义 [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicFields, typeof(AvaloniaComboBox))] private static readonly Lazy PopupFieldInfo = new Lazy(() => typeof(AvaloniaComboBox).GetFieldInfoOrThrow("_popup", BindingFlags.Instance | BindingFlags.NonPublic)); #endregion public static void SetPopup(this AvaloniaComboBox comboBox, Popup? popup) { PopupFieldInfo.Value.SetValue(comboBox, popup); } } ```