mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-18 14:37:19 +08:00
21 KiB
21 KiB
让控件支持 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 与控件之间的数据交互协议。
接口定义
public interface IFormItemAware
{
/// <summary>
/// 当控件值发生变化时触发。Form 据此判断是否需要验证。
/// </summary>
event EventHandler ValueChanged;
/// <summary>
/// Form 调用此方法设置控件的值(如初始值、外部赋值)。
/// </summary>
void SetFormValue(object? value);
/// <summary>
/// Form 调用此方法获取控件的当前值(用于验证和提交)。
/// </summary>
object? GetFormValue();
/// <summary>
/// Form 重置时调用此方法清除控件的值。
/// </summary>
void ClearFormValue();
/// <summary>
/// Form 验证完成后调用此方法通知控件当前验证状态。
/// 控件可据此更新视觉样式(如红色边框)。
/// </summary>
void NotifyValidateStatus(FormValidateStatus status);
}
实现模板
using AtomUI.Controls;
using Avalonia;
using Avalonia.Controls.Primitives;
namespace YourNamespace;
public class MyInputControl : TemplatedControl, IFormItemAware
{
// 1. 定义控件的值属性
public static readonly StyledProperty<string?> ValueProperty =
AvaloniaProperty.Register<MyInputControl, string?>(nameof(Value));
public string? Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
// 2. 监听值变化
static MyInputControl()
{
ValueProperty.Changed.AddClassHandler<MyInputControl>(
(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,可以在此设置状态
}
}
关键要点
- 使用显式接口实现:将
IFormItemAware的成员作为显式接口实现(void IFormItemAware.SetFormValue(...)),避免污染控件的公共 API。 - 使用
SetCurrentValue而非SetValue:SetCurrentValue设置值但不覆盖外部绑定,是 Form 场景下的最佳实践。 - 提供
protected virtual方法:使用委托到protected virtual方法的模式,使子类可以覆写行为。 - 值变化必须触发事件:确保控件值每次变化时都调用
_formValueChanged?.Invoke(this, EventArgs.Empty),否则 Form 无法感知值变更。 - 注意
SetFormValue的类型转换:Form 通过object?传递值,控件需要做适当的类型转换。
3.2 IFormItemFeedbackAware — 验证反馈(可选)
当控件需要在输入框内显示验证状态图标(✓、✗、⚠、⏳)时,实现此接口。
public interface IFormItemFeedbackAware
{
void SetFeedbackControl(FormValidateFeedback? value);
}
实现方式
public class MyInputControl : TemplatedControl,
IFormItemAware,
IFormItemFeedbackAware
{
// 定义内部属性存储反馈控件
internal static readonly StyledProperty<FormValidateFeedback?> FormFeedbackProperty =
AvaloniaProperty.Register<MyInputControl, FormValidateFeedback?>(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 — 尺寸级联(推荐)
public class MyInputControl : TemplatedControl, ISizeTypeAware
{
// 必须使用 AddOwner,不能自行 Register
public static readonly StyledProperty<SizeType> SizeTypeProperty =
SizeTypeControlProperty.SizeTypeProperty.AddOwner<MyInputControl>();
public SizeType SizeType
{
get => GetValue(SizeTypeProperty);
set => SetValue(SizeTypeProperty, value);
}
}
⚠️ 必须使用
AddOwner,确保属性名"SizeType"与系统一致,级联绑定才能正确工作。
3.4 IInputControlStyleVariantAware — 样式变体级联(推荐)
public class MyInputControl : TemplatedControl, IInputControlStyleVariantAware
{
public static readonly StyledProperty<InputControlStyleVariant> StyleVariantProperty =
InputControlStyleVariantProperty.StyleVariantProperty.AddOwner<MyInputControl>();
public InputControlStyleVariant StyleVariant
{
get => GetValue(StyleVariantProperty);
set => SetValue(StyleVariantProperty, value);
}
}
3.5 IInputControlStatusAware — 验证状态视觉反馈(推荐)
当控件需要根据验证状态显示不同的边框颜色时:
public class MyInputControl : TemplatedControl,
IFormItemAware,
IInputControlStatusAware
{
public static readonly StyledProperty<InputControlStatus> StatusProperty =
InputControlStatusProperty.StatusProperty.AddOwner<MyInputControl>();
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 主题中使用伪类或属性选择器实现视觉反馈:
<Style Selector="^[Status=Error]">
<Setter Property="BorderBrush" Value="{atom:SharedTokenResource ColorError}" />
</Style>
<Style Selector="^[Status=Warning]">
<Setter Property="BorderBrush" Value="{atom:SharedTokenResource ColorWarning}" />
</Style>
3.6 IMotionAwareControl — 动画开关级联(可选)
public class MyInputControl : TemplatedControl, IMotionAwareControl
{
public static readonly StyledProperty<bool> IsMotionEnabledProperty =
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<MyInputControl>();
public bool IsMotionEnabled
{
get => GetValue(IsMotionEnabledProperty);
set => SetValue(IsMotionEnabledProperty, value);
}
}
4. 完整实现示例
4.1 简单文本输入控件
一个只需要基本 Form 集成的控件:
using AtomUI.Controls;
using Avalonia;
using Avalonia.Controls.Primitives;
namespace MyApp.Controls;
public class SimpleInput : TemplatedControl, IFormItemAware
{
public static readonly StyledProperty<string?> TextProperty =
AvaloniaProperty.Register<SimpleInput, string?>(nameof(Text));
public string? Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
static SimpleInput()
{
TextProperty.Changed.AddClassHandler<SimpleInput>(
(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 中使用:
<atom:FormItem LabelText="Simple Input" FieldName="simpleInput">
<atom:FormItem.Validators>
<atom:FormValidatorProvider>
<atom:FormStringNotEmptyValidator Message="Please input!" />
</atom:FormValidatorProvider>
</atom:FormItem.Validators>
<local:SimpleInput />
</atom:FormItem>
4.2 完整功能的复合输入控件
一个包含完整 Form 集成的复合输入控件:
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<SizeType> SizeTypeProperty =
SizeTypeControlProperty.SizeTypeProperty.AddOwner<PriceInput>();
public static readonly StyledProperty<bool> IsMotionEnabledProperty =
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<PriceInput>();
public static readonly StyledProperty<InputControlStyleVariant> StyleVariantProperty =
InputControlStyleVariantProperty.StyleVariantProperty.AddOwner<PriceInput>();
public static readonly StyledProperty<InputControlStatus> StatusProperty =
InputControlStatusProperty.StatusProperty.AddOwner<PriceInput>();
public static readonly StyledProperty<PriceInfo?> ValueProperty =
AvaloniaProperty.Register<PriceInput, PriceInfo?>(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<NumericUpDown>("NumberInput");
_unitSelect = e.NameScope.Find<Select>("UnitSelect");
if (_numberInput != null)
_numberInput.ValueChanged += (_, _) => UpdateValue();
if (_unitSelect != null)
_unitSelect.SelectionChanged += (_, _) => UpdateValue();
}
private void UpdateValue()
{
var amount = _numberInput?.Value ?? 0;
var unit = _unitSelect?.SelectedOption?.Content?.ToString() ?? "RMB";
Value = new PriceInfo(amount, unit);
_formValueChanged?.Invoke(this, EventArgs.Empty); // 通知 Form 值已变更
}
#region IFormItemAware 实现
private EventHandler? _formValueChanged;
event EventHandler? IFormItemAware.ValueChanged
{
add => _formValueChanged += value;
remove => _formValueChanged -= value;
}
void IFormItemAware.SetFormValue(object? value)
=> NotifySetFormValue(value as PriceInfo);
object? IFormItemAware.GetFormValue()
=> NotifyGetFormValue();
void IFormItemAware.ClearFormValue()
=> NotifyClearFormValue();
void IFormItemAware.NotifyValidateStatus(FormValidateStatus status)
=> NotifyValidateStatus(status);
protected virtual void NotifySetFormValue(PriceInfo? value)
{
SetCurrentValue(ValueProperty, value);
}
protected virtual PriceInfo? NotifyGetFormValue() => Value;
protected virtual void NotifyClearFormValue()
{
SetCurrentValue(ValueProperty, null);
}
protected virtual void NotifyValidateStatus(FormValidateStatus status)
{
var inputStatus = status switch
{
FormValidateStatus.Error => InputControlStatus.Error,
FormValidateStatus.Warning => InputControlStatus.Warning,
_ => InputControlStatus.Default
};
SetCurrentValue(StatusProperty, inputStatus);
}
#endregion
}
4.3 自定义验证器
using AtomUI.Controls;
namespace MyApp.Validators;
public class PriceValidator : AbstractFormValidator
{
protected override async Task<bool> NotifyValidateAsync(
string fieldName,
object? value,
CancellationToken cancellationToken)
{
var price = value as PriceInfo;
return await Task.FromResult(price != null && price.Value > 0);
}
}
在 AXAML 中使用:
<atom:FormItem LabelText="Price" FieldName="price">
<atom:FormItem.Validators>
<atom:FormValidatorProvider>
<local:PriceValidator Message="Price must be greater than zero!" />
</atom:FormValidatorProvider>
</atom:FormItem.Validators>
<local:PriceInput />
</atom:FormItem>
5. 使用 FormItemDecorator 包裹控件
当你需要在 FormItem 中为控件添加额外 UI(如后缀说明文字),但不想修改原控件时,可以用 FormItemDecorator:
<atom:FormItem LabelText="InputNumber" FieldName="inputNumber">
<atom:FormItemDecorator>
<atom:NumericUpDown Minimum="1" Maximum="10" Width="120"/>
<atom:FormItemDecorator.Extra>
machines
</atom:FormItemDecorator.Extra>
</atom:FormItemDecorator>
</atom:FormItem>
FormItemDecorator 自身实现了 IFormItemAware 和 IFormItemFeedbackAware,它会将所有 Form 交互透传给内部子控件。
注意:子控件本身必须已经实现了
IFormItemAware。FormItemDecorator不会凭空为非 Form-aware 的控件添加 Form 能力。
6. 自定义 FormItem 子类
当需要实现 FormItem 之间的联动(如"Gender"选择变化后自动填充"Note"),可以继承 FormItem 并覆写 NotifyFormItemChanged:
public class NoteFormItem : FormItem
{
protected override void NotifyFormItemChanged(IFormItem formItem)
{
if (formItem.FieldName == "gender")
{
if (formItem.Content is Select select && select.Mode == SelectMode.Single)
{
if (formItem.GetItemValue() is ISelectOption selectOption)
{
SetItemValue($"Hi, {selectOption.Content}!");
}
}
}
}
}
当 Form 中任何 FormItem 的值发生变化时,Form 会遍历其他 FormItem 调用 NotifyFormItemChanged,传入变化了值的 FormItem 引用。
7. 实现检查清单
为确保控件的 Form 集成完整且正确,请逐项核对:
必须项
- 实现
IFormItemAware接口(如果控件是输入控件) ValueChanged事件在每次值变化时触发SetFormValue使用SetCurrentValue设置值属性GetFormValue返回正确的值(注意类型)ClearFormValue将值重置为默认状态NotifyValidateStatus正确处理验证状态
推荐项
- 实现
ISizeTypeAware(使用AddOwner,不要自行Register) - 实现
IInputControlStyleVariantAware(使用AddOwner) - 实现
IInputControlStatusAware(使用AddOwner),并在NotifyValidateStatus中映射状态 - 实现
IMotionAwareControl(使用AddOwner)
可选项
- 实现
IFormItemFeedbackAware(如需显示验证状态图标) - 提供自定义
IFormValidator(如控件的值类型需要特殊验证逻辑)
常见错误
| 错误 | 后果 | 解决方案 |
|---|---|---|
未实现 IFormItemAware |
运行时抛出异常 | 实现接口或设置 IsValidateContentType="False" |
值变化时未触发 ValueChanged |
Form 无法感知值变更,验证不会触发 | 确保在值变化的回调中调用 _formValueChanged?.Invoke(...) |
SizeType 使用 Register 而非 AddOwner |
级联绑定不生效,尺寸不会跟随 Form 变化 | 使用 SizeTypeControlProperty.SizeTypeProperty.AddOwner<T>() |
SetFormValue 中使用 SetValue 而非 SetCurrentValue |
可能覆盖用户的外部绑定 | 使用 SetCurrentValue |
GetFormValue 返回类型不匹配验证器预期 |
验证器无法正确处理值 | 确保返回值类型与验证器的类型检查匹配 |
8. AXAML 中 Form 使用速查
基本表单
<atom:Form LabelColInfo="8*" WrapperColInfo="16*">
<atom:FormItem LabelText="Username" FieldName="username" IsRequired="True">
<atom:FormItem.Validators>
<atom:FormValidatorProvider>
<atom:FormStringNotEmptyValidator Message="Please input!" />
</atom:FormValidatorProvider>
</atom:FormItem.Validators>
<atom:LineEdit />
</atom:FormItem>
<atom:FormActionsItem>
<atom:SubmitButton />
</atom:FormActionsItem>
</atom:Form>
带初始值的表单
var values = new FormValues();
values.Add("username", "admin");
values.Add("remember", true);
myForm.InitialValues = values;
外部设值
var values = new FormValues();
values.Add("url", "https://example.com/");
myForm.SetFormValues(values);
监听表单事件
myForm.Submitted += (sender, args) =>
{
// args.Values 包含所有表单值
var username = args.Values["username"];
};
myForm.Validated += (sender, args) =>
{
if (args.Result == FormValidateResult.Error)
{
// args.ErrorMessages 包含验证失败的信息
}
};
myForm.ItemValueChanged += (sender, args) =>
{
// args.Item 是变化了值的 FormItem
// args.Value 是新值
};
动态添加 FormItem
var formItem = new FormItem
{
FieldName = "newField",
LabelText = "New Field",
Content = new LineEdit(),
Validators = new List<IFormValidator>
{
new FormStringNotEmptyValidator { Message = "Required!" }
}
};
myForm.Items.Insert(index, formItem);
SubmitButton 自动启用/禁用
<!-- IsWatchValidateResult="True" 时,按钮根据表单验证结果自动启用/禁用 -->
<atom:SubmitButton IsWatchValidateResult="True" />