Skip to content

Translation notice

This page was translated with machine translation and may contain inaccuracies. If you can help improve it, please open an issue or submit a pull request.

Implementing data binding in MC's UI

Alumopper

Alumopper

introduce

If you still don’t know what Floating UI is, see here will do.

Book continues above. Floating UI uses NBT data to define layout data. Just imagine, suppose we want to make a scrolling list whose content is the player's backpack items. How should we do it?

listone of themchildList field, which defines the elements to be displayed in the list. So, we just need to iterate through the player backpack list and add it based on the contentspriteControl data is just fine.

Hmm... "Just need", it sounds like traversal is very convenient (

As we all know, it is very troublesome to complete a traversal operation in MC, and it can only be completed by recursion. Moreover, it is obvious that this requirement is very common, and we need to write repeated code many times. Although this is a must-evaluate part of the data pack, we are not Mojang and will not let you taste a bunch of things, so we definitely need to provide a very convenient thing.

Let's see how this kind of problem is solved in other UI frameworks. WPF, the most powerful Windows desktop development framework in the universe, provides two things: template (Template) and data binding (Data Binding) to elegantly solve such problems.

xml
<!-- ItemsControl用于显示数据集合,ItemsSource绑定到ViewModel的数据源 -->
<ItemsControl x:Name="listControl" ItemsSource="{Binding ItemList}">

    <!-- 定义每个数据项的显示模板 -->
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <!-- 每个数据项显示为带边框的文本块 -->
            <Border Margin="5" Padding="10" Background="LightBlue">
                <TextBlock Text="{Binding Name}" FontSize="16"/>
            </Border>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>
cs
// MainWindow.xaml.cs
using System.Collections.Generic;
using System.Windows;

namespace WpfApp
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            // 创建测试数据
            var items = new List<Item>
            {
                new Item { Name = "项目1" },
                new Item { Name = "项目2" },
                new Item { Name = "项目3" },
                // 可继续添加更多项目测试滚动效果
            };

            // 设置数据上下文
            DataContext = new ViewModel { ItemList = items };
        }
    }

    // 数据模型类
    public class Item
    {
        public string Name { get; set; }
    }

    // ViewModel类
    public class ViewModel
    {
        public List<Item> ItemList { get; set; }
    }
}

can't read? It’s okay, meow. To put it simply, a template is defined, which will generate corresponding UI elements based on the data in the data source. Isn’t it very convenient? Therefore, we also need to make such a function.

Achievements

In Floating UI, there islistandstackpanelBoth controls havechildList fields, both controls support templates and data binding. IfchildThe field is defined as a data source, and an additionaltemplateField, Floating UI will automatically generate UI elements based on the template and data source.

json
{
    "type": "list",
    "size":[5f,5f],
    "template": {   // 模板
        "type":"button",
        "size":[1.2f,1.2f],
        "item":{
            "id":"apple"
        }
    },
    "child": {
        "path": "temp qwq.value",   //数据源
        "binds": [  //绑定关系
            {
                "source": "id",
                "target": "item.id",
            }
        ]
    }
}

In the above example,templatein is a control, that is, a template, andchildIt should have been a list, but here it is defined as a composite tag, indicating a data source reference.child.pathis astoragePath, the first half separated by spaces is the namespaceID of the storage, and the second half is the nbt path in this storage, which must correspond to a list.child.bindsThe list describes a binding relationship in whichsourceThe field represents the path in the data source,targetThe field represents the path in the template. In this binding relationship, theminecraft:tempmiddleqwq.valueA list serves as a data source, in which each elementidfields will be included in the templateitem.idThe value of the field, thereby generating a button.

At this point, the UI is still only generated from an existing data. If the content in the data source changes, the UI will not be automatically updated. At this time, it isset_propertyIt’s time for function to appear. By usingfloating_ui:datasource/set_propertyThe function sets the content in the data source and can automatically trigger UI updates. Of course, this function must be set up. After all, it is impossible for us to poll the content in the data source every tick. This would be too expensive. In fact, in WPF, it is also usedSetPropertyThis method triggers events to notify the UI to update.

Use it like thisset_propertyfunction:

mcfunction
data modify storage floating_ui:temp binding.path set value "minecraft:temp qwq.value"
data modify storage floating_ui:temp binding.value set from entity @p Inventory
floating_ui:datasource/set_property

It's that simple~

principle

