跳转至

集合绑定

集合绑定通过 @for 遍历数据上下文中的集合,为每个数据项生成对应的 HTML 内容。集合项可以是标量值、对象,也可以是另一个集合。

支持的集合类型

Markup UI 支持以下三类集合:

类型 数据组织方式 单项访问 常见用途
Array(数组或列表) 按位置排列的数据项 从 0 开始的整数下标,如 Items[0] 背包物品、任务列表
Map(映射或字典) 键与值的对应关系,键不可重复 按键读取,如 Stats['Health'] 按名称或标识查找数据
Set(集合) 不包含重复项的数据集合 不支持下标访问,使用遍历读取数据项 标签、已解锁能力

这些名称表示绑定所识别的集合类型,各平台使用对应的实际容器类型。集合是否能在修改后自动更新界面,还取决于平台是否提供相应的变更通知。

下面每组示例分别定义一个可观察对象,按所选示例创建 player 并提供为文档默认数据上下文:

// 以 NumberListObject 为例,其他示例使用对应的类名。
auto player = std::make_shared<NumberListObject>();
// 以 NumberListObject 为例,其他示例使用对应的类名。
var player = new NumberListObject();

集合自身负责增删通知;对象项的属性设置器负责项内属性通知。C# 示例的集合属性只读,仍可通过集合操作增删内容;需要替换整个集合时,应增加设置器并通知属性变化。

Array:按位置排列的数据

遍历标量项

假设默认数据上下文提供以下数组:

#include <algorithm>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include <map>
#include <set>
#include <markupui/binding/modeling/object_model.h>

class NumberListObject final : public MarkupUI::Binding::Modeling::ObservableObject
{
public:
    using NumberArray = MarkupUI::Binding::Modeling::ObservableArray<int32_t>;

    NumberListObject()
    {
        InitializeType<NumberListObject>();
        // SetNumbers 函数由 MARKUPUI_PROPERTY 宏生成。
        SetNumbers(NumberArray::From(std::vector<int32_t>{10, 20, 30}));
    }

    MARKUPUI_PROPERTY(NumberListObject, NumberArray, Numbers)
};
using System.Collections.ObjectModel;
using System.ComponentModel;

public class NumberListObject : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public ObservableCollection<int> Numbers { get; } = new ObservableCollection<int> { 10, 20, 30 };
}

在容器上声明 @for,其子内容作为每一项的模板。使用 @item 读取当前项,@index 读取从 0 开始的遍历序号:

<ul @for="{Numbers}">
  <li>{{ @index }}: {{ @item }}</li>
</ul>

最终输出的 HTML 代码:

<ul>
  <li>0: 10</li>
  <li>1: 20</li>
  <li>2: 30</li>
</ul>

ul 保留一份,内部的 li 为每个数据项生成一份。

遍历对象项

假设 Items 包含两个对象:

#include <algorithm>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include <map>
#include <set>
#include <markupui/binding/modeling/object_model.h>

class ItemObject final : public MarkupUI::Binding::Modeling::ObservableObject
{
public:
    ItemObject() { InitializeType<ItemObject>(); }

    MARKUPUI_PROPERTY(ItemObject, std::string, Name, "Sword")
    MARKUPUI_PROPERTY(ItemObject, int32_t, Quantity, 1)
};

class ItemListObject final : public MarkupUI::Binding::Modeling::ObservableObject
{
public:
    using ItemArray = MarkupUI::Binding::Modeling::ObservableArray<std::shared_ptr<ItemObject>>;

    ItemListObject()
    {
        InitializeType<ItemListObject>();
        auto sword = std::make_shared<ItemObject>();
        auto potion = std::make_shared<ItemObject>();
        potion->SetName("Potion");
        potion->SetQuantity(3);
        SetItems(ItemArray::From(std::vector<std::shared_ptr<ItemObject>>{sword, potion}));
    }

    MARKUPUI_PROPERTY(ItemListObject, ItemArray, Items)
};
using System.Collections.ObjectModel;
using System.ComponentModel;

public class ItemObject : INotifyPropertyChanged
{
    private string _name = "Sword";
    private int _quantity = 1;

    public event PropertyChangedEventHandler PropertyChanged;

    public string Name
    {
        get { return _name; }
        set
        {
            if (_name != value)
            {
                _name = value;
                PropertyChanged?.Invoke(
                    this, new PropertyChangedEventArgs("Name"));
            }
        }
    }

