绑定语法¶
属性路径从当前数据上下文读取值;表达式可以进一步组合属性值、字面量和运算符,用于显示计算结果或判断条件。表达式随其依赖的数据变化通知重新求值。
声明表达式¶
- 文本插值使用双括号
{{ ... }},可以与固定文字混合。 - 绑定属性使用单括号
{...},例如@text、@value和@style.opacity。
<p>Health: {{ Health }} / {{ MaxHealth }}</p>
<span @text="{Health + Shield}"></span>
假设 Health 为 80、MaxHealth 为 100、Shield 为 20。
输出:
<p>Health: 80 / 100</p>
<span>100</span>
表达式中的属性名、作用域名称和字符串内容区分大小写。HTML 中的静态文本和普通属性值不会自动作为绑定表达式求值。
属性路径¶
当前上下文与多级属性¶
不带前缀的名称从当前数据上下文读取。点号用于继续访问前一级对象的属性:
<p>{{ Name }}</p>
<p>{{ Weapon.Name }}</p>
<p>{{ Weapon.Owner.Name }}</p>
假设玩家名称为 Alice、武器名称为 Sword,武器所有者名称为 Alice。
输出:
<p>Alice</p>
<p>Sword</p>
<p>Alice</p>
Weapon.Owner.Name 依次读取当前对象的 Weapon、武器的 Owner 和所有者的 Name。@this 明确表示当前数据上下文,因此 Name 与 @this.Name 含义相同。
切换上下文后,路径从新上下文开始:
<section @context="{Weapon}">
<p>{{ Name }}</p>
<p>{{ Owner.Name }}</p>
</section>
输出:
<section>
<p>Sword</p>
<p>Alice</p>
</section>
这里的 Name 是武器名称。路径中的对象替换及属性通知见基础绑定。
固定索引与键¶
Array 使用从 0 开始的整数下标;Map 使用与实际键类型一致的键:
<p>{{ Items[0].Name }}</p>
<p>{{ Stats['Health'] }}</p>
<p>{{ PlayersById[1001].Name }}</p>
假设第一项名称为 Sword,Stats 的 Health 键对应 80,编号 1001 的玩家名称为 Alice。
输出:
<p>Sword</p>
<p>80</p>
<p>Alice</p>
Map 的整数键表示键值,不代表遍历位置。字符串键应使用单引号;Stats['Health'] 中的 Health 是固定键,不是数据上下文的属性。
Set 不支持下标访问
Tags[0] 等路径无效。Set 应通过循环读取数据项,不能根据遍历序号反向使用下标查找。各集合的访问规则见集合绑定。
动态索引与键¶
方括号中也可以使用属性或表达式,按当前值选择数据项:
<p>{{ Items[SelectedIndex].Name }}</p>
<p>{{ Items[SelectedIndex + 1].Name }}</p>
<p>{{ Stats[SelectedStat] }}</p>
假设 SelectedIndex 为 0,前两项名称依次为 Sword、Potion;SelectedStat 为 Health,对应的值为 80。
输出:
<p>Sword</p>
<p>Potion</p>
<p>80</p>
SelectedIndex 提供整数下标,SelectedStat 提供 Map 的键。它们发生变化并通知绑定后,路径会重新读取选中的数据项。集合变化及选中项属性变化也需要相应通知。
动态索引不会改变容器的访问规则:Array 不接受字符串下标,Map 的键仍须与实际类型匹配,Set 仍不支持索引。
字面量¶
| 类别 | 写法 | 说明 |
|---|---|---|
| 整数 | 0、100、-10 |
十进制整数 |
| 浮点数 | 0.5、1.25、1e3、2.5e-2 |
小数或科学计数法 |
| 字符串 | 'Alice'、'' |
使用单引号,'' 是空字符串 |
| 布尔值 | true、false |
使用小写形式 |
| 空值 | null |
表示没有值 |
字符转义¶
普通文本、表达式字符串和 HTML 属性使用不同的转义规则。以下代码均表示 HTML 文件中的实际内容;如果再将其写入 C++ 或 C# 字符串,还需遵守对应语言的字符串规则。
表达式字符串中的转义¶
转义字符:
| 转义 | 含义 |
|---|---|
\' |
单引号 |
\\ |
反斜杠 |
\n |
换行 |
\r |
回车 |
\t |
制表符 |
<p>{{ 'Player: ' + Name }}</p>
<span @text="{'Player\'s weapon: ' + Weapon.Name}"></span>
假设 Name 为 Alice,Weapon.Name 为 Sword。
输出:
<p>Player: Alice</p>
<span>Player's weapon: Sword</span>
表达式中的字符串必须用单引号包裹,不能用双引号或反引号包裹。字符串内容可以包含双引号;写在双引号包裹的 HTML 属性中时,应将内容中的双引号写为 "。Unicode 字符可以直接输入,不支持 \uXXXX 转义。
<span @text="{'Path: C:\\temp'}"></span>
输出:
<span>Path: C:\temp</span>
这里字符串内的反斜杠需要写成 \\,与普通文本直接写 C:\temp 的规则不同。
双引号处理¶
表达式字符串使用单引号包裹,内容中的双引号可以直接写入,无需反斜杠转义:
<!-- Name = "Alice" -->
<p>{{ '"Player: ' + Name + '"' }}</p>
输出:
<p>"Player: Alice"</p>
如果表达式写在双引号包裹的 HTML 属性中,内容中的双引号应写成 ",避免提前结束属性值:
<!-- Name = "Alice" -->
<span @text="{'"Player: ' + Name + '"'}"></span>
输出:
<span>"Player: Alice"</span>
HTML 先将 " 解码为双引号,再解析表达式。不要使用 \" 代替 HTML 实体:反斜杠不能转义 HTML 属性的结束引号,表达式字符串也不支持 \" 转义。
普通文本中的大括号¶
单个 {、} 是普通文字,无需转义。只有 {{ 会开始文本插值;需要原样显示它时,在前面添加反斜杠,写为 \{{:
<p>Object: { Name, Health }</p>
<p>Syntax: \{{ Name }}</p>
输出:
<p>Object: { Name, Health }</p>
<p>Syntax: {{ Name }}</p>
未参与插值结束的 }} 可以直接显示,不必额外转义。\{{ 只转义紧随其后的起始标记,不会使后续整段文字停止识别插值。
转义文本与插值混合¶
假设 Name 为 Alice,同一段文字中可以同时显示插值语法和实际绑定值:
<p>Literal: \{{ Name }}; value: {{ Name }}</p>
输出:
<p>Literal: {{ Name }}; value: Alice</p>
需要在绑定结果外侧显示单个大括号时,可将字面大括号与插值分开:
<p>{ {{ Name }} }</p>
输出:
<p>{ Alice }</p>
也可以在表达式内部使用字符串拼接:
<p>{{ '{' + Name + '}' }}</p>
<span @text="{'{' + Name + '}'}"></span>
输出:
<p>{Alice}</p>
<span>{Alice}</span>
字符串中的大括号是文字,不是新的绑定边界,不需要写成 \{ 或 \}。
插值前的反斜杠¶
只有紧邻 {{ 的连续反斜杠按插值转义规则处理:每两个反斜杠输出一个字面反斜杠,剩下一个时转义 {{。仍假设 Name 为 Alice:
| HTML 文本内容 | 最终显示 |
|---|---|
\{{ Name }} |
{{ Name }} |
\\{{ Name }} |
\Alice |
\\\{{ Name }} |
\{{ Name }} |
其他普通文本中的反斜杠保持原样。例如,C:\temp 不会将 \t 解释成制表符。普通文本中的 @、@@ 也保持原样,不使用 @@ 表示单个 @。
HTML 实体与模板转义¶
属性表达式中的 <、& 等字符推荐使用运算符别名,避免与 HTML/XML 标记冲突:
<p @if="{Health :lt: MaxHealth :and: !IsDead}">Needs healing</p>
假设 Health 为 80、MaxHealth 为 100、IsDead 为 false。
输出:
<p>Needs healing</p>
可以使用标准 HTML 转义字符,但不推荐
HTML 实体先解码,之后才解析表达式,因此也可以使用 <、& 等标准 HTML 转义字符。下面的写法与运算符别名等价,但可读性较差,推荐使用 :lt:、:and: 等别名。
<p @if="{Health < MaxHealth && !IsDead}">Needs healing</p>
等效的内置运算符别名写法:
<p @if="{Health :lt: MaxHealth :and: !IsDead}">Needs healing</p>
使用上面的示例数据,输出:
<p>Needs healing</p>
XML 兼容模板的完整规则见XHTML 支持。
字符实体不能阻止插值
{{ Name }} 解码后仍是 {{ Name }},会读取 Name,不会原样显示插值语法。需要显示字面插值标记时,应使用 \{{ Name }}。
绑定结果不再次解析¶
普通文本和属性绑定的结果是数据,不会再次扫描其中的模板语法。例如,Name 的实际内容为 {{ OtherName }} 时:
<!-- Name = "{{ OtherName }}" -->
<p>{{ Name }}</p>
输出:
<p>{{ OtherName }}</p>
其中 {{ OtherName }} 是字面文本,不会继续读取 OtherName。此时无需在业务数据中额外添加模板转义。显式的动态 HTML 绑定按其自身规则处理,不属于普通文本绑定。
运算符¶
数字、字符串、颜色、向量和矩阵各自支持的运算不同。以下介绍通用语法;完整类型范围及向量、矩阵算术见数据类型。
算术运算符¶
| 运算符 | 含义 | 示例 |
|---|---|---|
+ |
加法;任一操作数为字符串时进行字符串拼接 | Health + Shield、'HP: ' + Health |
- |
减法或一元负号 | MaxHealth - Health、-Offset |
* |
乘法 | Price * Quantity |
/ |
除法 | Health / MaxHealth |
% |
取余 | SelectedIndex % 2 |
<p>Total: {{ Price * Quantity }}</p>
<p>{{ 'HP: ' + Health + '/' + MaxHealth }}</p>
假设 Price 为 10、Quantity 为 3、Health 为 80、MaxHealth 为 100。
输出:
<p>Total: 30</p>
<p>HP: 80/100</p>
除法可产生小数,例如 5 / 2 为 2.5。不要依赖运算符将数字字符串自动转换成数值;'10' + 2 的结果是字符串 102,不是数值 12。
除零、对零取余、超出支持范围的整数运算和非有限浮点结果会得到空结果,不会自动变为 0。
比较运算符¶
| 运算符 | 含义 |
|---|---|
==、!= |
相等、不相等 |
<、<= |
小于、小于等于 |
>、>= |
大于、大于等于 |
数值之间按数值比较,字符串之间可比较文本顺序;不要依赖不同类别的值进行隐式转换。例如,null == 0 为 false,null == '' 也为 false。对象相等不表示逐个比较其所有业务属性。
逻辑运算符¶
| 运算符 | 含义 |
|---|---|
! |
逻辑非 |
&& |
逻辑与 |
|| |
逻辑或 |
逻辑运算返回布尔值:
<p>{{ IsReady :and: HasWeapon }}</p>
<p>{{ !IsDead }}</p>
假设 IsReady、HasWeapon 均为 true,IsDead 为 false。
输出:
<p>true</p>
<p>true</p>
逻辑判断中,null、不可用值、false、数值 0 和空字符串为假,非零有限数值及非空字符串为真。有效对象和集合为真,空集合也不会仅因项数为 0 而变为假。判断集合是否有内容时应使用 Items.Count :gt: 0。
短路求值¶
逻辑与 &&(:and:)和逻辑或 ||(:or:)支持短路求值:
- 逻辑与:左侧为假时,结果已经确定为
false,不计算右侧。 - 逻辑或:左侧为真时,结果已经确定为
true,不计算右侧。
<!-- 左侧为假,不计算右侧的除法 -->
<p>{{ false :and: (1 / 0) }}</p>
<!-- 左侧为真,不计算右侧的除法 -->
<p>{{ true :or: (1 / 0) }}</p>
输出:
<p>false</p>
<p>true</p>
也可以先检查计算条件,再决定是否继续求值:
<!-- Divisor = 0:左侧为假,不执行除法 -->
<p>{{ Divisor != 0 :and: Amount / Divisor :gt: 1 }}</p>
输出:
<p>false</p>
?? 运算符¶
空值合并运算符 ?? 用于在没有值时提供替代值。它先读取左侧:
- 左侧有值时,返回左侧的值,不计算右侧。
- 左侧为
null或不可用值时,计算并返回右侧的值。
?? 返回选中的值,不将结果转换为布尔值。
提供缺省值¶
<p>{{ Nickname ?? 'Guest' }}</p>
<p>{{ Weapon.Owner.Name ?? 'Unknown' }}</p>
<p>{{ Items[SelectedIndex].Name ?? 'No item' }}</p>
假设 Nickname、Weapon 均为空,Items 为空集合,SelectedIndex 为 0。
输出:
<p>Guest</p>
<p>Unknown</p>
<p>No item</p>
路径中的中间对象为空、属性缺失、Array 下标越界或 Map 键不存在时,可以通过 ?? 提供缺省值。
| 表达式 | 结果 |
|---|---|
null ?? 'Guest' |
Guest |
'' ?? 'Guest' |
空字符串 |
0 ?? 100 |
0 |
false ?? true |
false |
?? 不将空字符串、0 或 false 当作缺失值,也不能修复表达式的语法错误。
只在需要时计算替代值¶
左侧有值时,不会计算右侧的替代表达式:
<!-- Name = "Alice":保留左侧名称,不计算右侧的除法 -->
<p>{{ Name ?? (1 / 0) }}</p>
输出:
<p>Alice</p>
替代值也可以从另一个属性读取:
<!-- Nickname = null,Name = "Alice" -->
<p>{{ Nickname ?? Name ?? 'Guest' }}</p>
输出:
<p>Alice</p>
与逻辑或不同
Name || 'Guest' 返回布尔值,不返回名称或 Guest。需要在没有值时显示缺省文字,应使用 Name ?? 'Guest'。
不支持空值合并赋值
Markup UI 支持 ??,但不支持 C# 的 ??= 赋值运算符、throw 表达式或 ?. 可选链。?? 只计算结果,不修改原属性;不能将 C# 示例直接作为模板表达式使用。
运算符别名¶
在 HTML 中编写 <、>、& 等字符可能涉及标记与实体转义,可以使用以下等价别名:
| 运算符 | 别名 |
|---|---|
< |
:lt: |
<= |
:le: |
> |
:gt: |
>= |
:ge: |
&& |
:and: |
|| |
:or: |
<p>{{ Health :lt: MaxHealth :and: !IsDead }}</p>
<span @text="{Health :ge: 100}"></span>
假设 Health 为 80、MaxHealth 为 100、IsDead 为 false。
输出:
<p>true</p>
<span>false</span>
别名两端的冒号都必须保留。字符串中的 ':and:' 只是文本,不作为运算符处理。
优先级与括号¶
下表按优先级从高到低排列:
| 优先级 | 语法 |
|---|---|
| 1 | 属性访问 .、索引 [] |
| 2 | 一元 !、一元 - |
| 3 | *、/、% |
| 4 | +、- |
| 5 | <、<=、>、>= |
| 6 | ==、!= |
| 7 | && |
| 8 | || |
| 9 | ?? |
运算符别名与对应符号具有相同优先级。同级二元运算从左向右结合;括号可以明确计算顺序:
<p>{{ 1 + 2 * 3 }}</p>
<p>{{ (1 + 2) * 3 }}</p>
输出:
<p>7</p>
<p>9</p>
括号中的结果也可以继续访问属性或集合项:
<p>{{ (Weapon ?? DefaultWeapon).Name }}</p>
<p>{{ (Items ?? DefaultItems)[SelectedIndex].Name }}</p>
假设 Weapon、Items 均为空,DefaultWeapon.Name 为 Sword,DefaultItems 第一项名称为 Potion,SelectedIndex 为 0。
输出:
<p>Sword</p>
<p>Potion</p>
作用域与别名¶
外层数据上下文¶
通过 @as 为上下文声明别名,表达式中使用 @别名 访问。在切换上下文后,可以继续引用外层对象:
<section @context="{@this}" @as="player">
<div @context="{Weapon}">
<p>Weapon: {{ Name }}</p>
<p>Owner: {{ @player.Name }}</p>
</div>
</section>
假设当前玩家名称为 Alice、武器名称为 Sword。
输出:
<section>
<div>
<p>Weapon: Sword</p>
<p>Owner: Alice</p>
</div>
</section>
@player 是已声明的别名,不是自动识别的命名上下文。使用名为 player 的命名上下文仍需通过 @context="player" 选择。
循环作用域¶
循环中可使用 @item、@items、@index 和 Map 的 @key。@item-as 为当前项声明别名,@items-as 为当前集合声明别名:
<ul @for="{Items}" @item-as="entry" @items-as="inventory">
<li>{{ @index }}: {{ @entry.Name }} / {{ @inventory.Count }}</li>
</ul>
假设 Items 包含名称依次为 Sword、Potion 的两个对象。
输出:
<ul>
<li>0: Sword / 2</li>
<li>1: Potion / 2</li>
</ul>
嵌套循环中的内置名称指向内层循环;需要保留外层项或集合时,使用不同名称的别名。别名只在声明的作用域及其子作用域中有效,不会传递给相邻区域;内层同名别名会遮蔽外层别名。
表达式与绑定选项¶
属性路径或计算公式决定读取什么,绑定选项决定如何传递结果。例如:
<input @value="{Name, mode=twoWay, update=blur}">
假设 Name 为 Alice,初始输出:
<input value="Alice">
这里 Name 是表达式,逗号后的 mode 和 update 是绑定选项。展开形式与之对应:
<input @value="{Name}" @value.mode="twoWay" @value.update="blur">
输出:
<input value="Alice">
转换器通过 converter 指定,可通过 converter.parameter 提供参数。转换器需要由应用提供,不是表达式可以直接调用的函数:
<!-- Currency 是应用已提供的转换器名称 -->
<span @text="{Price}" @text.converter="Currency" @text.converter.parameter="'USD'"></span>
假设 Price 为 10,应用提供的 Currency 转换器将该值格式化为 USD 10.00。
输出:
<span>USD 10.00</span>
mode、update、转换器和事件参数属于各绑定声明的选项,不是通用运算符,也不能把这些逗号选项直接写进 {{ ... }}。模式与更新时机见数据绑定概述。
可写路径与计算结果¶
Name、Weapon.Name、Items[SelectedIndex].Name 等路径可以定位具体属性;能否写回还取决于属性是否可写、容器是否支持相应写入及绑定目标是否支持输入。
<input @value="{Items[SelectedIndex].Name}">
假设 SelectedIndex 为 0,对应物品名称为 Sword,初始输出:
<input value="Sword">
Price * Quantity、Name ?? 'Guest' 等表达式只产生计算结果,不对应一个可直接写入的业务属性:
<span @text="{Price * Quantity}"></span>
假设 Price 为 10、Quantity 为 3。
输出:
<span>30</span>
不能通过设置 twoWay 让计算结果自动反推到原属性。若要编辑计算结果,应由应用提供明确的可写属性及业务处理。
不支持的语法¶
表达式不是 JavaScript 或 C# 脚本,不支持以下写法:
| 写法 | 原因 |
|---|---|
GetName()、Math.Round(Price) |
不支持函数或方法调用 |
Health = 100、Health += 1、Health++ |
不支持赋值、复合赋值或自增 |
IsReady ? 'Ready' : 'Waiting' |
不支持三元条件运算符 |
Weapon?.Name |
不支持可选链;可用普通路径与 ?? 处理缺失结果 |
Value === 0、Value !== 0 |
不支持严格相等运算符,使用 == 或 != |
[1, 2]、{Name: 'Alice'} |
不支持数组或对象字面量 |
A & B、A << 1、2 ** 3 |
不支持位运算、移位或幂运算 |
需要格式化、复杂计算或业务操作时,由应用提供属性、转换器或命令;不要将脚本写入绑定表达式。