bystackpanelFor example. in its_newIn function, it will be judgedchildWhether the field is a list. If it is not a list, it means it is a data source, then data binding may be used, so enter./template/append_templatein function.

mcfunction
# template: (string|compound)
# 如果不是内联数据,则获取数据模板
execute unless data storage floating_ui:input temp.template.type run return run function log:_error {msg: "无效的模板"}
# temp.child: {value: [...], path:xxx, binds: [source:xxx, target: xxx]}
# 若有绑定,则注册绑定,并获取绑定数据,储存在source.value中。如果没有binding,则说明直接声明了数据源,不参与绑定
execute if data storage floating_ui:input temp.child.path if function floating_ui:element/stackpanel/template/register_binding run function floating_ui:element/stackpanel/template/set_source with storage floating_ui:temp binding
# 解析保存在temp.child.value中的源数据
function floating_ui:element/stackpanel/template/update_source

This function is divided into three steps. The first step is to register data binding; the second step is to parse the content in the data source for the first time; the third step is to update the UI according to the content of the data source.

Register data binding

First, passif functionSubcommand callfloating_ui:element/stackpanel/template/register_bindingfunction。

mcfunction
# floating_ui:element/stackpanel/template/register_binding

#注册绑定
data modify storage floating_ui:temp binding.path set from storage floating_ui:input temp.child.path
function floating_ui:datasource/register_binding
#在实体中写入绑定信息
function floating_ui:element/stackpanel/template/register_binding_1 with storage floating_ui:input temp.child
return 1
mcfunction
# floating_ui:element/stackpanel/template/register_binding_1

$data modify entity @s item.components."minecraft:custom_data".register_binding."$(path)" set value 'function floating_ui:element/stackpanel/template/before_update_source'

floating_ui:datasource/register_bindingFunction is used to register a data binding globally and bind this UI control to this path. We will look at the details of this function later. andfloating_ui:element/stackpanel/template/register_binding_1is a macro function. previously_newThe function uses the display entity corresponding to the current control as the context, so in the macro function, the information of the data binding event is written - when$(path)When the content in the corresponding data source changes, it will be executedfloating_ui:element/stackpanel/template/before_update_sourcefunction。

TIP

You may find that almost all content related to macro functions in Floating UI will open a separate function to ensure that the amount of commands in a single macro function is as small as possible. This is because in macro functions, even ordinary commands will occupy the macro's parsing events, and short macro functions are of great help to improve the overall execution efficiency.

Look backfloating_ui:datasource/register_bindingfunction. What needs to be remembered is that the execution context of this function should also be the display entity corresponding to the control.

mcfunction
# floating_ui:datasource/register_binding

execute store result score _ int run function floating_ui:datasource/get_or_create_data_id with storage floating_ui:temp binding
#设置实体绑定
function floating_ui:datasource/register_binding_1
mcfunction
# floating_ui:datasource/get_or_create_data_id

$execute unless data storage floating_ui:data binding.id."$(path)" store result storage floating_ui:data binding.id."$(path)" int 1.0 run scoreboard players add _static_index floating_ui.data_id 1
$return run data get storage floating_ui:data binding.id."$(path)"
mcfunction
function floating_ui:datasource/register_binding_1

#这个控件有数据绑定
scoreboard players set @s floating_ui.data_id 0
execute unless score @s floating_ui.data_id_0 matches -2147483648..2147483647 run return run scoreboard players operation @s floating_ui.data_id_0 = _ int
execute unless score @s floating_ui.data_id_1 matches -2147483648..2147483647 run return run scoreboard players operation @s floating_ui.data_id_1 = _ int
# ...穷举部分省略
execute unless score @s floating_ui.data_id_20 matches -2147483648..2147483647 run return run scoreboard players operation @s floating_ui.data_id_20 = _ int
function log:_error {msg: "Failed to register binding: No data_id is available"}
#绑定失败,移除绑定标记
scoreboard players reset @s floating_ui.data_id

get_or_create_data_idThe function will get the unique ID value of the data source (actually the path), and if it does not exist, create an ID. Unfortunately, if the scoreboard is used to store IDs, the performance here should be greatly improved, but our data source contains spaces, and the scoreboard's points cannot contain spaces. So, you can only use storage to store IDs. function usereturncommand returns the ID of this data source, and inregister_bindingSave it temporarily.