    public int Quantity
    {
        get { return _quantity; }
        set
        {
            if (_quantity != value)
            {
                _quantity = value;
                PropertyChanged?.Invoke(
                    this, new PropertyChangedEventArgs("Quantity"));
            }
        }
    }
}

public class ItemListObject : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public ObservableCollection<ItemObject> Items { get; } =
        new ObservableCollection<ItemObject>
        {
            new ItemObject { Name = "Sword", Quantity = 1 },
            new ItemObject { Name = "Potion", Quantity = 3 }
        };
}

每一项的内容以当前对象作为数据上下文,可以直接读取它的属性:

<ul @for="{Items}">
  <li>{{ Name }} × {{ Quantity }}</li>
</ul>

最终输出的 HTML 代码:

<ul>
  <li>Sword × 1</li>
  <li>Potion × 3</li>
</ul>

下标访问

读取特定位置的数据时,使用整数下标,也可以继续访问对象中的属性:

<p>First number: {{ Numbers[0] }}</p>
<p>First item: {{ Items[0].Name }}</p>

下标从 0 开始,应位于有效范围内。下标代表当前位置;插入、删除或移动数据后,同一下标可能对应不同数据项。

Map:按键组织的数据

遍历键和值

假设默认数据上下文提供以下 Map:

#include <algorithm>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include <map>
#include <set>
#include <markupui/binding/modeling/object_model.h>

class StatsObject final : public MarkupUI::Binding::Modeling::ObservableObject
{
public:
    using StatMap = MarkupUI::Binding::Modeling::ObservableMap<std::string, int32_t>;

    StatsObject()
    {
        InitializeType<StatsObject>();
        SetStats(StatMap::From(std::map<std::string, int32_t>{{"Health", 100}, {"Mana", 50}}));
    }

    MARKUPUI_PROPERTY(StatsObject, StatMap, Stats)
};
using MarkupUI.Sdk.Binding;
using System.ComponentModel;

public class StatsObject : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public ObservableDictionary<string, int> Stats { get; } = new ObservableDictionary<string, int>
    {
        { "Health", 100 },
        { "Mana", 50 }
    };
}

Map 循环中,@key 是当前键,@item 是该键对应的值:

<dl @for="{Stats}">
  <dt>{{ @key }}</dt>
  <dd>{{ @item }}</dd>
</dl>

假设本例按 Health、Mana 的顺序遍历,最终输出的 HTML 代码:

<dl>
  <dt>Health</dt>
  <dd>100</dd>
  <dt>Mana</dt>
  <dd>50</dd>
</dl>

Map 的值为对象时,当前数据上下文是该值对象,可以直接使用 {{ Name }} 等属性路径;键仍通过 @key 读取。

按键访问

Map 键的类型限制

Map 的键仅支持字符串和整数,不支持布尔值、浮点数、对象或其他类型。

  • 字符串:String。
  • 有符号整数:Int8、Int16、Int32、Int64,分别为 8、16、32、64 位。
  • 无符号整数:UInt8、UInt16、UInt32、UInt64,分别为 8、16、32、64 位。

字符串键使用引号,整数键使用整数。例如:

<!-- Stats 使用字符串键 -->
<p>Health: {{ Stats['Health'] }}</p>

<!-- PlayersById 使用整数键,其值为玩家对象 -->
<p>Player: {{ PlayersById[1001].Name }}</p>

键的类型必须与 Map 的实际键类型一致。Stats['Health'] 按键查找值;整数键 Map 中的 [0] 表示键 0,不表示遍历到的第一项。

Map 的遍历顺序

不应假定 Map 按键名或插入顺序排序。需要固定显示顺序时,应由应用提供按所需顺序排列的数据,例如 Array。

Set:不包含重复项的数据

Set 用于保存不重复的数据项。例如,默认数据上下文提供以下标签集合:

#include <algorithm>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include <map>
#include <set>
#include <markupui/binding/modeling/object_model.h>

class TagListObject final : public MarkupUI::Binding::Modeling::ObservableObject
{
public:
    using TagSet = MarkupUI::Binding::Modeling::ObservableSet<std::string>;

    TagListObject()
    {
        InitializeType<TagListObject>();
        SetTags(TagSet::From(std::set<std::string>{"Fire", "Ice"}));
    }

    MARKUPUI_PROPERTY(TagListObject, TagSet, Tags)
};
using MarkupUI.Sdk.Binding;
using System.ComponentModel;

public class TagListObject : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public ObservableHashSet<string> Tags { get; } = new ObservableHashSet<string> { "Fire", "Ice" };
}

