docs(Controls): add InputControl StyleVariant and Status design specification and developer guide

This commit is contained in:
chinboy
2026-04-11 21:14:07 +08:00
parent a73efcc2a8
commit e7e71d9422
3 changed files with 900 additions and 1 deletions
+1 -1
View File
@@ -366,7 +366,7 @@ public class SimpleInput : TemplatedControl, IFormItemAware
### 4.2 完整功能的复合输入控件
一个包含完整 Form 集成的复合控件(参考 Gallery 中的 PriceInput):
一个包含完整 Form 集成的复合输入控件:
```csharp
using AtomUI;
@@ -0,0 +1,508 @@
# AtomUI 输入控件样式变体与验证状态设计原理
## 1. 概述
AtomUI 的输入控件提供两个正交的外观维度:**样式变体**(StyleVariant)控制控件的视觉风格,**验证状态**(Status)反馈数据校验结果。两者可自由组合,通过 Design Token 系统驱动全部视觉表现,并能在 Form 中自动级联传播。
这一设计对应 Ant Design 5.0 中 Input 组件的 [`variant`](https://ant.design/components/input#api) 和 [`status`](https://ant.design/components/input#api) 属性,AtomUI 将其抽象为两组独立的接口 + 共享属性 + Token 映射体系,确保所有输入类控件(包括自定义控件)能够表现一致。
### 核心设计理念
1. **正交性**:StyleVariant 和 Status 是两个独立维度,4 种变体 × 3 种状态 = 12 种组合,每种组合在主题中有完整的视觉定义。
2. **接口驱动**:通过 `IInputControlStyleVariantAware` 和 `IInputControlStatusAware` 接口标记控件支持相应能力,Form 据此实现自动级联。
3. **共享属性模式**:所有控件使用 `AddOwner<T>()` 注册同名 `StyledProperty`,确保属性名一致,级联绑定正确生效。
4. **Token 驱动视觉**:每种 Variant × Status × State(hover / focus / pressed / disabled)组合的颜色均由 Design Token 定义,无硬编码值。
5. **Form 自动级联**:Form → FormItem → 内容控件的 StyleVariant 自动向下传播;验证管线自动将 `FormValidateStatus` 映射为 `InputControlStatus`。
---
## 2. 类型定义
### 2.1 InputControlStyleVariant 枚举
定义在 `AtomUI.Controls.Shared/InputControlStyleVariant.cs`:
```csharp
public enum InputControlStyleVariant
{
Outline, // 带边框(默认)
Filled, // 填充背景
Borderless, // 无边框
Underlined // 仅下划线
}
```
| 值 | 对应 Ant Design | 视觉特征 |
|---|---|---|
| `Outline` | `outlined`(默认) | 完整边框 + 透明背景,hover 时边框变主色,focus 时边框高亮 + 阴影 |
| `Filled` | `filled` | 灰色填充背景 + 同色边框,hover 时背景加深,focus 时切换为透明背景 + 主色边框 |
| `Borderless` | `borderless` | 完全无边框无背景,最小化视觉干扰 |
| `Underlined` | — | 仅底部下划线(AtomUI 扩展),适用于极简风格表单 |
### 2.2 InputControlStatus 枚举
定义在 `AtomUI.Controls.Shared/InputControlStatus.cs`:
```csharp
public enum InputControlStatus
{
Default, // 正常状态
Warning, // 警告(黄色系)
Error // 错误(红色系)
}
```
| 值 | 对应 Ant Design | 视觉特征 |
|---|---|---|
| `Default` | 无 status 属性 | 正常边框/背景颜色 |
| `Warning` | `status="warning"` | 边框/背景切换为 Warning 色系(`ColorWarning` 系列 Token) |
| `Error` | `status="error"` | 边框/背景切换为 Error 色系(`ColorError` 系列 Token) |
### 2.3 两者的正交组合
StyleVariant × Status 构成一个 4×3 的视觉矩阵,每种组合都有完整的交互状态定义:
```
Default Warning Error
┌────────────────┬────────────────┬────────────────┐
Outline │ 灰边框 │ 黄边框 │ 红边框 │
│ hover→主色 │ hover→黄hover │ hover→红hover │
│ focus→主色+影 │ focus→黄+黄影 │ focus→红+红影 │
├────────────────┼────────────────┼────────────────┤
Filled │ 灰底 │ 黄底 │ 红底 │
│ hover→深灰 │ hover→深黄 │ hover→深红 │
│ focus→白底+主框│ focus→白底+黄框│ focus→白底+红框│
├────────────────┼────────────────┼────────────────┤
Borderless │ 透明 │ 透明 │ 透明 │
│ (无边框变化) │ (无边框变化) │ (无边框变化) │
├────────────────┼────────────────┼────────────────┤
Underlined │ 灰下划线 │ 黄下划线 │ 红下划线 │
│ hover→主色 │ hover→黄hover │ hover→红hover │
│ focus→主色 │ focus→黄色 │ focus→红色 │
└────────────────┴────────────────┴────────────────┘
```
---
## 3. 接口与共享属性
### 3.1 IInputControlStyleVariantAware
```csharp
public interface IInputControlStyleVariantAware
{
InputControlStyleVariant StyleVariant { get; set; }
}
```
标记控件支持**样式变体切换**。Form 系统通过 `is IInputControlStyleVariantAware` 类型检查来决定是否向子控件级联 `StyleVariant`。
### 3.2 InputControlStyleVariantProperty(共享属性持有者)
```csharp
public abstract class InputControlStyleVariantProperty
{
public const string StyleVariantPropertyName = "StyleVariant";
public static readonly StyledProperty<InputControlStyleVariant> StyleVariantProperty =
AvaloniaProperty.Register<StyledElement, InputControlStyleVariant>(
StyleVariantPropertyName, InputControlStyleVariant.Outline);
}
```
所有控件 **必须** 使用 `AddOwner<T>()` 注册此属性,**禁止** 自行 `Register` 同名属性:
```csharp
// ✅ 正确
public static readonly StyledProperty<InputControlStyleVariant> StyleVariantProperty =
InputControlStyleVariantProperty.StyleVariantProperty.AddOwner<MyControl>();
// ❌ 错误:自行 Register 会导致属性名相同但实例不同,级联绑定失败
public static readonly StyledProperty<InputControlStyleVariant> StyleVariantProperty =
AvaloniaProperty.Register<MyControl, InputControlStyleVariant>("StyleVariant");
```
### 3.3 IInputControlStatusAware
```csharp
public interface IInputControlStatusAware
{
InputControlStatus Status { get; set; }
}
```
标记控件支持**验证状态反馈**。当 Form 验证完成后,通过 `IFormItemAware.NotifyValidateStatus()` 通知控件更新状态。
### 3.4 InputControlStatusProperty(共享属性持有者)
```csharp
public abstract class InputControlStatusProperty
{
public const string StatusPropertyName = "Status";
public static readonly StyledProperty<InputControlStatus> StatusProperty =
AvaloniaProperty.Register<StyledElement, InputControlStatus>(
StatusPropertyName, InputControlStatus.Default);
}
```
同理,**必须** 使用 `AddOwner<T>()`:
```csharp
public static readonly StyledProperty<InputControlStatus> StatusProperty =
InputControlStatusProperty.StatusProperty.AddOwner<MyControl>();
```
### 3.5 为什么必须使用 AddOwner?
Avalonia 的属性级联绑定(`BindUtils.RelayBind`)要求父控件和子控件的 `StyledProperty` 实例来自同一个 `Register` 调用链。如果子控件自行 `Register` 了同名属性,虽然属性名相同,但 Avalonia 内部的属性标识符不同,导致:
- Form → FormItem → 子控件 的 `RelayBind` 绑定静默失败
- 在 CompactSpace 中属性传播失效
- 在 AXAML 中使用 `{TemplateBinding StyleVariant}` 时无法正确匹配
---
## 4. Token 集成
样式变体和验证状态的视觉表现完全由 Design Token 驱动。以 `AddOnDecoratedBox`(所有输入控件的通用装饰框)为例:
### 4.1 组件 Token 定义
```csharp
[ControlDesignToken]
internal class AddOnDecoratedBoxToken : AbstractControlDesignToken
{
/// <summary>
/// 悬浮态边框色
/// </summary>
public Color HoverBorderColor { get; set; }
/// <summary>
/// 激活态边框色
/// </summary>
public Color ActiveBorderColor { get; set; }
/// <summary>
/// 激活态阴影
/// </summary>
public BoxShadow ActiveShadow { get; set; }
/// <summary>
/// 错误状态时激活态阴影
/// </summary>
public BoxShadow ErrorActiveShadow { get; set; }
/// <summary>
/// 警告状态时激活态阴影
/// </summary>
public BoxShadow WarningActiveShadow { get; set; }
public override void CalculateTokenValues(bool isDarkMode)
{
base.CalculateTokenValues(isDarkMode);
ActiveBorderColor = SharedToken.ColorPrimary;
HoverBorderColor = SharedToken.ColorPrimaryHover;
ActiveShadow = new BoxShadow
{
Spread = SharedToken.ControlOutlineWidth,
Color = SharedToken.ColorControlOutline
};
ErrorActiveShadow = new BoxShadow
{
Spread = SharedToken.ControlOutlineWidth,
Color = SharedToken.ColorErrorOutline // ← Error 色系
};
WarningActiveShadow = new BoxShadow
{
Spread = SharedToken.ControlOutlineWidth,
Color = SharedToken.ColorWarningOutline // ← Warning 色系
};
}
}
```
### 4.2 全局共享 Token 映射表
各种 Status × State 组合使用的 SharedToken:
| 场景 | Token | 说明 |
|---|---|---|
| Outline 默认边框 | `ColorBorder` | 灰色边框 |
| Outline hover | `HoverBorderColor`(组件 Token,= `ColorPrimaryHover`) | 主色悬浮 |
| Outline focus/pressed | `ActiveBorderColor`(组件 Token,= `ColorPrimary`) | 主色激活 |
| Error 边框 | `ColorError` | 红色 |
| Error hover | `ColorErrorBorderHover` | 红色悬浮 |
| Error focus 阴影 | `ErrorActiveShadow`(= `ColorErrorOutline`) | 红色光晕 |
| Warning 边框 | `ColorWarning` | 黄色 |
| Warning hover | `ColorWarningBorderHover` | 黄色悬浮 |
| Warning focus 阴影 | `WarningActiveShadow`(= `ColorWarningOutline`) | 黄色光晕 |
| Filled 默认背景 | `ColorFillTertiary` | 浅灰填充 |
| Filled hover 背景 | `ColorFillSecondary` | 中灰填充 |
| Filled + Error 背景 | `ColorErrorBg` | 浅红填充 |
| Filled + Warning 背景 | `ColorWarningBg` | 浅黄填充 |
| Disabled 背景 | `ColorBgContainerDisabled` | 禁用灰色 |
---
## 5. 主题选择器模式
### 5.1 AXAML 中 StyleVariant 选择器
AtomUI 同时使用两种选择器策略:
**属性选择器**(用于主题 AXAML):
```xml
<!-- Outline 变体 -->
<Style Selector="^[StyleVariant=Outline]">
<Setter Property="BorderBrush" Value="{atom:SharedTokenResource ColorBorder}" />
</Style>
<!-- Filled 变体 -->
<Style Selector="^[StyleVariant=Filled]">
<Setter Property="Background" Value="{atom:SharedTokenResource ColorFillTertiary}" />
<Setter Property="BorderBrush" Value="{atom:SharedTokenResource ColorFillTertiary}" />
</Style>
<!-- Borderless 变体 -->
<Style Selector="^[StyleVariant=Borderless]">
<!-- 无边框、无背景 -->
</Style>
<!-- Underlined 变体 -->
<Style Selector="^[StyleVariant=Underlined]">
<Setter Property="BorderBrush" Value="{atom:SharedTokenResource ColorBorder}" />
</Style>
```
### 5.2 Status 选择器(嵌套在 StyleVariant 内)
Status 选择器 **嵌套** 在 StyleVariant 选择器内部,因为不同变体下 Status 的视觉表达方式不同:
```xml
<Style Selector="^[StyleVariant=Outline]">
<!-- Outline 默认样式 -->
<Setter Property="BorderBrush" Value="{atom:SharedTokenResource ColorBorder}" />
<!-- Outline + Error -->
<Style Selector="^[Status=Error]">
<Setter Property="BorderBrush" Value="{atom:SharedTokenResource ColorError}" />
<Style Selector="^[IsInnerBoxHover=True]">
<Setter Property="BorderBrush" Value="{atom:SharedTokenResource ColorErrorBorderHover}" />
</Style>
<Style Selector="^:focus-within">
<Setter Property="BorderBrush" Value="{atom:SharedTokenResource ColorError}" />
</Style>
</Style>
<!-- Outline + Warning -->
<Style Selector="^[Status=Warning]">
<Setter Property="BorderBrush" Value="{atom:SharedTokenResource ColorWarning}" />
<Style Selector="^[IsInnerBoxHover=True]">
<Setter Property="BorderBrush" Value="{atom:SharedTokenResource ColorWarningBorderHover}" />
</Style>
<Style Selector="^:focus-within">
<Setter Property="BorderBrush" Value="{atom:SharedTokenResource ColorWarning}" />
</Style>
</Style>
</Style>
```
### 5.3 伪类策略
部分控件同时使用属性选择器和伪类来处理 StyleVariant/Status 变化:
```csharp
// AddOnDecoratedBox 中的伪类更新
protected virtual void UpdatePseudoClasses()
{
PseudoClasses.Set(AddOnDecoratedBoxPseudoClass.Outline, StyleVariant == InputControlStyleVariant.Outline);
PseudoClasses.Set(AddOnDecoratedBoxPseudoClass.Filled, StyleVariant == InputControlStyleVariant.Filled);
PseudoClasses.Set(AddOnDecoratedBoxPseudoClass.Borderless, StyleVariant == InputControlStyleVariant.Borderless);
}
// LineEdit 中的 Status 伪类更新
PseudoClasses.Set(StdPseudoClass.Error, Status == InputControlStatus.Error);
PseudoClasses.Set(StdPseudoClass.Warning, Status == InputControlStatus.Warning);
```
> **设计选择**:主题 AXAML 中优先使用 `[StyleVariant=Outline]`、`[Status=Error]` 等属性选择器(语义更清晰);伪类(`:outline`、`:error`)主要用于控件内部需要向下游子控件传递状态的场景。
---
## 6. Form 级联传播
### 6.1 StyleVariant 级联链
Form 中的 StyleVariant 通过 `BindUtils.RelayBind` 自动向下传播:
```
Form ─── StyleVariant ──→ FormItem ─── StyleVariant ──→ Content (子控件)
└── StyleVariant ──→ FormItemDecorator ──→ Child
```
**FormItem 中的级联代码**:
```csharp
// FormItem.cs — 当 Content 变化时设置级联绑定
if (Content is IInputControlStyleVariantAware)
{
_disposables.Add(BindUtils.RelayBind(this, StyleVariantProperty, Content, StyleVariantProperty));
}
```
**FormItemDecorator 中的级联代码**:
```csharp
// FormItemDecorator.cs — 当 Child 变化时设置级联绑定
if (newChild is IInputControlStyleVariantAware)
{
_disposables.Add(BindUtils.RelayBind(this, StyleVariantProperty, newChild, StyleVariantProperty));
}
```
这意味着开发者只需在 `<atom:Form>` 上设置一次 `StyleVariant`,所有子输入控件会自动采用相同的样式变体。
### 6.2 Status 从验证管线自动设置
Status 不通过级联绑定传播,而是由 Form 验证管线通过 `IFormItemAware.NotifyValidateStatus()` 设置:
```
Form 验证管线
↓ ValidateValueAsync()
FormItem
↓ NotifyValidateStatus(FormValidateStatus.Error)
IFormItemAware.NotifyValidateStatus()
↓ 控件内部映射
SetCurrentValue(StatusProperty, InputControlStatus.Error)
```
**控件中的映射代码**(以 LineEdit 为例):
```csharp
protected override void NotifyValidateStatus(FormValidateStatus status)
{
if (status == FormValidateStatus.Error)
{
SetCurrentValue(StatusProperty, InputControlStatus.Error);
}
else if (status == FormValidateStatus.Warning)
{
SetCurrentValue(StatusProperty, InputControlStatus.Warning);
}
else
{
SetCurrentValue(StatusProperty, InputControlStatus.Default);
}
}
```
### 6.3 级联传播的设计优势
| 特性 | 说明 |
|---|---|
| **单点配置** | 在 Form 级别设置 `StyleVariant="Filled"`,所有子控件统一切换 |
| **逐级覆盖** | 单个控件可覆盖 Form 级别的 StyleVariant |
| **类型安全** | 只有实现了 `IInputControlStyleVariantAware` 的控件才会参与级联 |
| **自动清理** | 当控件从 Form 移除时,通过 `CompositeDisposable` 自动解除绑定 |
| **验证自动化** | Status 由验证管线自动设置,开发者无需手动管理 |
---
## 7. 已实现控件清单
### 7.1 同时实现 StyleVariant + Status 的控件
这些控件同时支持样式变体切换和验证状态反馈,是完整的输入控件:
| 控件 | 项目 | 说明 |
|---|---|---|
| `LineEdit` | Desktop.Controls | 单行文本输入 |
| `TextArea` | Desktop.Controls | 多行文本输入 |
| `SearchButton` | Desktop.Controls | 搜索按钮(SearchEdit 内部) |
| `ButtonSpinner` | Desktop.Controls | 带按钮的微调器 |
| `NumericUpDown` | Desktop.Controls | 数值输入 |
| `ComboBox` | Desktop.Controls | 下拉选择 |
| `AbstractSelect` | Desktop.Controls | Select 基类 |
| `Mentions` | Desktop.Controls | @提及输入 |
| `AbstractAutoComplete` | Desktop.Controls | AutoComplete 基类 |
| `InfoPickerInput` | Desktop.Controls | 日期/时间选择器输入框 |
| `AddOnDecoratedBox` | Desktop.Controls | 输入装饰框(内部组件) |
| `CompactSpaceAddOn` | Desktop.Controls | 紧凑空间装饰(内部组件) |
| `FormItemDecorator` | Desktop.Controls | 表单项装饰器 |
| `AbstractColorPicker` | Desktop.Controls.ColorPicker | 颜色选择器 |
### 7.2 仅实现 Status 的控件
| 控件 | 项目 | 说明 |
|---|---|---|
| `AbstractTransfer` | Desktop.Controls | 穿梭框基类 |
| `TransferItemDecorator` | Desktop.Controls | 穿梭框项装饰器 |
### 7.3 仅实现 StyleVariant 的控件
| 控件 | 项目 | 说明 |
|---|---|---|
| `Form` | Desktop.Controls | 表单容器(仅负责向下级联) |
---
## 8. 设计优势分析
### 8.1 与 Ant Design 的对齐
Ant Design 5.0 引入了 `variant` 属性(替代旧版的 `bordered` 布尔值),AtomUI 将其映射为类型安全的枚举,并增加了 `Underlined` 变体以适应更多场景。`status` 属性与 Ant Design 完全一致。
### 8.2 与尺寸系统的解耦
StyleVariant 和 SizeType 是完全独立的维度:
- `SizeType` 控制高度、内边距、字号
- `StyleVariant` 控制边框、背景、视觉风格
- `Status` 控制色彩反馈
三者可自由组合,例如 `SizeType="Small" StyleVariant="Filled" Status="Error"` 产生一个小号填充式红色错误输入框。
### 8.3 统一的视觉语义
所有输入控件共享同一套颜色语义:
- Error 永远使用 `ColorError` 系列 Token
- Warning 永远使用 `ColorWarning` 系列 Token
- 默认状态永远使用 `ColorPrimary` 系列 Token 作为交互色
这保证了不同控件在相同 Status 下视觉表现一致。
### 8.4 Icon 自动染色
`AddOnDecoratedBox` 在 Status 变化时自动将内部 Icon 的颜色切换为对应的状态色:
```csharp
// Warning 状态下自动将前后缀图标染为黄色
var warningStyle = new Style(x =>
x.PropertyEquals(StatusProperty, InputControlStatus.Warning));
iconStyle.Add(Icon.FillBrushProperty, SharedTokenKind.ColorWarning);
iconStyle.Add(Icon.ForegroundProperty, SharedTokenKind.ColorWarning);
```
开发者可通过 `skip-status` CSS 类排除特定图标不参与自动染色。
---
## 9. 设计限制与注意事项
### 9.1 Borderless 变体下 Status 视觉有限
Borderless 变体没有边框和背景,因此 Error/Warning 状态的视觉反馈仅体现在:
- 内部 Icon 的颜色变化
- 如果结合 Form 使用,由 FormItem 显示验证消息
### 9.2 Status 不影响 Disabled 样式
当控件处于 `:disabled` 状态时,所有 Status 的视觉反馈被覆盖为统一的禁用样式(灰色背景、灰色边框、灰色文字),Status 设置仍保留但不可见。
### 9.3 Status 与 Form 的关系
- **独立使用**:可以在 Form 外部直接设置 `Status="Error"` 手动显示错误状态。
- **Form 内使用**:验证管线自动管理 Status 值,开发者通常不需要手动设置。
- **两者冲突**:如果在 Form 内手动设置了 Status,下次验证触发时会被覆盖。
@@ -0,0 +1,391 @@
# 自定义控件接入 StyleVariant 与 Status 开发指南
## 1. 概述
本文档面向控件开发者,详细说明如何让自定义控件支持 AtomUI 的样式变体(`InputControlStyleVariant`)和验证状态(`InputControlStatus`)系统,使其与 AtomUI 内置输入控件表现一致,并能在 Form 中自动级联。
> 前置阅读:[输入控件样式变体与验证状态设计原理](./InputControlStyleAndStatusDesign.md)、[Form 集成指南](./FormIntegrationGuide.md)
---
## 2. 快速判断:你的控件需要实现哪些接口?
```
你的控件是输入类控件吗?(用户通过它输入/选择/切换值)
│
├── 是 → 建议同时实现 IInputControlStyleVariantAware 和 IInputControlStatusAware
│ │
│ ├── 控件需要 Form 验证? → 必须同时实现 IFormItemAware
│ ├── 控件需要跟随 Form 尺寸? → 实现 ISizeTypeAware
│ └── 控件需要跟随 Form 动画开关? → 实现 IMotionAwareControl
│
└── 否 → 不需要实现这两个接口
```
---
## 3. 基础实现模板
### 3.1 最小实现
以下展示了一个自定义输入控件同时支持 StyleVariant 和 Status 的最小实现:
```csharp
using AtomUI.Controls;
using Avalonia;
using Avalonia.Controls.Primitives;
namespace MyApp.Controls;
public class MyInput : TemplatedControl,
IInputControlStyleVariantAware,
IInputControlStatusAware
{
// ===== 1. 使用 AddOwner 注册共享属性 =====
public static readonly StyledProperty<InputControlStyleVariant> StyleVariantProperty =
InputControlStyleVariantProperty.StyleVariantProperty.AddOwner<MyInput>();
public static readonly StyledProperty<InputControlStatus> StatusProperty =
InputControlStatusProperty.StatusProperty.AddOwner<MyInput>();
// ===== 2. 属性包装器 =====
public InputControlStyleVariant StyleVariant
{
get => GetValue(StyleVariantProperty);
set => SetValue(StyleVariantProperty, value);
}
public InputControlStatus Status
{
get => GetValue(StatusProperty);
set => SetValue(StatusProperty, value);
}
}
```
### 3.2 完整实现(含 Form 集成)
以下是一个支持 Form 完整集成的自定义输入控件模板:
```csharp
using AtomUI;
using AtomUI.Controls;
using Avalonia;
using Avalonia.Controls.Primitives;
namespace MyApp.Controls;
public class MyInput : TemplatedControl,
IInputControlStyleVariantAware,
IInputControlStatusAware,
ISizeTypeAware,
IMotionAwareControl,
IFormItemAware
{
#region 公共属性定义
// ===== 共享属性:必须使用 AddOwner =====
public static readonly StyledProperty<InputControlStyleVariant> StyleVariantProperty =
InputControlStyleVariantProperty.StyleVariantProperty.AddOwner<MyInput>();
public static readonly StyledProperty<InputControlStatus> StatusProperty =
InputControlStatusProperty.StatusProperty.AddOwner<MyInput>();
public static readonly StyledProperty<SizeType> SizeTypeProperty =
SizeTypeControlProperty.SizeTypeProperty.AddOwner<MyInput>();
public static readonly StyledProperty<bool> IsMotionEnabledProperty =
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<MyInput>();
// ===== 控件自有属性 =====
public static readonly StyledProperty<string?> ValueProperty =
AvaloniaProperty.Register<MyInput, string?>(nameof(Value));
public InputControlStyleVariant StyleVariant
{
get => GetValue(StyleVariantProperty);
set => SetValue(StyleVariantProperty, value);
}
public InputControlStatus Status
{
get => GetValue(StatusProperty);
set => SetValue(StatusProperty, value);
}
public SizeType SizeType
{
get => GetValue(SizeTypeProperty);
set => SetValue(SizeTypeProperty, value);
}
public bool IsMotionEnabled
{
get => GetValue(IsMotionEnabledProperty);
set => SetValue(IsMotionEnabledProperty, value);
}
public string? Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
#endregion
static MyInput()
{
ValueProperty.Changed.AddClassHandler<MyInput>(
(control, _) => control.HandleValueChanged());
}
#region IFormItemAware 实现
private EventHandler? _formValueChanged;
event EventHandler? IFormItemAware.ValueChanged
{
add => _formValueChanged += value;
remove => _formValueChanged -= value;
}
void IFormItemAware.SetFormValue(object? value) => NotifySetFormValue(value?.ToString());
object? IFormItemAware.GetFormValue() => NotifyGetFormValue();
void IFormItemAware.ClearFormValue() => NotifyClearFormValue();
void IFormItemAware.NotifyValidateStatus(FormValidateStatus status) => NotifyValidateStatus(status);
private void HandleValueChanged()
{
_formValueChanged?.Invoke(this, EventArgs.Empty);
}
protected virtual void NotifySetFormValue(string? value)
{
SetCurrentValue(ValueProperty, value);
}
protected virtual string? NotifyGetFormValue() => Value;
protected virtual void NotifyClearFormValue()
{
SetCurrentValue(ValueProperty, null);
}
// ===== 关键:将 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);
}
#endregion
}
```
---
## 4. 控件模板中传递 StyleVariant 和 Status
当自定义控件的模板中包含 AtomUI 内置输入控件时,**必须** 将 StyleVariant 和 Status 通过 `{TemplateBinding}` 向下传递。
### 4.1 AXAML 模板示例
```xml
<ControlTheme x:Key="{x:Type local:MyInput}" TargetType="local:MyInput">
<Setter Property="Template">
<ControlTemplate>
<StackPanel Orientation="Horizontal" Spacing="10">
<!-- 将 StyleVariant、Status、SizeType 全部传递给内部 LineEdit -->
<atom:LineEdit SizeType="{TemplateBinding SizeType}"
IsMotionEnabled="{TemplateBinding IsMotionEnabled}"
Status="{TemplateBinding Status}"
StyleVariant="{TemplateBinding StyleVariant}"
Width="200"
Text="{TemplateBinding Value, Mode=TwoWay}" />
<!-- 非输入控件不需要传递 StyleVariant 和 Status -->
<atom:Button SizeType="{TemplateBinding SizeType}"
IsMotionEnabled="{TemplateBinding IsMotionEnabled}">
Submit
</atom:Button>
</StackPanel>
</ControlTemplate>
</Setter>
</ControlTheme>
```
### 4.2 实际示例
**验证码控件**(`Captcha`)— 组合 LineEdit + Button:
```xml
<ControlTheme x:Key="{x:Type gallery:Captcha}" TargetType="gallery:Captcha">
<Setter Property="Template">
<ControlTemplate>
<StackPanel Orientation="Horizontal" Spacing="10">
<atom:LineEdit SizeType="{TemplateBinding SizeType}"
IsMotionEnabled="{TemplateBinding IsMotionEnabled}"
Status="{TemplateBinding Status}"
StyleVariant="{TemplateBinding StyleVariant}"
Width="200"
Text="{TemplateBinding Value, Mode=TwoWay}"/>
<atom:Button SizeType="{TemplateBinding SizeType}"
IsMotionEnabled="{TemplateBinding IsMotionEnabled}">
Get captcha
</atom:Button>
</StackPanel>
</ControlTemplate>
</Setter>
</ControlTheme>
```
**价格输入控件**(`PriceInput`)— 组合 NumericUpDown + Select:
```xml
<ControlTheme x:Key="{x:Type gallery:PriceInput}" TargetType="gallery:PriceInput">
<Setter Property="Template">
<ControlTemplate>
<StackPanel Orientation="Horizontal" Spacing="10">
<atom:NumericUpDown SizeType="{TemplateBinding SizeType}"
IsMotionEnabled="{TemplateBinding IsMotionEnabled}"
Status="{TemplateBinding Status}"
StyleVariant="{TemplateBinding StyleVariant}"
Width="120"
Value="0"
Name="NumberInput"
Minimum="0"/>
<atom:Select SizeType="{TemplateBinding SizeType}"
IsMotionEnabled="{TemplateBinding IsMotionEnabled}"
Status="{TemplateBinding Status}"
StyleVariant="{TemplateBinding StyleVariant}"
DefaultValues="RMB"
Name="UnitInput">
<atom:SelectOption Header="RMB" Content="RMB"/>
<atom:SelectOption Header="Dollar" Content="Dollar"/>
</atom:Select>
</StackPanel>
</ControlTemplate>
</Setter>
</ControlTheme>
```
> **要点**:每个内部的 AtomUI 输入控件都绑定了 `SizeType`、`IsMotionEnabled`、`Status`、`StyleVariant` 四个属性。这样当 Form 级联传播这些属性到外层自定义控件时,内部子控件会自动跟随变化。
---
## 5. 在 AXAML 中独立使用
### 5.1 设置样式变体
```xml
<!-- 四种变体 -->
<atom:LineEdit PlaceholderText="Outlined" StyleVariant="Outline" />
<atom:LineEdit PlaceholderText="Filled" StyleVariant="Filled" />
<atom:LineEdit PlaceholderText="Borderless" StyleVariant="Borderless" />
<atom:LineEdit PlaceholderText="Underlined" StyleVariant="Underlined" />
```
### 5.2 设置验证状态
```xml
<!-- 手动设置 Status(不在 Form 中时) -->
<atom:LineEdit PlaceholderText="Error" Status="Error" />
<atom:LineEdit PlaceholderText="Warning" Status="Warning" />
```
### 5.3 组合使用
```xml
<!-- StyleVariant + Status 自由组合 -->
<atom:LineEdit PlaceholderText="Filled + Error"
StyleVariant="Filled"
Status="Error" />
<atom:LineEdit PlaceholderText="Underlined + Warning"
StyleVariant="Underlined"
Status="Warning" />
```
### 5.4 在 Form 中自动级联
```xml
<!-- Form 级别设置 StyleVariant,所有子控件自动跟随 -->
<atom:Form StyleVariant="Filled" SizeType="Large">
<atom:FormItem LabelText="Username" FieldName="username">
<!-- 此 LineEdit 自动采用 Filled 变体 + Large 尺寸 -->
<atom:LineEdit PlaceholderText="Enter username" />
</atom:FormItem>
<atom:FormItem LabelText="Email" FieldName="email">
<!-- 此 LineEdit 也自动采用 Filled 变体 + Large 尺寸 -->
<atom:LineEdit PlaceholderText="Enter email" />
</atom:FormItem>
</atom:Form>
```
---
## 6. NotifyValidateStatus 映射规则
当控件接入 Form 时,`NotifyValidateStatus` 是将 Form 验证结果映射到控件视觉反馈的关键方法。标准映射规则如下:
```csharp
protected virtual void NotifyValidateStatus(FormValidateStatus status)
{
var inputStatus = status switch
{
FormValidateStatus.Error => InputControlStatus.Error,
FormValidateStatus.Warning => InputControlStatus.Warning,
_ => InputControlStatus.Default
};
SetCurrentValue(StatusProperty, inputStatus);
}
```
| FormValidateStatus | InputControlStatus | 说明 |
|---|---|---|
| `Error` | `Error` | 验证失败,显示红色反馈 |
| `Warning` | `Warning` | 验证警告,显示黄色反馈 |
| `Success` | `Default` | 验证通过,恢复正常样式 |
| `Validating` | `Default` | 验证中,保持正常样式 |
| `Default` | `Default` | 初始状态 |
> **注意**:使用 `SetCurrentValue` 而非 `SetValue`,以避免覆盖用户的外部绑定。
---
## 7. 实现检查清单
### StyleVariant 接入
- [ ] 实现 `IInputControlStyleVariantAware` 接口
- [ ] 使用 `InputControlStyleVariantProperty.StyleVariantProperty.AddOwner<T>()` 注册属性(**不要** 自行 `Register`)
- [ ] 模板中的内部 AtomUI 输入控件通过 `{TemplateBinding StyleVariant}` 绑定
- [ ] 默认值为 `Outline`(由共享属性自动提供)
### Status 接入
- [ ] 实现 `IInputControlStatusAware` 接口
- [ ] 使用 `InputControlStatusProperty.StatusProperty.AddOwner<T>()` 注册属性(**不要** 自行 `Register`)
- [ ] 模板中的内部 AtomUI 输入控件通过 `{TemplateBinding Status}` 绑定
- [ ] 如果实现了 `IFormItemAware`,在 `NotifyValidateStatus` 中映射 `FormValidateStatus` → `InputControlStatus`
- [ ] 默认值为 `Default`(由共享属性自动提供)
### Form 集成
- [ ] 同时实现 `IFormItemAware`(参见 [Form 集成指南](./FormIntegrationGuide.md))
- [ ] 同时实现 `ISizeTypeAware`(使 Form 的 SizeType 能级联)
- [ ] 同时实现 `IMotionAwareControl`(使 Form 的动画开关能级联)
- [ ] 所有 `IFormItemAware` 成员使用显式接口实现
### 常见错误
| 错误 | 后果 | 解决方案 |
|---|---|---|
| 使用 `Register` 而非 `AddOwner` | Form 级联绑定静默失败 | 使用 `AddOwner<T>()` |
| 模板中遗漏 `{TemplateBinding StyleVariant}` | 内部控件不跟随变体变化 | 为所有内部输入控件添加绑定 |
| 模板中遗漏 `{TemplateBinding Status}` | 内部控件不显示验证状态 | 为所有内部输入控件添加绑定 |
| `NotifyValidateStatus` 中使用 `SetValue` | 可能覆盖外部绑定 | 使用 `SetCurrentValue` |
| 未实现 `IInputControlStyleVariantAware` 但 Form 中使用 | Form 不会级联 StyleVariant 到该控件 | 实现接口 |