# 让控件支持 AtomUI Form 的开发指南 ## 1. 概述 本文档详细说明如何让一个自定义控件(或第三方控件)能够在 AtomUI 的 `Form` 中使用,参与数据收集、验证和提交流程。 --- ## 2. 快速判断:你的控件需要实现什么? ``` 你的控件是"输入控件"吗?(用户通过它输入/选择值) │ ├── 是 → 必须实现 IFormItemAware │ │ │ ├── 控件需要显示验证状态图标吗? → 实现 IFormItemFeedbackAware │ ├── 控件需要响应 Form 统一尺寸? → 实现 ISizeTypeAware │ ├── 控件需要响应 Form 统一样式变体? → 实现 IInputControlStyleVariantAware │ ├── 控件需要显示验证状态边框? → 实现 IInputControlStatusAware │ └── 控件需要响应 Form 动画开关? → 实现 IMotionAwareControl │ └── 否(如按钮、文本标签、纯展示控件) │ └── 放在 FormActionsItem 中(无需实现任何接口) 或 FormItem + IsValidateContentType="False" ``` --- ## 3. 接口实现详解 ### 3.1 IFormItemAware — 核心接口(必须) 这是控件接入 Form 的**唯一必要接口**。它定义了 Form 与控件之间的数据交互协议。 #### 接口定义 ```csharp public interface IFormItemAware { /// /// 当控件值发生变化时触发。Form 据此判断是否需要验证。 /// event EventHandler ValueChanged; /// /// Form 调用此方法设置控件的值(如初始值、外部赋值)。 /// void SetFormValue(object? value); /// /// Form 调用此方法获取控件的当前值(用于验证和提交)。 /// object? GetFormValue(); /// /// Form 重置时调用此方法清除控件的值。 /// void ClearFormValue(); /// /// Form 验证完成后调用此方法通知控件当前验证状态。 /// 控件可据此更新视觉样式(如红色边框)。 /// void NotifyValidateStatus(FormValidateStatus status); } ``` #### 实现模板 ```csharp using AtomUI.Controls; using Avalonia; using Avalonia.Controls.Primitives; namespace YourNamespace; public class MyInputControl : TemplatedControl, IFormItemAware { // 1. 定义控件的值属性 public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register(nameof(Value)); public string? Value { get => GetValue(ValueProperty); set => SetValue(ValueProperty, value); } // 2. 监听值变化 static MyInputControl() { ValueProperty.Changed.AddClassHandler( (control, _) => control.HandleValueChanged()); } // ===== IFormItemAware 实现 ===== // 3. 声明值变更事件(显式接口实现,不暴露为公共 API) private EventHandler? _formValueChanged; event EventHandler? IFormItemAware.ValueChanged { add => _formValueChanged += value; remove => _formValueChanged -= value; } // 4. 在值变化时触发事件 private void HandleValueChanged() { _formValueChanged?.Invoke(this, EventArgs.Empty); } // 5. 实现值的设置(注意类型转换) void IFormItemAware.SetFormValue(object? value) => NotifySetFormValue(value?.ToString()); // 6. 实现值的获取 object? IFormItemAware.GetFormValue() => NotifyGetFormValue(); // 7. 实现值的清除 void IFormItemAware.ClearFormValue() => NotifyClearFormValue(); // 8. 实现验证状态通知 void IFormItemAware.NotifyValidateStatus(FormValidateStatus status) => NotifyValidateStatus(status); // ===== 可覆写的保护方法(便于子类扩展) ===== protected virtual void NotifySetFormValue(string? value) { SetCurrentValue(ValueProperty, value); } protected virtual string? NotifyGetFormValue() { return Value; } protected virtual void NotifyClearFormValue() { SetCurrentValue(ValueProperty, null); } protected virtual void NotifyValidateStatus(FormValidateStatus status) { // 基本实现:如果控件实现了 IInputControlStatusAware,可以在此设置状态 } } ``` #### 关键要点 1. **使用显式接口实现**:将 `IFormItemAware` 的成员作为显式接口实现(`void IFormItemAware.SetFormValue(...)`),避免污染控件的公共 API。 2. **使用 `SetCurrentValue` 而非 `SetValue`**:`SetCurrentValue` 设置值但不覆盖外部绑定,是 Form 场景下的最佳实践。 3. **提供 `protected virtual` 方法**:使用委托到 `protected virtual` 方法的模式,使子类可以覆写行为。 4. **值变化必须触发事件**:确保控件值每次变化时都调用 `_formValueChanged?.Invoke(this, EventArgs.Empty)`,否则 Form 无法感知值变更。 5. **注意 `SetFormValue` 的类型转换**:Form 通过 `object?` 传递值,控件需要做适当的类型转换。 ### 3.2 IFormItemFeedbackAware — 验证反馈(可选) 当控件需要在输入框内显示验证状态图标(✓、✗、⚠、⏳)时,实现此接口。 ```csharp public interface IFormItemFeedbackAware { void SetFeedbackControl(FormValidateFeedback? value); } ``` #### 实现方式 ```csharp public class MyInputControl : TemplatedControl, IFormItemAware, IFormItemFeedbackAware { // 定义内部属性存储反馈控件 internal static readonly StyledProperty FormFeedbackProperty = AvaloniaProperty.Register(nameof(FormFeedback)); internal FormValidateFeedback? FormFeedback { get => GetValue(FormFeedbackProperty); set => SetValue(FormFeedbackProperty, value); } // 实现接口 void IFormItemFeedbackAware.SetFeedbackControl(FormValidateFeedback? value) => NotifySetFeedBackControl(value); protected virtual void NotifySetFeedBackControl(FormValidateFeedback? value) { FormFeedback = value; } // ... 在模板中使用 {TemplateBinding FormFeedback} 展示反馈控件 } ``` ### 3.3 ISizeTypeAware — 尺寸级联(推荐) ```csharp public class MyInputControl : TemplatedControl, ISizeTypeAware { // 必须使用 AddOwner,不能自行 Register public static readonly StyledProperty SizeTypeProperty = SizeTypeControlProperty.SizeTypeProperty.AddOwner(); public SizeType SizeType { get => GetValue(SizeTypeProperty); set => SetValue(SizeTypeProperty, value); } } ``` > ⚠️ **必须使用 `AddOwner`**,确保属性名 `"SizeType"` 与系统一致,级联绑定才能正确工作。 ### 3.4 IInputControlStyleVariantAware — 样式变体级联(推荐) ```csharp public class MyInputControl : TemplatedControl, IInputControlStyleVariantAware { public static readonly StyledProperty StyleVariantProperty = InputControlStyleVariantProperty.StyleVariantProperty.AddOwner(); public InputControlStyleVariant StyleVariant { get => GetValue(StyleVariantProperty); set => SetValue(StyleVariantProperty, value); } } ``` ### 3.5 IInputControlStatusAware — 验证状态视觉反馈(推荐) 当控件需要根据验证状态显示不同的边框颜色时: ```csharp public class MyInputControl : TemplatedControl, IFormItemAware, IInputControlStatusAware { public static readonly StyledProperty StatusProperty = InputControlStatusProperty.StatusProperty.AddOwner(); public InputControlStatus Status { get => GetValue(StatusProperty); set => SetValue(StatusProperty, value); } // 在 NotifyValidateStatus 中映射 FormValidateStatus → InputControlStatus protected virtual void NotifyValidateStatus(FormValidateStatus status) { var inputStatus = status switch { FormValidateStatus.Error => InputControlStatus.Error, FormValidateStatus.Warning => InputControlStatus.Warning, _ => InputControlStatus.Default }; SetCurrentValue(StatusProperty, inputStatus); } } ``` 然后在 AXAML 主题中使用伪类或属性选择器实现视觉反馈: ```xml ``` ### 3.6 IMotionAwareControl — 动画开关级联(可选) ```csharp public class MyInputControl : TemplatedControl, IMotionAwareControl { public static readonly StyledProperty IsMotionEnabledProperty = MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner(); public bool IsMotionEnabled { get => GetValue(IsMotionEnabledProperty); set => SetValue(IsMotionEnabledProperty, value); } } ``` --- ## 4. 完整实现示例 ### 4.1 简单文本输入控件 一个只需要基本 Form 集成的控件: ```csharp using AtomUI.Controls; using Avalonia; using Avalonia.Controls.Primitives; namespace MyApp.Controls; public class SimpleInput : TemplatedControl, IFormItemAware { public static readonly StyledProperty TextProperty = AvaloniaProperty.Register(nameof(Text)); public string? Text { get => GetValue(TextProperty); set => SetValue(TextProperty, value); } static SimpleInput() { TextProperty.Changed.AddClassHandler( (control, _) => control.HandleValueChanged()); } #region IFormItemAware 实现 private EventHandler? _formValueChanged; event EventHandler? IFormItemAware.ValueChanged { add => _formValueChanged += value; remove => _formValueChanged -= value; } void IFormItemAware.SetFormValue(object? value) => SetCurrentValue(TextProperty, value?.ToString()); object? IFormItemAware.GetFormValue() => Text; void IFormItemAware.ClearFormValue() => SetCurrentValue(TextProperty, null); void IFormItemAware.NotifyValidateStatus(FormValidateStatus status) { } private void HandleValueChanged() { _formValueChanged?.Invoke(this, EventArgs.Empty); } #endregion } ``` 在 AXAML 中使用: ```xml ``` ### 4.2 完整功能的复合输入控件 一个包含完整 Form 集成的复合输入控件: ```csharp using AtomUI; using AtomUI.Controls; using AtomUI.Desktop.Controls; using Avalonia; using Avalonia.Controls.Primitives; namespace MyApp.Controls; // 值类型 public record PriceInfo(decimal Value, string Unit); public class PriceInput : TemplatedControl, IFormItemAware, ISizeTypeAware, IMotionAwareControl, IInputControlStatusAware, IInputControlStyleVariantAware { #region 公共属性定义 public static readonly StyledProperty SizeTypeProperty = SizeTypeControlProperty.SizeTypeProperty.AddOwner(); public static readonly StyledProperty IsMotionEnabledProperty = MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner(); public static readonly StyledProperty StyleVariantProperty = InputControlStyleVariantProperty.StyleVariantProperty.AddOwner(); public static readonly StyledProperty StatusProperty = InputControlStatusProperty.StatusProperty.AddOwner(); public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register(nameof(Value)); public SizeType SizeType { get => GetValue(SizeTypeProperty); set => SetValue(SizeTypeProperty, value); } public bool IsMotionEnabled { get => GetValue(IsMotionEnabledProperty); set => SetValue(IsMotionEnabledProperty, value); } public InputControlStyleVariant StyleVariant { get => GetValue(StyleVariantProperty); set => SetValue(StyleVariantProperty, value); } public InputControlStatus Status { get => GetValue(StatusProperty); set => SetValue(StatusProperty, value); } public PriceInfo? Value { get => GetValue(ValueProperty); set => SetValue(ValueProperty, value); } #endregion // 内部子控件 private NumericUpDown? _numberInput; private Select? _unitSelect; protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); // 查找模板中的子控件并订阅变更事件 _numberInput = e.NameScope.Find("NumberInput"); _unitSelect = e.NameScope.Find