使用 @for 遍历,并通过 @item 读取当前项:

<ul @for="{Tags}">
  <li>{{ @item }}</li>
</ul>

假设本例按 Fire、Ice 的顺序遍历,最终输出的 HTML 代码:

<ul>
  <li>Fire</li>
  <li>Ice</li>
</ul>

Set 的数据项为对象时,可以直接访问该对象的属性。数据项是否重复,由集合自身的相等性规则决定。

Set 不支持下标访问

Tags[0]、Tags[1] 等路径无效,不能用来读取 Set 中的第一项或第二项。循环中的 @index 只是本次遍历的序号,不表示 Set 提供下标访问,也不应作为数据项的稳定标识。

错误用例:

<!-- 错误:Set 不支持下标访问 -->
<p>{{ Tags[0] }}</p>

该路径无法取得有效值,文本插值显示为空:

<p></p>

Set 的显示顺序取决于实际集合的遍历顺序,不应依赖固定位置。需要按位置读取或固定排序时,应由应用提供相应的 Array。

当前项与集合信息

循环内容中可以使用以下名称:

名称 含义
@this 当前数据上下文,默认是当前项;通过 @context 切换后指向新上下文
@item 当前循环的数据项;Map 中是值
@index 当前循环从 0 开始的遍历序号
@key Map 当前项的键
@items 当前循环所遍历的集合

集合属性

目前集合提供 Count 属性,Array、Map 和 Set 均支持。

属性 含义 访问权限
Count 集合中的数据项数量;Map 中为键值对数量 只读

通过集合的属性路径读取数量:

<p>Items: {{ Items.Count }}</p>
<p>Stats: {{ Stats.Count }}</p>
<p>Tags: {{ Tags.Count }}</p>

空集合的 Count 为 0。集合值为 null 时,无法读取 Count,不应将这种情况与空集合混同。

循环内部可以通过 @items.Count 读取当前集合的总项数:

<ul @for="{Items}">
  <li>{{ Name }} / {{ @items.Count }}</li>
</ul>

Count 是集合总数,不是当前项的序号。集合数量变化后,需要相应的属性变更通知才能更新绑定到 Count 的内容。

模板与嵌套集合

不增加容器元素

如果不需要额外容器,可以将 @for 写在 template 上。template 自身不生成显示元素,内部内容按集合项重复:

<ul>
  <li>Inventory</li>
  <template @for="{Items}">
    <li>{{ Name }} × {{ Quantity }}</li>
  </template>
</ul>

这里的固定标题只显示一次,后续条目按 Items 生成。

嵌套遍历与别名

集合项可以包含另一个集合。例如,Groups 的每个对象都有 Name 和 Items。使用 @item-as 为外层当前项声明别名,便于在内层访问:

<section @for="{Groups}" @item-as="group">
  <article>
    <h2>{{ Name }}</h2>
    <ul @for="{Items}">
      <li>{{ @group.Name }}: {{ Name }}</li>
    </ul>
  </article>
</section>

内层的 Name 来自当前物品,@group.Name 来自所属分组。内层 @item、@index 和 @items 对应内层循环。

集合项本身就是集合时,可以使用 @for="{@this}" 遍历当前集合项。

集合变化与界面更新

集合结构变化、集合项的属性变化与替换整个集合,需要分别提供对应通知:

变化 示例 更新方式
集合结构变化 Array 插入或移除项、Map 更新某个键的值、Set 添加或移除项 集合发出相应变化通知,更新对应的重复内容
集合项的属性变化 将某个物品的 Quantity 从 1 改为 2 物品对象通知 Quantity 变化,更新该项的文本
集合属性替换 将数据上下文的 Items 替换为另一个集合 数据上下文通知 Items 变化,绑定转向新集合

Array 的移动通知会更新显示顺序,相关 @index 也随位置变化。Map 和 Set 的变化按其自身支持的操作处理,不能套用 Array 的下标规则。

普通容器不等于可通知集合

能读取和遍历集合,并不代表直接修改容器后界面一定自动更新。应用应使用平台支持的集合通知方式;集合项对象的属性变化也需要单独通知。详细信息请阅读各平台文档。

空集合

集合为空或集合值为 null 时,循环不生成任何项。普通容器仍保留,容器内声明的循环模板不作为占位内容显示:

<!-- Items 为空时 -->
<ul @for="{Items}">
  <li>{{ Name }}</li>
</ul>

最终输出的 HTML 代码:

<ul></ul>