数据上下文¶
数据上下文(Data Context)是页面绑定表达式读取数据时使用的对象。例如,玩家信息面板可以将玩家对象作为数据上下文,通过对象的 Name、Health 等属性显示角色信息。
数据上下文由应用通过平台插件提供。HTML 负责声明使用哪些数据,不需要将当前角色名称、生命值等业务数据固定写入页面。
可观察对象¶
Markup UI 的数据上下文统一以可观察对象的形式提供。可观察对象包含供 HTML 绑定读取的业务属性,并在属性变化时发出通知,使界面能够更新。将可观察对象提供给文档或某个元素后,该对象就是对应的数据上下文。
以下示例定义玩家对象 PlayerObject 和武器对象 WeaponObject。两者均提供属性变更通知,既可以更新玩家的 Health,也可以更新 Weapon.Name,或替换整个 Weapon 对象。
- 继承 SDK 提供的
ObservableObject基类。 - 使用
MARKUPUI_PROPERTY声明可绑定属性。 - 在构造函数中调用
InitializeType<T>(),其中T为当前类的类型。 - 使用
std::make_shared<T>()创建对象。
#include <cstdint>
#include <memory>
#include <string>
#include <markupui/binding/modeling/object_model.h>
class WeaponObject final
: public MarkupUI::Binding::Modeling::ObservableObject
{
public:
WeaponObject()
{
InitializeType<WeaponObject>();
}
/*
* 参数 1:WeaponObject,声明该属性的类。
* 参数 2:std::string,属性的数据类型。
* 参数 3:Name,属性名称,也是 HTML 绑定使用的名称;
* 同时生成 GetName() 和 SetName() 访问器。
* 参数 4:"Sword",属性的默认值。
*/
MARKUPUI_PROPERTY(WeaponObject, std::string, Name, "Sword")
MARKUPUI_PROPERTY(WeaponObject, int32_t, Damage, 25)
};
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::shared_ptr<WeaponObject>, Weapon)
};
创建玩家及其武器:
auto weapon = std::make_shared<WeaponObject>();
auto player = std::make_shared<PlayerObject>();
player->SetWeapon(weapon);
宏生成 GetName()、SetName() 等访问器。通过 SetHealth()、SetWeapon() 等生成的设置方法修改属性时,由可观察对象处理变更通知。HTML 中使用的是声明的属性名 Health、Weapon,不是访问器名称。
SDK 支持标准的 INotifyPropertyChanged 接口,通过公开属性读取数据,通过 PropertyChanged 接收变更通知。下面两个类直接实现该接口,在属性设置器中发送通知。
using System.ComponentModel;
public class WeaponObject : INotifyPropertyChanged
{
private string _name = "Sword";
private int _damage = 25;
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 class PlayerObject : INotifyPropertyChanged
{
private string _name = "Alice";
private int _health = 100;
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 WeaponObject Weapon
{
get { return _weapon; }
set
{
if (_weapon != value)
{
_weapon = value;
PropertyChanged?.Invoke(
this, new PropertyChangedEventArgs("Weapon"));
}
}
}
}
创建玩家及其武器:
var player = new PlayerObject();
var weapon = player.Weapon;
属性设置器在值变化时发送与属性同名的通知。例如,设置 Health 会通知 Health 变化,设置 Weapon 会通知武器对象被替换。
使用现成的 ObservableObject 基类
项目已引用相应工具包时,可以让业务类继承 ObservableObject。它实现了 INotifyPropertyChanged,并提供 SetProperty 等辅助方法,可减少属性变更通知的重复代码。
链接中的类型属于 Microsoft.Toolkit.Mvvm 包,不是 .NET Standard 内置类型。仅使用其属性通知能力,无需采用特定的应用架构。
每个对象通知自身的属性变化
修改 Weapon.Name 时,由武器对象通知 Name 变化;替换 PlayerObject.Weapon 时,由玩家对象通知 Weapon 变化。只让玩家对象可观察,不能替代武器内部属性的通知。
从数据上下文读取属性¶
将上例中的 player 对象提供为文档默认数据上下文。具体设置入口请阅读各平台文档。
以该对象作为数据上下文时,可以在 HTML 中引用属性:
<h1>{{ Name }}</h1>
<p>Health: {{ Health }}</p>
<p>Weapon: {{ Weapon.Name }}</p>
最终输出的 HTML 代码:
<h1>Alice</h1>
<p>Health: 100</p>
<p>Weapon: Sword</p>
{{ ... }} 表示文本插值。Name 从当前数据上下文读取角色名称,Weapon.Name 则沿属性路径读取武器名称。C++ 和 C# 示例提供相同的属性名称,因此使用同一份 HTML。
数据上下文提供查找属性的起点;具体属性路径决定绑定读取的值。显示在哪个元素、写入文本还是元素属性,由绑定声明决定。
默认与命名数据上下文¶
一个文档允许配置多个数据上下文(DataContext),其中最多只有一个默认数据上下文,它没有名称。其他数据上下文通过名称区分,各名称在同一文档内必须唯一。
默认数据上下文¶
默认情况下,所有 HTML 元素都以文档的默认数据上下文作为自己的数据上下文。通过 @context 指定上下文后,元素自身和其子元素将使用指定的数据上下文。
<!-- 使用文档的默认数据上下文 -->
<p>{{ Name }}</p>
<!-- 改变数据上下文:使用默认数据上下文的 Weapon 属性作为上下文 -->
<section @context="{Weapon}">
<h1>{{ Name }}</h1>
<p>Damage: {{ Damage }}</p>
</section>
<!-- 改变数据上下文:使用名为 "player" 的上下文 -->
<section @context="player">
<h1>{{ Name }}</h1>
<p>Health: {{ Health }}</p>
</section>
命名数据上下文¶
当一个页面需要多个独立业务对象时,应用可以按名称登记数据上下文。例如,将玩家对象登记为 player,将设置对象登记为 settings。
<section @context="player">
<h1>{{ Name }}</h1>
<p>Health: {{ Health }}</p>
</section>
<section @context="settings">
<p>Volume: {{ Volume }}</p>
</section>
@context="player" 选择应用已登记的 player 对象,@context="settings" 选择 settings 对象。页面可以只使用命名数据上下文,不设置默认对象。
名称需要由应用登记
在 HTML 中填写 player 不会创建玩家对象,也不会自动寻找同名游戏对象。名称区分大小写;应用未登记对应名称时,该区域使用空上下文,不回退到父级或默认对象。
静态名称使用非空的 ASCII 字母、数字、下划线或连字符组合。mk:context 与 @context 等价,可按页面书写习惯选择一种形式。
嵌套上下文与当前对象¶
通过属性切换上下文¶
在当前上下文中取得一个子对象,可将其作为某个区域的新上下文:
<section @context="player">
<h1>{{ Name }}</h1>
<div @context="{Weapon}">
<p>{{ Name }}</p>
<p>Damage: {{ Damage }}</p>
</div>
</section>
外层的 Name 表示玩家名称;进入 Weapon 后,内部的 Name 与 Damage 分别表示武器名称和伤害。该切换作用于声明元素及其后代,不改变外部兄弟区域的上下文。
| 写法 | 含义 |
|---|---|
不写 @context |
继承父级上下文,最外层使用默认上下文 |
@context="player" |
选择应用登记的命名上下文 |
@context="{Weapon}" |
从当前上下文求值 Weapon 属性,使用其结果 |
名称与属性表达式不同
@context="Weapon" 选择名称为 Weapon 的上下文;@context="{Weapon}" 读取当前对象的 Weapon 属性。两者不能互换。
当前对象¶
@this 表示当前数据上下文。普通属性路径 Name 相当于 @this.Name;切换上下文后,@this 的指向也随之改变。
数据上下文别名¶
@as 可以在任意 HTML 元素上使用,为该元素的数据上下文创建别名。通过 @别名 访问对应对象,不需要同时声明 @context。
为继承的上下文创建别名¶
@as="page" 为继承的当前上下文创建别名:
<section @as="page">
<p>{{ @page.Name }}</p>
</section>
@page 指向该 section 继承的数据上下文,@as 本身不切换上下文。
为切换后的上下文创建别名¶
同时声明 @context 和 @as 时,别名指向切换后的上下文:
<section @as="page">
<div @context="{Child}" @as="child">
<p>Child: {{ @child.Name }}</p>
<p>Page: {{ @page.Name }}</p>
</div>
</section>
@child 指向 Child 属性提供的对象,@page 仍指向外层上下文。
循环容器的上下文别名¶
@for="{Items}" @as="page" 为循环容器的数据上下文创建别名;集合元素使用 @item-as 创建别名:
<ul @for="{Items}" @as="page" @item-as="item">
<li>{{ @page.Name }}: {{ @item.Name }}</li>
</ul>
@page 指向提供 Items 的上下文,@item 指向当前集合元素。@as 不会为集合元素创建别名。循环规则见集合绑定。
别名在声明区域及其子区域中使用,不影响外部兄弟区域。它是模板内的名称,不是由应用登记的命名数据上下文。
数据变化与上下文替换¶
修改上例中已有对象的属性:
player->SetHealth(80);
weapon->SetName("Bow");
player.Health = 80;
weapon.Name = "Bow";
两种写法均通过对象的通知机制更新绑定。原来的 HTML 随后显示 Health: 80 和 Weapon: Bow。
修改业务对象的属性与替换整个数据上下文是不同操作:
- 属性变化:例如将当前玩家的
Health从100改为80,对象本身未替换。应用应通过相应的变更通知更新绑定。 - 上下文替换:例如选择另一名玩家,将页面使用的玩家对象替换为新对象,相关绑定随后从新对象读取数据。
默认上下文与各命名上下文分别管理。替换默认对象不会自动改写已登记的 player、settings 等命名对象。
界面没有更新时
先检查上下文是否已提供、名称是否一致、属性路径是否正确,以及数据变化是否发送了对应通知。单纯修改 HTML 中的上下文名称不能代替应用侧的数据配置。
平台提供数据的方式¶
默认上下文、命名上下文和页面内作用域属于公共概念;业务对象的类型、设置入口、引用管理和变更通知由平台接入方式决定。详细信息请阅读各平台文档。