Next, inregister_binding_1, it is to bind the control (that is, the display entity) and the ID of this data source (that is, the path). entityfloating_ui.data_id_xThe value corresponds to the data source to which it is bound. There are 20 entitiesdata_idscoreboard, fromdata_id_0arrivedata_id_20, which means that a control can support the binding of up to 21 data sources. If there are no free binding bits, the binding will fail and a prompt will be given. In fact, this approach is equivalent to using a static array with a length of 21. From the perspective of generality, a variable-length list should be used here, that is, a list type NBT should be used for storage. But accessing the list is expensive, and 21 binding bits are enough in most cases.

Obtain content from data source for the first time

back to the beginningappend_templatefunction. The next step is to usefunction floating_ui:element/stackpanel/template/set_source with storage floating_ui:temp bindingto parse the content in the data source. This step is very simple, using only one macro command.

mcfunction
$data modify storage floating_ui:input temp.child.value set from storage $(source)

It temporarily stores the parsed resultsvaluein the field. The later parsing part updates the UI based on this content.

Update content in data source

Let's not talk about parsing first, let's talk about what happens when the data source is updated. Because whether it is initialization or update, the same function is called for parsing, so it is better to talk about it later.

To update the contents of the data source is to usefunction floating_ui:datasource/set_propertyfunction is completed. We said before that before using this function, you need to givefloating_ui:temp bindinginpathandvalueCopy respectively represents the data source path and the content to be assigned. The function looks like this:

mcfunction
# floating_ui:temp binding
# {path: xxx, value: xxx}
execute store result score _ int run function floating_ui:datasource/get_or_create_data_id with storage floating_ui:temp binding
#设置值
function floating_ui:datasource/set_value with storage floating_ui:temp binding
execute if score isChanged _ matches 0 run return 0
#通知所有UI刷新
scoreboard players operation now floating_ui.notify_id = SOURCE_UPDATE floating_ui.notify_id
execute as @e[tag=floating_ui_control] run function floating_ui:datasource/set_property_1

First of all, it’s familiarfloating_ui:datasource/get_or_create_data_id, obtain the unique ID of the data source. Then use a simple macro command to set the value of the data source. A little trick is used here, that is, if the value to be set is the same as the original value,dataThe command will return failure. By getting the return value of command, we can know whether the data source has changed before and after setting it, so as to decide whether to refresh the UI, thus saving performance.

After that, all UIs are notified to refresh. From the perspective of scalability, considering that in addition to data source updates, there may be other notification events in the future, here usefloating_ui.notify_idThe scoreboard represents the event ID, whileSOURCE_UPDATEConstants represent data source update events. After that, all UIs are traversed and the UI bound to the corresponding data source is notified to refresh, that is,set_property_1function. This function is still a lengthy exhaustive process, which can be understood at a glance.

mcfunction
# 依次检查绑定槽,判断是否绑定了该数据源
execute if score @s floating_ui.data_id_0 = _ int run return run function floating_ui:macro/notify with entity @s item.components."minecraft:custom_data".data.ui
execute if score @s floating_ui.data_id_1 = _ int run return run function floating_ui:macro/notify with entity @s item.components."minecraft:custom_data".data.ui
# ...
execute if score @s floating_ui.data_id_20 = _ int run return run function floating_ui:macro/notify with entity @s item.components."minecraft:custom_data".data.ui

floating_ui:macro/notifyThe content is like this:

mcfunction
$function floating_ui:element/$(type)/_notified

This is actually a trick similar to polymorphism. Each control stores atypeThe field represents the type of control. The command built based on this field can call the function of the corresponding control. forstackpanelFor example, its function is like this:

mcfunction
function floating_ui:element/control/_notified

#0 - 源更新通知
execute if score now floating_ui.notify_id = SOURCE_UPDATE floating_ui.notify_id run function floating_ui:element/list/binding/update_source

First, the first step is to call the function of its base control (parent class), because generally the child control should inherit the event processing logic of the parent control, and then its own logic, that is, to handle the notification event of the data source update, callfloating_ui:element/list/binding/update_sourcefunction。

mcfunction
#获取绑定数据的更新行为
function floating_ui:element/list/binding/update_source_1 with storage floating_ui:temp binding
#执行更新
function floating_ui:macro/action with storage floating_ui:temp binding_info

Still considering it from the perspective of scalability, since there may be multiple bindings, not all field bindings necessarily call one method, or it should be said that onlychildOnly the binding of fields will call the function of updating the list control, so first you need to passupdate_source_1The macro function obtains the update behavior of the bound data. When we registered data binding before, the things we wrote into the entity came into use here.

