# 让控件支持 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