基础绑定¶
基础绑定从数据上下文读取单个值,将其用于文本、HTML 属性或 CSS 样式。读取的值可以是字符串、数值或布尔值,也可以是对象中的某个属性。对象本身则可以用作一组元素的数据上下文。
标量值与对象¶
以下代码定义玩家、武器和武器所有者;将创建的 player 作为文档默认数据上下文。标量值、对象及其支持范围见数据类型。
#include <cstdint>
#include <memory>
#include <string>
#include <markupui/binding/modeling/object_model.h>
class OwnerObject final : public MarkupUI::Binding::Modeling::ObservableObject
{
public:
OwnerObject() { InitializeType<OwnerObject>(); }
MARKUPUI_PROPERTY(OwnerObject, std::string, Name, "Alice")
MARKUPUI_PROPERTY(OwnerObject, std::string, AvatarPath, "/images/alice.png")
};
class WeaponObject final : public MarkupUI::Binding::Modeling::ObservableObject
{
public:
WeaponObject() { InitializeType<WeaponObject>(); }
MARKUPUI_PROPERTY(WeaponObject, std::string, Name, "Sword")
MARKUPUI_PROPERTY(WeaponObject, int32_t, Damage, 25)
/*
* 参数 1:WeaponObject,声明该属性的类。
* 参数 2:std::weak_ptr<OwnerObject>,不延长所有者生命周期的弱引用。
* 参数 3:WeaponOwner,C++ 属性标识符,用于生成
* GetWeaponOwner() 和 SetWeaponOwner() 访问器。
* 参数 4:"WeaponOwner",HTML 绑定使用的属性名称,
* 例如 Weapon.WeaponOwner.Name 中的 WeaponOwner。
* 参数 5(可选):属性默认值;本例省略,对象引用初始为空。
*/
MARKUPUI_NAMED_PROPERTY(WeaponObject, std::weak_ptr<OwnerObject>, WeaponOwner, "WeaponOwner")
};
class PlayerObject final : public MarkupUI::Binding::Modeling::ObservableObject
{
public:
PlayerObject() { InitializeType<PlayerObject>(); }
MARKUPUI_PROPERTY(PlayerObject, std::string, Name, "Alice")
MARKUPUI_PROPERTY(PlayerObject, int32_t, Health, 100)
MARKUPUI_PROPERTY(PlayerObject, std::string, AvatarPath, "/images/alice.png")
MARKUPUI_PROPERTY(PlayerObject, bool, IsReady, true)
MARKUPUI_PROPERTY(PlayerObject, std::shared_ptr<WeaponObject>, Weapon)
};
创建对象并连接嵌套属性:
auto owner = std::make_shared<OwnerObject>();
auto weapon = std::make_shared<WeaponObject>();
weapon->SetWeaponOwner(std::weak_ptr<OwnerObject>{owner});
auto player = std::make_shared<PlayerObject>();
player->SetWeapon(weapon);
WeaponOwner 的绑定名称为 WeaponOwner,因此 HTML 中使用 Weapon.WeaponOwner.Name,C++ 中通过 SetWeaponOwner() 设置该业务属性。
武器对所有者使用弱引用,不延长所有者的生命周期。示例中的 owner 强引用应由应用保留;所有者销毁后,弱引用失效。C++ 业务代码访问弱引用时,应通过 lock() 取得临时强引用并检查结果。
using System;
using System.ComponentModel;
public class OwnerObject : INotifyPropertyChanged
{
private string _name = "Alice";
private string _avatarPath = "/images/alice.png";
public event PropertyChangedEventHandler PropertyChanged;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
PropertyChanged?.Invoke(
this, new PropertyChangedEventArgs("Name"));
}
}
}
public string AvatarPath
{
get { return _avatarPath; }
set
{
if (_avatarPath != value)
{
_avatarPath = value;
PropertyChanged?.Invoke(
this, new PropertyChangedEventArgs("AvatarPath"));
}
}
}
}
public class WeaponObject : INotifyPropertyChanged
{
private string _name = "Sword";
private int _damage = 25;
// C# GC 可以回收不再可达的循环引用对象。
// 本例使用 WeakReference<T> 仅用于说明 SDK 也支持弱引用属性。
private WeakReference<OwnerObject> _weaponOwner;
public event PropertyChangedEventHandler PropertyChanged;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
PropertyChanged?.Invoke(
this, new PropertyChangedEventArgs("Name"));
}
}
}
public int Damage
{
get { return _damage; }
set
{
if (_damage != value)
{
_damage = value;
PropertyChanged?.Invoke(
this, new PropertyChangedEventArgs("Damage"));
}
}
}
public WeakReference<OwnerObject> WeaponOwner
{
get { return _weaponOwner; }
set
{
if (_weaponOwner != value)
{
_weaponOwner = value;
PropertyChanged?.Invoke(
this, new PropertyChangedEventArgs("WeaponOwner"));
}
}
}
}
public class PlayerObject : INotifyPropertyChanged
{
private string _name = "Alice";
private int _health = 100;
private string _avatarPath = "/images/alice.png";
private bool _isReady = true;
private WeaponObject _weapon = new WeaponObject();
public event PropertyChangedEventHandler PropertyChanged;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
PropertyChanged?.Invoke(
this, new PropertyChangedEventArgs("Name"));
}
}
}
public int Health
{
get { return _health; }
set
{
if (_health != value)
{
_health = value;
PropertyChanged?.Invoke(
this, new PropertyChangedEventArgs("Health"));
}
}
}
public string AvatarPath
{
get { return _avatarPath; }
set
{
if (_avatarPath != value)
{
_avatarPath = value;
PropertyChanged?.Invoke(
this, new PropertyChangedEventArgs("AvatarPath"));
}
}
}
public bool IsReady
{
get { return _isReady; }
set
{
if (_isReady != value)
{
_isReady = value;
PropertyChanged?.Invoke(
this, new PropertyChangedEventArgs("IsReady"));
}
}
}
public WeaponObject Weapon
{
get { return _weapon; }
set
{
if (_weapon != value)
{
_weapon = value;
PropertyChanged?.Invoke(
this, new PropertyChangedEventArgs("Weapon"));
}
}
}
}
创建对象:
var owner = new OwnerObject();
var player = new PlayerObject();
var weapon = player.Weapon;
weapon.WeaponOwner = new WeakReference<OwnerObject>(owner);
WeaponOwner 是直接声明为 WeakReference<OwnerObject> 的弱引用属性,绑定可以继续访问其目标对象,例如 Weapon.WeaponOwner.Name,无需在可观察对象中额外定义返回强引用的包装属性。应用应保留 owner 的强引用;目标被回收后,绑定路径无法取得目标对象。弱引用失效本身不会发出属性通知,业务上解除所有者关系时应显式设置 WeaponOwner = null 并通知绑定。
Name、Health、AvatarPath和IsReady是标量属性。Weapon是对象属性,包含Name和Damage。Weapon.Name是属性路径,最终读取的是字符串"Sword"。
对象属性与 HTML 属性
数据上下文中的属性属于业务对象,例如 PlayerObject.Name;HTML 属性属于元素,例如 img 的 src。绑定将前者的值用于后者,两者不要求同名。
绑定标量值¶
显示文本¶
文本插值将标量值插入元素的文本内容,可与固定文字组合:
<h1>{{ Name }}</h1>
<p>Health: {{ Health }}</p>
最终输出的 HTML 代码:
<h1>Alice</h1>
<p>Health: 100</p>
也可以使用 @text 绑定整个文本内容:
<h1 @text="{Name}">Player name</h1>
Player name 是设计时占位文本,运行时由绑定结果替换。文本绑定用于显示文字,不将字符串中的 HTML 标签作为页面结构解析。
设置 HTML 属性¶
使用 @属性名 将值绑定到 HTML 属性。下面将 AvatarPath 用于图片的 src,将 Name 用于输入框的 value:
<img @src="{AvatarPath}" alt="Player avatar">
<input @value="{Name}">
绑定目标决定值的用途:图片路径按资源路径规则解析;输入框显示字符串,并可按绑定模式将输入写回 Name。
设置样式¶
使用 @style.属性名 设置单个 CSS 样式属性。例如,数据上下文提供数值属性 Opacity 时:
<div @style.opacity="{Opacity}">Player information</div>
绑定结果必须符合目标的类型和取值范围。opacity 需要有效的透明度值;长度、颜色等其他样式也需要相应的值。绑定不会自动为任意数值选择 CSS 单位。
访问对象中的属性¶
使用点号连接属性名称,可以逐级读取对象中的值:
<p>Weapon: {{ Weapon.Name }}</p>
<p>Damage: {{ Weapon.Damage }}</p>
路径从当前数据上下文开始:先读取 PlayerObject 的 Weapon,再读取武器对象的 Name 或 Damage。
最终输出的 HTML 代码:
<p>Weapon: Sword</p>
<p>Damage: 25</p>
Name 与 Weapon.Name 指向不同属性:前者是玩家名称,后者是武器名称。属性名称区分大小写,应与应用提供的名称保持一致。
多级属性路径¶
属性路径可以通过多个点号访问更深层的对象。例如,Weapon.WeaponOwner.Name 从当前数据上下文依次读取 Weapon、武器的 Owner,最后读取所有者的 Name:
<p>Owner: {{ Weapon.WeaponOwner.Name }}</p>
最终输出的 HTML 代码:
<p>Owner: Alice</p>
路径中的每一级名称都属于前一级读取到的对象,不是包含点号的单个属性名称。中间对象必须存在,才能继续读取后续属性。
多级路径也可以用于 HTML 属性绑定。例如,武器的 Owner 对象提供 AvatarPath 时:
<img @src="{Weapon.WeaponOwner.AvatarPath}" alt="Weapon owner avatar">
路径从当前数据上下文开始解析。使用 @context="{Weapon}" 后,同一个所有者名称应写为 WeaponOwner.Name:
<section @context="{Weapon}">
<p>Owner: {{ WeaponOwner.Name }}</p>
</section>
对于 Weapon.WeaponOwner.Name,修改所有者名称时,由所有者对象通知 Name 变化;替换所有者时,由武器对象通知 Owner 变化;替换武器时,由玩家对象通知 Weapon 变化。绑定据此重新读取受影响的路径,后续更新使用替换后的对象。
将对象作为数据上下文¶
当一组元素都显示同一个对象的属性时,可以通过 @context 使用该对象作为数据上下文:
<section @context="{Weapon}">
<h2>{{ Name }}</h2>
<p>Damage: {{ Damage }}</p>
</section>
Weapon 从外层数据上下文读取。指定上下文后,元素自身和其子元素将使用指定的数据上下文,因此这里的 Name 和 Damage 都属于武器对象。
两种写法的用途不同:
| 写法 | 含义 | 适用场景 |
|---|---|---|
{{ Weapon.Name }} |
从当前上下文读取嵌套属性 | 只显示对象中的少量属性 |
@context="{Weapon}",内部使用 {{ Name }} |
使用对象作为数据上下文,再读取属性 | 一组元素共同显示该对象 |
对象不是自动生成的界面
Weapon 表示对象,Weapon.Name 才表示武器名称。不要用 {{ Weapon }} 代替 {{ Weapon.Name }} 来显示名称,也不要依赖它自动展开对象字段。需要展示哪些属性,应在 HTML 中明确声明。
直接将对象用于文本插值时,绑定使用对象提供的字符串表示;未提供时使用 [Object]。该字符串不等于对象的 Name 属性,也不是所有字段的序列化结果。
错误用例:希望显示武器名称,却直接绑定了武器对象。假设该对象未提供字符串表示:
<!-- 错误:Weapon 是对象,不能用它代替 Weapon.Name 显示武器名称 -->
<p>Weapon: {{ Weapon }}</p>
最终输出的 HTML 代码:
<p>Weapon: [Object]</p>
命名上下文、嵌套作用域和外层别名见数据上下文。
属性变化与对象替换¶
绑定持续更新需要相应的变更通知。对于 Weapon.Name,应区分以下两种操作:
| 操作 | 示例 | 所需通知与结果 |
|---|---|---|
| 修改对象内部的属性 | 将当前武器的 Name 从 Sword 改为 Bow |
武器对象通知 Name 变化,相关文本重新读取名称 |
| 替换对象属性 | 将玩家的 Weapon 替换为另一把武器 |
玩家对象通知 Weapon 变化,绑定重新解析后续路径,读取新武器的属性 |
替换 Weapon 后,Weapon.Name 以及使用 @context="{Weapon}" 的区域都会转向新对象。后续应由新武器的属性通知驱动更新,旧武器的变化不再决定该区域的内容。
对于直接绑定的 Health 等标量属性,同样需要通知对应属性发生变化。单纯修改业务对象中的值,不等于已经通知界面更新。
以上持续更新行为适用于需要监听数据变化的绑定;oneTime 不持续监听后续变化。模式与输入写回规则见数据绑定概述,各平台的通知方式请阅读对应平台文档。
空对象与缺失属性¶
当 Weapon 为空时,Weapon.Name 无法读取武器名称;当属性不存在时,也无法得到有效值。文本插值在这些情况下显示为空,不会自动使用外层同名属性。
<!-- Weapon 为空时,此处不显示武器名称 -->
<p>Weapon: {{ Weapon.Name }}</p>
之后为 Weapon 设置有效对象并发送对应通知,绑定可以重新取得名称。
不要将空结果视为数值 0 或布尔值 false;也不要假定文本、图片、样式和输入目标对空值的处理完全相同。业务上需要缺省文字或隐藏区域时,应明确设置相应的表达式或显示条件。