实践范例:多步表单¶
向导式表单就是一个步骤索引 UseState 加上一个决定渲染哪一步的
switch。字段状态与步骤索引活在同一个组件里,因此前进与后退绝不会丢掉用户
输入的内容 —— 当可见槽位变化时,Microsoft.UI.Reactor(Reactor)不会卸载外围组件。
原语¶
| 关注点 | API |
|---|---|
| 步骤索引 | UseState<int> |
| 逐字段状态 | UseState<string> / UseState<int> / UseState<bool> |
| 步骤分支 | 对步骤索引做 switch,返回 Element |
| 前进门控 | 「下一步」按钮上的 .IsEnabled(canAdvance) |
| 输入控件 | TextBox、RadioButtons、CheckBox |
状态¶
// 步骤索引一个 UseState,每个字段各一个。这些字段声明在
// Render 顶部,因此能挺过每一次步骤切换 —— Reactor 从不卸载
// Wizard,所以在用户前进后退时这些 Hook 会保有自己的值。
var (step, setStep) = UseState(0);
var (name, setName) = UseState("");
var (email, setEmail) = UseState("");
var (role, setRole) = UseState(-1);
var (newsletter, setNewsletter) = UseState(false);
步骤索引与所有字段 Hook 都位于 Render 顶部。可见步骤变化时它们一个都不会被
拆除;被换掉的只是 switch 返回的那个槽位。这就是为什么用户可以从第 2 步
退回第 1 步、改一个字段,再前进时第 2 步的选择依然还在。
逐步校验¶
// canAdvance 是步骤索引与当前字段值的纯函数。下一步按钮
// 直接绑定它;没有防抖,也没有单独的校验轮次。
bool canAdvance = step switch
{
0 => name.Trim().Length >= 2
&& email.Contains('@') && email.Contains('.'),
1 => role >= 0,
_ => true,
};
canAdvance 是当前步骤与字段值的纯函数。它在每次渲染时运行 —— 没有防抖,
没有校验轮次 —— 而「下一步」按钮通过 .IsEnabled(canAdvance) 直接绑定它。
第 0 步要求姓名和一个像样的邮箱;第 1 步要求选定一个角色;
第 2 步(摘要)总是可以前进,因为唯一的向前动作就是提交。
各步骤主体¶
Element StepAccount() => VStack(10,
SubHeading("Step 1 of 3 — Account"),
TextBox(name, setName, placeholderText: "Your name",
header: "Name").Width(340),
TextBox(email, setEmail, placeholderText: "you@example.com",
header: "Email").Width(340)
);
每一步都是一个返回 Element 的局部函数。账户步骤收集文本字段;
第二步收集单选按钮 + 复选框:
Element StepPreferences() => VStack(10,
SubHeading("Step 2 of 3 — Preferences"),
TextBlock("Pick the role that fits best.").Opacity(0.7),
RadioButtons(new[] { "Engineer", "Designer", "Manager" },
role, setRole),
CheckBox(newsletter, setNewsletter,
label: "Send me product updates")
);
这些局部函数闭包捕获了 Render 顶部的 Hook setter,因此
表单 里的受控输入契约直接就能用 —— 值进、setter 出,
与单页表单形态相同。
编排器¶
// 编排器决定渲染哪一步,然后把返回 / 下一步按钮摆在下面。
// 第 0 步时返回按钮禁用;在 canAdvance 之前下一步一直禁用;
// 最后一步把下一步的标签换成「提交」。
Element body = step switch
{
0 => StepAccount(),
1 => StepPreferences(),
_ => StepSummary(),
};
return VStack(16,
Heading("Create your account"),
body,
HStack(8,
Button("Back", () => setStep(step - 1)).IsEnabled(step != 0),
Button(step == 2 ? "Submit" : "Next",
() => setStep(step + 1))
.AutomationName(step == 2 ? "Submit form" : "Next step")
.IsEnabled(canAdvance && step != 2)
)
).Padding(20).Width(380);

编排器就是对 step 的一个 switch 加上一排「返回 / 下一步」。
第 0 步时返回按钮禁用;在 canAdvance 之前下一步按钮禁用;
到最后一步,下一步按钮换成「提交」并收敛(真实应用会在这里
调用它的提交路径)。
提示¶
步骤索引就是整台状态机。 向导不需要路由库,不需要逐步的
Component,也不需要 WizardController。一个 UseState<int> 加一个 switch
就是最正统的形态;其余都是装饰。
把所有字段 Hook 声明在顶部,而不是步骤内部。 每次渲染都必须以相同的顺序
调用 Hook(Hook 规则)。把 UseState 放进某个步骤分支
会导致在该步骤不活跃的渲染中跳过这个 Hook,从而破坏
Hook 顺序。声明在顶层也保证了这些值能挺过步骤切换。
在按钮处把关前进,而不是在处理函数内部。 禁用的下一步按钮是 用户看得见的一次渲染检查;而点击处理函数内部的守卫是在用户已经按下 一个被 UI 显示为就绪的东西之后才运行的。渲染即禁用才是 承重的那一层。
三步向导不要真的引入 导航。 导航负责在顶层
界面(页面、对话框)之间路由;而向导是一个内部自我变化的界面。只有当某一步
需要自己的 URL、自己的回退栈条目或自己的深链接时,才该动用真正的路由。