mcfunction
$data modify storage floating_ui:temp binding_info.action set from entity @s item.components."minecraft:custom_data".register_binding."$(path)"

followed by a brieffloating_ui:macro/actionTool function, just used to executebinding_info.actioncommand stored in .

mcfunction
$$(action)

(It’s really short, meow)

So we will actually call the target function previously written in the entity, that isfloating_ui:element/stackpanel/template/before_update_sourcefunction。

mcfunction
#floating_ui:temp binding
#{path: xxx, value: xxx}
data modify storage floating_ui:input temp.template set from entity @s item.components."minecraft:custom_data".data.ui.template
data modify storage floating_ui:input temp.source.binds set from entity @s item.components."minecraft:custom_data".data.ui.source.binds
data modify storage floating_ui:input temp.source.value set from storage floating_ui:temp binding.value
# 移除已有的所有子控件
#删除子节点
execute on passengers run function floating_ui:dispose_control with entity @s item.components.minecraft:custom_data.data.ui
#更新源
function floating_ui:element/stackpanel/template/update_source

This is for compatibilityfloating_ui:element/stackpanel/template/update_sourcePerform a series of assignments on the required NBT data structure pattern, and remove existing sub-controls on the current control to prepare for subsequent updates. Finally, callupdate_sourcefunction to update the UI.

Now, we can finally talk aboutupdate_sourcefunction.

parse

floating_ui:element/stackpanel/template/update_sourceThe content is as follows:

mcfunction
# floating_ui:input temp.child: {value: [...], path:xxx, binds: [{source:xxx, target: xxx}]}

#遍历函数,确定参数
data modify storage floating_ui:temp temp.source.value set from storage floating_ui:input temp.source.value
execute unless data storage floating_ui:temp temp.source.value[0] run return run function log:_error {"message":"Data source must be a list"}

#覆盖手动定义的子元素
data modify storage floating_ui:input temp.child set value []

function floating_ui:element/stackpanel/template/update_source/loop

scoreboard players set isUpdate _ 1

#子元素
function floating_ui:element/stackpanel/child

That's right, the tedious traversal is completed here. There are a total of two traversal processes here, the first one is traversalvalueThe data in the list, the second one is traversedbindsBinding relationships in the list, and apply each relationship to the template to obtain the new subspace layout data, stored inchildin the list.

Related functions
mcfunction
# floating_ui:element/stackpanel/template/update_source/loop

#没有元素了,返回
execute unless data storage floating_ui:temp temp.source.value[0] run return 0

#复制一份模板
data modify storage floating_ui:temp temp.template set from storage floating_ui:input temp.template
#复制绑定参数表
data modify storage floating_ui:temp temp.source.binds set from storage floating_ui:input temp.source.binds

# 绑定替换
function floating_ui:element/stackpanel/template/update_source/params_loop

# 得到了模板,加入child列表
data modify storage floating_ui:input temp.child append from storage floating_ui:temp temp.template

data remove storage floating_ui:temp temp.source.value[0]

function floating_ui:element/stackpanel/template/update_source/loop
mcfunction
# function floating_ui:element/stackpanel/template/update_source/params_loop

#没有元素了,返回
execute unless data storage floating_ui:temp temp.source.binds[0] run return 0

# 绑定替换
function floating_ui:element/stackpanel/template/update_source/get_source with storage floating_ui:temp temp.source.binds[0]

data remove storage floating_ui:temp temp.source.binds[0]

function floating_ui:element/stackpanel/template/update_source/params_loop
mcfunction
# function floating_ui:element/stackpanel/template/update_source/get_source

$data modify storage floating_ui:temp temp.template.$(target) set from storage floating_ui:temp temp.source.value[0].$(source)

When all child control layout data is writtenchildAfter the list, callfunction floating_ui:element/stackpanel/childfunction, generates child controls. At this step, it is the same as directly declaringchildThe list is the same, so I won’t go into details here.

Summarize

Through data binding and templates, we can easily dynamically generate UI elements based on the data source, and automatically update the UI when the data source changes. In this way, we can easily implement functions such as scrolling lists. Within this framework, not onlychildfields, other attributes, such astextitemWait, you can also use data binding to dynamically update, but it has not been implemented yet. In the future, Floating UI will continue to improve functions in this area, making it easier for everyone to use data binding to implement dynamic UI.

Powered by VitePress and GitHub Pages