名称: dotnet-accessibility 描述: “实现可访问的 .NET UI。语义属性、ARIA、自动化对等体、每个平台的测试。” 用户可调用: false
dotnet-accessibility
跨平台的 .NET UI 框架可访问性模式:语义标记、键盘导航、焦点管理、颜色对比和屏幕阅读器集成。深入覆盖 Blazor(HTML ARIA)、MAUI(语义属性)和 WinUI(自动化属性/UI 自动化)。为 WPF、Uno 平台和 TUI 框架提供简要指导与交叉参考。
范围
- 跨平台可访问性原则(语义标记、键盘导航、焦点、对比度)
- Blazor 可访问性(HTML ARIA 属性、屏幕阅读器模式)
- MAUI 可访问性(语义属性、平台特定设置)
- WinUI 可访问性(自动化属性、UI 自动化、自动化对等体)
- WPF、Uno 平台和 TUI 可访问性指导与交叉参考
超出范围
- 框架项目设置 – 参见各个框架技能
- 法律合规建议(引用 WCAG 但不提供法律指导)
- UI 框架选择 – 参见 [技能:dotnet-ui-选择器]
交叉参考:[技能:dotnet-blazor-模式] 用于 Blazor 托管和渲染模式,[技能:dotnet-blazor-组件] 用于 Blazor 组件生命周期,[技能:dotnet-maui-开发] 用于 MAUI 模式,[技能:dotnet-winui] 用于 WinUI 3 模式,[技能:dotnet-wpf-现代] 用于 .NET 8+ 上的 WPF,[技能:dotnet-uno-平台] 用于 Uno 平台模式,[技能:dotnet-终端-gui] 用于 Terminal.Gui,[技能:dotnet-spectre-控制台] 用于 Spectre.Console,[技能:dotnet-ui-选择器] 用于框架选择。
跨平台原则
这些原则适用于所有 .NET UI 框架。框架特定实现在后续部分中。
语义标记
为所有交互式和信息元素提供有意义的名称和描述。屏幕阅读器依赖于语义元数据 – 而不是视觉外观 – 来传达 UI 结构。
- 每个交互式控件必须有一个可访问名称(文本标签、ARIA 标签或自动化属性)
- 图像和图标必须有描述其目的的文本替代
- 装饰性元素应从可访问性树中隐藏
- 逻辑分组相关控件,以便屏幕阅读器在上下文中宣布它们
键盘导航
所有功能必须仅通过键盘即可操作。不能使用鼠标、指针或触摸的用户完全依赖于键盘交互。
- 维持一个遵循视觉阅读流程的逻辑选项卡顺序
- 在所有交互式元素上提供可见焦点指示器
- 支持标准键盘模式:Tab/Shift+Tab 用于导航,Enter/Space 用于激活,Escape 用于关闭,箭头键在复合控件内
- 避免键盘陷阱 – 用户必须能够从每个控件导航离开
焦点管理
程序化焦点管理确保屏幕阅读器正确宣布上下文更改。
- 将焦点移动到新揭示的内容(对话框、展开面板、内联通知)
- 当关闭覆盖时,将焦点返回到触发元素
- 在后台更新期间避免意外窃取焦点
- 当页面或对话框加载时,在主要操作上设置初始焦点
颜色对比
确保文本和交互式元素满足 WCAG 对比度比率。
| 元素类型 | 最小比率(WCAG AA) | 增强比率(WCAG AAA) |
|---|---|---|
| 普通文本(< 18pt) | 4.5:1 | 7:1 |
| 大文本(>= 18pt 或 14pt 粗体) | 3:1 | 4.5:1 |
| UI 组件和图形对象 | 3:1 | 3:1 |
- 不要仅依靠颜色来传达信息(使用图标、模式或文本标签作为补充)
- 支持高对比度主题和系统颜色覆盖
- 使用色盲模拟工具进行测试
Blazor 可访问性(深度)
Blazor 渲染 HTML,因此标准 web 可访问性模式适用。使用原生 HTML 语义和 ARIA 属性来构建可访问的 Blazor 应用。
语义 HTML 和 ARIA
@* 使用语义 HTML 元素进行结构 *@
<nav aria-label="主导航">
<ul>
<li><a href="/products">产品</a></li>
<li><a href="/about">关于</a></li>
</ul>
</nav>
<main>
<h1>产品目录</h1>
@* 带有 alt 文本的图像 *@
<img src="hero.png" alt="产品展示显示三个特色项目" />
@* 从可访问性树隐藏的装饰性图像 *@
<img src="divider.svg" alt="" role="presentation" />
@* 按钮,可访问名称来自内容 *@
<button @onclick="添加到购物车">添加到购物车</button>
@* 图标按钮需要 aria-label *@
<button @onclick="切换收藏" aria-label="添加到收藏">
<span class="icon-heart" aria-hidden="true"></span>
</button>
</main>
键盘事件处理
<div role="listbox"
tabindex="0"
aria-label="产品列表"
aria-activedescendant="@_activeId"
@onkeydown="处理按键"
@onkeydown:preventDefault>
@foreach (var product in 产品列表)
{
<div id="@($"product-{product.Id}")"
role="option"
aria-selected="@(product.Id == 选中Id)"
@onclick="() => 选择(product)">
@product.名称
</div>
}
</div>
@code {
private string _activeId = "";
private void 处理按键(KeyboardEventArgs e)
{
switch (e.Key)
{
case "ArrowDown":
移动选择(1);
break;
case "ArrowUp":
移动选择(-1);
break;
case "Enter":
case " ":
确认选择();
break;
}
}
}
实时区域
在不移动焦点的情况下,向屏幕阅读器宣布动态内容更改:
@* 礼貌:在当前讲话结束后宣布 *@
<div aria-live="polite" aria-atomic="true">
@if (_statusMessage is not null)
{
<p>@_statusMessage</p>
}
</div>
@* 断言:中断当前讲话(谨慎使用) *@
<div aria-live="assertive" role="alert">
@if (_errorMessage is not null)
{
<p>@_errorMessage</p>
}
</div>
表单可访问性
<EditForm Model="@_model" OnValidSubmit="处理提交">
<DataAnnotationsValidator />
<div>
<label for="product-name">产品名称</label>
<InputText id="product-name"
@bind-Value="_model.名称"
aria-describedby="name-error"
aria-invalid="@(_nameInvalid ? "true" : null)" />
<ValidationMessage For="() => _model.名称" id="name-error" />
</div>
<div>
<label for="quantity">数量</label>
<InputNumber id="quantity"
@bind-Value="_model.数量"
aria-describedby="quantity-help"
min="1" max="100" />
<span id="quantity-help">输入 1 到 100 之间的值</span>
</div>
<button type="submit">提交订单</button>
</EditForm>
对于 Blazor 托管模型和渲染模式配置,参见 [技能:dotnet-blazor-模式]。对于组件生命周期和 EditForm 模式,参见 [技能:dotnet-blazor-组件]。
MAUI 可访问性(深度)
MAUI 提供 SemanticProperties 附加属性作为推荐的可访问性 API。这些映射到原生平台可访问性 API(iOS/macOS 上的 VoiceOver,Android 上的 TalkBack,Windows 上的 Narrator)。
SemanticProperties
<!-- 描述:主要屏幕阅读器宣布 -->
<Image Source="product.png"
SemanticProperties.Description="产品照片显示蓝色小部件" />
<!-- 提示:关于操作的额外上下文 -->
<Button Text="添加到购物车"
SemanticProperties.Hint="将当前产品添加到您的购物车" />
<!-- HeadingLevel:启用基于标题的导航 -->
<Label Text="订单摘要"
SemanticProperties.HeadingLevel="Level1" />
<Label Text="项目"
SemanticProperties.HeadingLevel="Level2" />
关键 API:
SemanticProperties.Description– 屏幕阅读器宣布的短文本(相当于 iOS 上的accessibilityLabel,Android 上的contentDescription)SemanticProperties.Hint– 额外目的上下文(相当于 iOS 上的accessibilityHint)SemanticProperties.HeadingLevel– 标记标题(Level1 到 Level9);Android 和 iOS 只支持单一标题级别,Windows 支持所有 9 个
平台警告: 不要在 Label 上设置 SemanticProperties.Description – 它会覆盖屏幕阅读器的 Text 属性,导致视觉和口头文本不匹配。不要在 Android 上的 Entry 或 Editor 上设置 SemanticProperties.Description – 使用 Placeholder 或 SemanticProperties.Hint 替代,因为 Description 与 TalkBack 操作冲突。
遗留 AutomationProperties
AutomationProperties 是较旧的 Xamarin.Forms API,在 MAUI 中被 SemanticProperties 取代。对于新代码使用 SemanticProperties。
| 遗留 API | 替换 |
|---|---|
AutomationProperties.Name |
SemanticProperties.Description |
AutomationProperties.HelpText |
SemanticProperties.Hint |
AutomationProperties.LabeledBy |
绑定 SemanticProperties.Description 到标签的 Text |
AutomationProperties.IsInAccessibleTree 和 AutomationProperties.ExcludedWithChildren 对于控制可访问性树包含仍然有用。
程序化焦点和宣布
// 将屏幕阅读器焦点移动到特定元素
myLabel.SetSemanticFocus();
// 在不移动焦点的情况下,向屏幕阅读器宣布文本
SemanticScreenReader.Default.Announce("成功将项目添加到购物车。");
可访问的自定义控件
构建自定义控件时,确保设置可访问性元数据:
public class RatingControl : ContentView
{
private int _rating;
public int Rating
{
get => _rating;
set
{
_rating = value;
SemanticProperties.SetDescription(this,
$"评分:{value} 颗星,共 5 颗");
SemanticScreenReader.Default.Announce(
$"评分更改为 {value} 颗星");
}
}
}
对于 MAUI 项目结构、MVVM 模式和平台服务,参见 [技能:dotnet-maui-开发]。
WinUI 可访问性(深度)
WinUI 3 / Windows App SDK 基于 Microsoft UI 自动化框架。内置控件默认包括自动化支持。自定义控件需要自动化对等体。
AutomationProperties
<!-- 名称:屏幕阅读器的主要可访问名称 -->
<Image Source="ms-appx:///Assets/product.png"
AutomationProperties.Name="产品照片显示蓝色小部件" />
<!-- HelpText:补充描述 -->
<Button Content="添加到购物车"
AutomationProperties.HelpText="将当前产品添加到您的购物车" />
<!-- LabeledBy:将标签与控件关联 -->
<TextBlock x:Name="QuantityLabel" Text="数量:" />
<NumberBox AutomationProperties.LabeledBy="{x:Bind QuantityLabel}"
Value="{x:Bind ViewModel.Quantity, Mode=TwoWay}" />
<!-- 从可访问性树隐藏装饰性元素 -->
<Image Source="ms-appx:///Assets/divider.png"
AutomationProperties.AccessibilityView="Raw" />
自定义自动化对等体
对于自定义控件,实现 AutomationPeer 以将控件暴露给 UI 自动化客户端:
// 自定义控件
public sealed class StarRating : Control
{
public int Value
{
get => (int)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value), typeof(int),
typeof(StarRating), new PropertyMetadata(0, OnValueChanged));
private static void OnValueChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
if (FrameworkElementAutomationPeer
.FromElement((StarRating)d) is StarRatingAutomationPeer peer)
{
peer.RaiseValueChanged((int)e.OldValue, (int)e.NewValue);
}
}
protected override AutomationPeer OnCreateAutomationPeer()
=> new StarRatingAutomationPeer(this);
}
// 自动化对等体(使用 Microsoft.UI.Xaml.Automation.Provider)
public sealed class StarRatingAutomationPeer
: FrameworkElementAutomationPeer, IRangeValueProvider
{
private StarRating Owner => (StarRating)base.Owner;
public StarRatingAutomationPeer(StarRating owner) : base(owner) { }
protected override string GetClassNameCore() => nameof(StarRating);
protected override string GetNameCore()
=> $"评分:{Owner.Value} 颗星,共 5 颗";
protected override AutomationControlType GetAutomationControlTypeCore()
=> AutomationControlType.Slider;
// IRangeValueProvider
public double Value => Owner.Value;
public double Minimum => 0;
public double Maximum => 5;
public double SmallChange => 1;
public double LargeChange => 1;
public bool IsReadOnly => false;
public void SetValue(double value)
=> Owner.Value = (int)Math.Clamp(value, Minimum, Maximum);
public void RaiseValueChanged(int oldValue, int newValue)
{
RaisePropertyChangedEvent(
RangeValuePatternIdentifiers.ValueProperty,
(double)oldValue, (double)newValue);
}
}
WinUI 中的键盘可访问性
WinUI XAML 控件提供内置键盘支持。确保自定义控件遵循相同模式:
<!-- TabIndex 控制导航顺序 -->
<TextBox Header="名" TabIndex="1" />
<TextBox Header="姓" TabIndex="2" />
<Button Content="提交" TabIndex="3" />
<!-- AccessKey 提供键盘快捷键(Alt + 键) -->
<Button Content="保存" AccessKey="S" />
<Button Content="删除" AccessKey="D" />
对于 WinUI 项目设置、XAML 模式和 Windows 集成,参见 [技能:dotnet-winui]。
WPF 可访问性(简要)
.NET 8+ 上的 WPF 使用与 WinUI 相同的 UI 自动化框架。API 几乎相同,但命名空间不同。
AutomationProperties.Name、AutomationProperties.HelpText、AutomationProperties.LabeledBy与 WinUI 中的相同- 自定义控件覆盖
OnCreateAutomationPeer()并返回FrameworkElementAutomationPeer子类 - WPF Fluent 主题(.NET 9+)自动包括高对比度支持
- 使用
AutomationProperties.LiveSetting进行实时区域宣布
<!-- WPF 可访问性遵循与 WinUI 相同的模式 -->
<Image Source="product.png"
AutomationProperties.Name="产品照片" />
<TextBlock x:Name="StatusLabel"
AutomationProperties.LiveSetting="Polite"
Text="{Binding StatusText}" />
对于 .NET 8+ 上的 WPF 开发模式,参见 [技能:dotnet-wpf-现代]。
Uno 平台可访问性(简要)
Uno 平台遵循 UWP/WinUI AutomationProperties 模式,因为其 API 表面是 WinUI 兼容的。
AutomationProperties.Name、AutomationProperties.HelpText、AutomationProperties.LabeledBy跨平台工作- 自定义
AutomationPeer实现遵循 WinUI 模式 - 在 WebAssembly 上,Uno 自动将
AutomationProperties映射到 HTML ARIA 属性 - 平台特定行为可能不同 – 在每个目标(Windows、iOS、Android、WASM)上进行测试
对于 Uno 平台开发模式,参见 [技能:dotnet-uno-平台]。对于每个目标的部署和测试,参见 [技能:dotnet-uno-目标]。
TUI 可访问性(简要)
终端 UI 框架具有固有的可访问性限制。屏幕阅读器支持取决于终端仿真器和操作系统。
Terminal.Gui(v2):
- 屏幕阅读器可以通过终端仿真器的可访问性支持读取终端文本内容
- 没有等同于 ARIA 或 AutomationProperties 的程序化可访问性 API
- 逻辑选项卡顺序遵循视图上的
TabIndex属性 - 高对比度由终端颜色主题管理,而不是应用程序
Spectre.Console:
- 只输出库 – 屏幕阅读器直接读取终端文本缓冲区
- 当可访问性关键时,使用纯文本回退用于复杂视觉元素(表格、树)
AnsiConsole.Profile.Capabilities可以检测终端功能但不能检测屏幕阅读器存在
诚实约束: TUI 应用程序不能程序化控制屏幕阅读器行为。终端仿真器提供不同程度的可访问性支持。对于可访问性是硬性要求的应用程序,考虑使用 GUI 框架(Blazor、MAUI、WinUI)替代。
对于 Terminal.Gui 模式,参见 [技能:dotnet-终端-gui]。对于 Spectre.Console 模式,参见 [技能:dotnet-spectre-控制台]。
可访问性测试工具
每个平台的测试
| 平台 | 主要工具 | 次要工具 |
|---|---|---|
| Windows | Accessibility Insights for Windows | Narrator(Win+Ctrl+Enter),Inspect.exe(Windows SDK) |
| Web(Blazor) | axe-core / axe DevTools | Lighthouse(Chrome),WAVE,NVDA,VoiceOver(macOS) |
| Android | Accessibility Scanner | TalkBack,Android Studio Layout Inspector |
| iOS / macOS | Accessibility Inspector(Xcode) | VoiceOver(内置),XCUITest 可访问性断言 |
自动化测试集成
// Blazor:将 axe-core 与 Playwright 集成用于自动化可访问性测试
// 需要:Deque.AxeCore.Playwright NuGet 包
// 安装:dotnet add package Deque.AxeCore.Playwright
var axeResults = await new Deque.AxeCore.Playwright.AxeBuilder(page)
.AnalyzeAsync();
// 检查违规
Assert.Empty(axeResults.Violations);
// WinUI/WPF:在 CI 管道中使用 Accessibility Insights for Windows CLI
// 需要:AccessibilityInsights.CLI(可通过 Microsoft Store 或直接下载获得)
手动测试清单
- 仅键盘导航 – 通过键盘无鼠标选项卡遍历整个应用;验证所有功能可达
- 屏幕阅读器演练 – 启用 Narrator/VoiceOver/TalkBack 并导航完整工作流程
- 高对比度 – 启用系统高对比度主题并验证所有内容仍可见
- 缩放/缩放 – 增加文本大小到 200% 并验证布局不破裂或剪辑内容
- 颜色对比 – 验证所有文本和交互式元素满足 WCAG AA 比率(文本 4.5:1,大文本和 UI 组件 3:1)
WCAG 参考
此技能参考 Web 内容可访问性指南(WCAG) 作为全球可访问性标准。WCAG 2.1 是当前基线;WCAG 2.2 增加了移动和认知可访问性的额外标准。
四个原则(POUR):
- 可感知 – 信息必须以所有用户可感知的方式呈现
- 可操作 – UI 组件必须对所有用户可操作
- 可理解 – 信息和 UI 操作必须可理解
- 稳健 – 内容必须足够稳健以与辅助技术协同工作
符合级别: A(最小),AA(大多数应用的推荐目标),AAA(增强)。大多数法律要求和行业标准针对 WCAG 2.1 级别 AA。
注意: 此技能提供技术实施指导。不构成关于可访问性合规要求的法律建议,这些要求因管辖区和应用类型而异。
代理注意事项
- 不要在 MAUI
Label控件上设置SemanticProperties.Description。 它会覆盖屏幕阅读器的Text属性,导致视觉和口头内容不匹配。标签已经通过其Text属性可访问。 - 不要在 Android 上的 MAUI
Entry/Editor上设置SemanticProperties.Description。 使用Placeholder或SemanticProperties.Hint替代 –Description与这些控件的 TalkBack 操作冲突。 - 对于新的 MAUI 代码,不要使用
AutomationProperties.Name或AutomationProperties.HelpText。 使用SemanticProperties替代(MAUI 原生 API)。AutomationProperties.IsInAccessibleTree和ExcludedWithChildren对于控制可访问性树包含仍然有效。 - 不要在仅图标的 Blazor 按钮上省略
aria-label。 没有可见文本内容的按钮对屏幕阅读器不可见,除非设置aria-label或aria-labelledby。 - 不要将
aria-live="assertive"用于常规状态更新。 断言会立即中断屏幕阅读器。使用aria-live="polite"用于非关键更新;保留断言用于错误和时间关键警报。 - 不要假设 TUI 应用默认可访问。 终端屏幕阅读器支持因仿真器和 OS 而异。对于关键可访问性场景,始终提供替代输出格式。
- 不要在不验证对比度比率的情况下硬编码颜色。 使用工具(Accessibility Insights,Lighthouse)验证 WCAG AA 合规性。也必须测试系统高对比度主题。
- 不要忘记在频繁使用的 WinUI/WPF 按钮上添加
AccessKey。 访问键(Alt+键 快捷键)对于键盘依赖用户至关重要,添加起来很简单。
前提条件
- .NET 8.0+(所有框架的基线)
- 框架特定 SDK:MAUI 工作负载,Windows App SDK(WinUI),Blazor 项目模板
- 测试工具:Accessibility Insights(Windows),axe-core(web),Xcode Accessibility Inspector(macOS/iOS)
- 手动测试屏幕阅读器:Narrator(Windows),VoiceOver(macOS/iOS),TalkBack(Android),NVDA(Windows,免费)