# 4. Data Binding and Custom Controls Part of [[UI Toolkit and Diegetic UI]]. Requires Unity 6000.5+. Previous: [[3.HUD and Inventory Example]]. Next: [[5.Diegetic UI Approaches]]. [[3.HUD and Inventory Example]] used runtime binding without much explanation. This note covers the model properly, then builds a reusable, bindable `[UxmlElement]` control (`ResourceBar`) and swaps it into the HUD. ## 4.1 The binding model A **binding** connects a *target* (a property on a `VisualElement`) to a *source* (a property reachable from a **data source** object) through a **data source path**. | Concept | API | Notes | |---|---|---| | Data source | `VisualElement.dataSource` (any `object`) | Inherited by descendants unless they set their own. | | Data source path | `DataBinding.dataSourcePath` (`PropertyPath`) | Relative to the data source. `"Stats.Health"` for nested, `"Items[0].Name"` for lists. | | Data source type | `dataSourceType` (UXML/Builder helper) | Lets UI Builder autocomplete paths. Not needed at runtime. | | Target property | First arg to `SetBinding(string, Binding)` | Names the element property, e.g. `"text"`, `"value"`. | | Binding | `DataBinding` | The concrete binding type for data sources. | Binding modes (`BindingMode`): | Mode | Direction | Use | |---|---|---| | `TwoWay` | source <-> UI | Settings sliders, text fields. Default. | | `ToTarget` | source -> UI | Read-only HUD. Prefer for HUD/labels. | | `ToSource` | UI -> source | Rare. | | `ToTargetOnce` | source -> UI, once | Static labels, localized names. | Update triggers (`DataBinding.updateTrigger`, `BindingUpdateTrigger`): | Trigger | Behaviour | |---|---| | `OnSourceChanged` (default) | Re-evaluates when the source reports a change (see 4.3). Falls back to polling if the source cannot notify. | | `EveryUpdate` | Re-evaluates every frame. Convenient for prototyping; wasteful at scale. | | `WhenDirty` | Re-evaluates when marked dirty (`MarkDirty()`), for sources you control manually. | ## 4.2 Defining a data source Anything can be a source. Properties must be visible to the **property system** (`Unity.Properties`): - On `UnityEngine.Object` types (MonoBehaviour, ScriptableObject), add `[CreateProperty]` to a field or property. Fields marked `[SerializeField]` are also exposed by default unless you add `[DontCreateProperty]`. That is why `PlayerStats` in note 3 hid its backing fields. - On plain C# classes/structs, add `[Unity.Properties.GeneratePropertyBag]` to the type so Unity can generate an AOT-safe property bag. Without it, the reflection fallback is slower and may be stripped in IL2CPP builds. - Computed values (`HealthText`) are fine as read-only properties; just remember to **notify** when their inputs change. ```csharp using Unity.Properties; using UnityEngine; [CreateAssetMenu(menuName = "Settings/Audio Settings")] public class AudioSettings : ScriptableObject // ScriptableObject as a data source: easy to hot-edit { [CreateProperty, Range(0f, 1f)] public float MasterVolume = 0.8f; [CreateProperty, Range(0f, 1f)] public float MusicVolume = 0.6f; } ``` ```csharp // Two-way binding of a Slider to a ScriptableObject (e.g. in an options menu): var slider = root.Q<Slider>("master-volume"); slider.dataSource = audioSettings; slider.SetBinding("value", new DataBinding { dataSourcePath = new PropertyPath(nameof(AudioSettings.MasterVolume)), bindingMode = BindingMode.TwoWay }); ``` ## 4.3 Telling the UI something changed The binding system needs a signal, or it must poll. Options, in order of preference: 1. **`INotifyBindablePropertyChanged`** on the source. Raise `propertyChanged` with the property name when it changes (as `PlayerStats` does). Only bindings for that property update. Do not forget **derived** properties (`HealthText`). 2. **`IDataSourceViewHashProvider`** for sources that cannot raise per-property events (big lists, third-party types). Return a cheap number that changes whenever the data changes; the system compares it between frames instead of re-reading every property. ```csharp using Unity.Properties; using UnityEngine.UIElements; public class WaveState : IDataSourceViewHashProvider { long version; [CreateProperty] public int Wave { get; private set; } public void Next() { Wave++; version++; } public long GetViewHashCode() => version; // changes => UI re-reads } ``` 3. Nothing: the system falls back to checking values each update. Fine for a handful of bindings, not for hundreds. ## 4.4 Bindings in UXML You can declare bindings in UXML so the Builder shows them: ```xml <ui:Label name="hp-text"> <Bindings> <ui:DataBinding property="text" data-source-path="HealthText" binding-mode="ToTarget" /> </Bindings> </ui:Label> ``` The `data-source` (the actual object) is a runtime reference, so assign it in code: `root.dataSource = stats;`. A ScriptableObject can be assigned in the Builder instead. Prefer code bindings when your source is a scene object (like `PlayerStats`); use UXML bindings for static, asset-based sources. ## 4.5 Converters Use converters when the UI type differs from the source type, or to format. Per-binding converters: ```csharp var goldLabel = root.Q<Label>("gold-text"); var binding = new DataBinding { dataSourcePath = new PropertyPath(nameof(PlayerStats.Gold)), bindingMode = BindingMode.ToTarget }; binding.sourceToUiConverters.AddConverter((ref int gold) => quot;{gold:N0} gold"); // int -> string goldLabel.SetBinding("text", binding); ``` For converters shared across the project, create a `ConverterGroup`, add converters, register it with `ConverterGroups.RegisterConverterGroup(...)`, and reference it by id (`source-to-ui-converters` in UXML). Check the converter pages in the manual (links at the end) for the exact registration API in your version. > [!tip] Binding limits > Bindings set *properties*. They cannot toggle a USS class. For "red when low", either (a) bind a bool/enum to a custom control that sets classes in its property setter (see 4.6), or (b) subscribe to the source event and call `EnableInClassList("bar--low", ratio < 0.25f)`. ## 4.6 Custom control: `ResourceBar` A reusable bar with a value, a max, a label, a class-driven low state, and bindable properties. It animates by scaling an inner element (`scale` does not dirty layout, unlike `width`). ### 4.6a Rules for `[UxmlElement]` controls (Unity 6) - Class must be `partial` (Unity's source generator adds the UXML factory) and have a public parameterless constructor. - `[UxmlElement]` on the class; `[UxmlAttribute]` on each property that should be an attribute in UXML. Attribute names are the property names converted to kebab-case (`MaxValue` -> `max-value`): check the Builder's Attributes list. - For bindings add `[CreateProperty]`, a `static readonly BindingId` per property, and call `NotifyPropertyChanged(...)` in the setter **only when the value changed**. - Use a namespace and reference it in UXML with an `xmlns` alias. - Styling comes from USS classes you document (`resource-bar`, ...), not from loading assets in the constructor. ### 4.6b `ResourceBar.cs` (`Assets/UI/Controls/`) ```csharp using Unity.Properties; using UnityEngine; using UnityEngine.UIElements; namespace Game.UI { [UxmlElement] public partial class ResourceBar : VisualElement { public const string UssClassName = "resource-bar"; public const string FillUssClassName = "resource-bar__fill"; public const string LabelUssClassName = "resource-bar__label"; public const string LowUssClassName = "resource-bar--low"; static readonly BindingId ValueProperty = nameof(Value); static readonly BindingId MaxValueProperty = nameof(MaxValue); static readonly BindingId LabelProperty = nameof(Label); readonly VisualElement fill; readonly Label labelElement; float value; float maxValue = 100f; string label = string.Empty; [UxmlAttribute, CreateProperty] public float Value { get => value; set { float clamped = Mathf.Clamp(value, 0f, maxValue); if (Mathf.Approximately(this.value, clamped)) return; this.value = clamped; Refresh(); NotifyPropertyChanged(ValueProperty); } } [UxmlAttribute, CreateProperty] public float MaxValue { get => maxValue; set { float v = Mathf.Max(1f, value); if (Mathf.Approximately(maxValue, v)) return; maxValue = v; this.value = Mathf.Min(this.value, maxValue); Refresh(); NotifyPropertyChanged(MaxValueProperty); } } [UxmlAttribute, CreateProperty] public string Label { get => label; set { if (label == value) return; label = value; Refresh(); NotifyPropertyChanged(LabelProperty); } } public ResourceBar() { AddToClassList(UssClassName); pickingMode = PickingMode.Ignore; fill = new VisualElement { name = "fill", pickingMode = PickingMode.Ignore }; fill.AddToClassList(FillUssClassName); Add(fill); labelElement = new Label { name = "label", pickingMode = PickingMode.Ignore }; labelElement.AddToClassList(LabelUssClassName); Add(labelElement); Refresh(); } void Refresh() { float ratio = maxValue <= 0f ? 0f : value / maxValue; // scale (not width): no layout pass. USS sets transform-origin: left center. fill.style.scale = new Scale(new Vector3(ratio, 1f, 1f)); labelElement.text = string.IsNullOrEmpty(label) ? quot;{Mathf.CeilToInt(value)} / {Mathf.CeilToInt(maxValue)}" : quot;{label} {Mathf.CeilToInt(value)} / {Mathf.CeilToInt(maxValue)}"; EnableInClassList(LowUssClassName, ratio < 0.25f); } } } ``` Points to notice: - Inside setters we use `this.value` for the field and `value` for the implicit setter argument. If that reads badly, rename the fields. - `NotifyPropertyChanged` is inherited from `VisualElement`; it raises the signal binding listens for (`INotifyBindablePropertyChanged`). - `Refresh()` runs in the constructor so the control renders sensibly in UI Builder with default values. - Guard with `Mathf.Approximately`/equality: otherwise setting a bound property to the same value every frame would notify every frame. ### 4.6c USS for the control (add to `Hud.uss` or its own file) ```css .resource-bar { height: 22px; background-color: rgba(255, 255, 255, 0.08); border-radius: 4px; overflow: hidden; justify-content: center; } .resource-bar__fill { position: absolute; left: 0; top: 0; bottom: 0; right: 0; /* full size; the scale shrinks it */ background-color: var(--color-accent); transform-origin: left center; transition-property: scale, background-color; transition-duration: 0.15s; transition-timing-function: ease-out; } .resource-bar__label { -unity-text-align: middle-center; font-size: 12px; margin: 0; } .resource-bar--low .resource-bar__fill { background-color: var(--color-health); } ``` ### 4.6d Use it in the HUD In `Hud.uxml`, add an `xmlns` alias to the root tag and replace the `ProgressBar` and hp label: ```xml <ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:game="Game.UI" editor-extension-mode="False"> ... <game:ResourceBar name="hp-bar" label="HP" /> ... </ui:UXML> ``` In `HudController.OnUIReload`, replace the two `hpBar` binding lines and the `hp-text` line: ```csharp var hpBar = Require<Game.UI.ResourceBar>(root, "hp-bar"); hpBar.SetBinding(nameof(Game.UI.ResourceBar.MaxValue), ToTarget(nameof(PlayerStats.MaxHealth))); hpBar.SetBinding(nameof(Game.UI.ResourceBar.Value), ToTarget(nameof(PlayerStats.Health))); ``` Binding order matters slightly: if `Value` bound first while `MaxValue` were still the default 100, a health of 250 would be clamped to 100. That is why `MaxValue` is bound first above. Then play: damage the player and watch the fill shrink with a transition and flip red under 25 percent. > [!note] UI Builder refresh > After a script change, the new control shows up in the Builder Library under *Project > Custom Controls (C#)*. If the XML shows "unknown element", wait for compilation to finish and reimport the UXML. ## 4.7 Diagnosing binding problems - **Console messages**: a failed binding logs a warning/error naming the element, property and path. Misspelled property names are the usual cause. - **Not updating**: the source did not notify (4.3), the property is missing `[CreateProperty]`, or you changed the field instead of the property. - **Updates once, never again**: the binding mode is `ToTargetOnce`. - **Debugger**: the UI Toolkit Debugger shows the applied bindings on a selected element in recent Unity 6 versions (check the Bindings section of its inspector). - **Polling cost**: a source that does not notify and a binding with `EveryUpdate` both re-evaluate each frame. Profile before you scale up. ## 4.8 Further reading - Runtime binding overview: https://docs.unity3d.com/Manual/UIE-runtime-binding.html - Define a data source: https://docs.unity3d.com/6000.2/Documentation/Manual/UIE-runtime-binding-define-data-source.html - Binding types and C# examples: https://docs.unity3d.com/Manual/UIE-runtime-binding-types.html - Custom controls: https://docs.unity3d.com/6000.3/Documentation/Manual/UIE-create-custom-controls.html - Customize UXML attributes: https://docs.unity3d.com/Manual/ui-systems/custom-control-customize-uxml-attributes.html Next: [[5.Diegetic UI Approaches]].