Data-bound windows
WindowDescriptor.DataSource holds the data that a window shows. The window sets it as the UI Toolkit dataSource of its content. Bindings in the content resolve against it, also after the window moves into a tab group or a dock.
- Bind in UXML with
<Bindings>anddata-source-path, or in C# withSetBinding. - Plain C# objects work. The binding system polls them each frame. For many windows, implement
INotifyBindablePropertyChangedorIDataSourceViewHashProvideron the data so that bindings update only on change. - Public fields bind by default. A property needs
[CreateProperty]. - Toolbox charts bind too. Use the C# property name:
<ui:DataBinding property="Value" data-source-path="Temperature" />. The bindable properties areGauge.Value,MinValueandMaxValue,ProgressRing.Value,PieSlice.LabelandValue,PieChart.CenterLabel, andGraphSeries.LabelandData.Datatakes afloat[]or aList<float>. The charts redraw only when a bound value changes. - Set
DataSourceagain to show other data in the same window. The window raisesDataSourceChanged. Use it for state that bindings do not cover. For the title, setTitleFromData. - Layouts save window ids, not data. A window keeps its
DataSourceacross close, reopen and layout load, because the workspace keeps the descriptor.
One content template, many windows:
<!-- Resources/UI/ItemDetail.uxml -->
<ui:UXML xmlns:ui="UnityEngine.UIElements">
<ui:Label>
<Bindings>
<ui:DataBinding property="text" data-source-path="Name" binding-mode="ToTarget" />
</Bindings>
</ui:Label>
</ui:UXML>
public class Item
{
public string Id = string.Empty;
public string Name = string.Empty;
}
public static class ItemWindows
{
private static readonly VisualTreeAsset Template = Resources.Load<VisualTreeAsset>("UI/ItemDetail");
// Opens the item's window, or focuses it when it is already registered
public static void Show(Workspace workspace, Item item)
{
var id = $"Item_{item.Id}";
if (workspace.GetWindow(id) == null)
{
WindowDescriptor.Builder()
.WithId(id)
.WithTitle(item.Name)
.WithContent(Template.Instantiate())
.WithDataSource(item)
.BuildAndRegister(workspace);
}
workspace.OpenWindow(id);
}
// Queries use GetAllWindows
public static WindowDescriptor? Find(Workspace workspace, Item item) =>
workspace.GetAllWindows().FirstOrDefault(w => w.DataSource == item);
public static IEnumerable<WindowDescriptor> All(Workspace workspace) =>
workspace.GetAllWindows().Where(w => w.DataSource is Item);
}
Binding converters
ToolboxConverters registers converter groups for conversions that Unity does not have. A binding opts in by the group id, in UXML or in UI Builder:
<ui:VisualElement>
<Bindings>
<ui:DataBinding property="style.display" data-source-path="IsAlive" binding-mode="ToTarget" source-to-ui-converters="tb:visible" />
</Bindings>
</ui:VisualElement>
<ui:RadioButtonGroup choices="Easy,Normal,Hard">
<Bindings>
<ui:DataBinding property="value" data-source-path="Difficulty" binding-mode="TwoWay"
source-to-ui-converters="tb:enum-int" ui-to-source-converters="tb:enum-int" />
</Bindings>
</ui:RadioButtonGroup>
A group has no parameters, so the id says what it does. A group matches the declared type of the source exactly, with no base types. Groups do not chain.
| Group | Source | UI | Result |
|---|---|---|---|
tb:visible, tb:hidden |
bool | DisplayStyle, style.display |
True shows (Flex) or hides (None). tb:hidden inverts. |
tb:visibility, tb:invisibility |
bool | Visibility, style.visibility |
Same, but the element keeps its space. |
tb:not |
bool | bool | Inverts, both ways. |
tb:has-value |
string, object, a class | bool | False for null, a destroyed UnityEngine.Object or an empty string. |
tb:percent |
float, double 0 to 1 | Length, StyleLength |
0.25 gives 25%. |
tb:hex |
Color | string | #RRGGBB, or #RRGGBBAA when not opaque. Both ways; text that is not a color gives clear. Use is-delayed="true" on the text field. |
tb:display-name |
an enum | string | The [InspectorName], or the name with spaces: MaxHealth gives "Max Health". |
tb:f0, tb:f1, tb:f2 |
int, long, float, double | string | 0, 1 or 2 decimals. |
tb:n0 |
int, long, float, double | string | Thousands separators: "1,234,567". |
tb:percent-text |
int, long, float, double | string | 0.256 gives "26%". |
tb:currency |
int, long, float, double | string | Money in the current culture. |
tb:bytes |
int, long, float, double | string | 1536000 gives "1.5 MB" (1024 based). |
tb:duration |
seconds (int, long, float, double), TimeSpan | string | 5400 gives "1:30:00"; 90 gives "1:30". |
tb:date, tb:time, tb:datetime |
DateTime | string | Short date, short time, or both. |
tb:upper, tb:lower |
string | string | Upper or lower case. |
tb:xy, tb:xz, tb:yz |
Vector2, Vector3, Vector2Int, Vector3Int | Vector3, Vector2 | The id names the axes of the Vector3. tb:xz: (x, y) gives (x, 0, y), and (x, y, z) gives (x, z). Both ways. |
tb:enum-int |
an enum | int | The int value, both ways. For RadioButtonGroup.value and DropdownField.index. |
All text groups use the current culture. The groups register when the player starts and when the editor loads. Startup adds tb:enum-int and tb:display-name for each public enum, and tb:has-value for each public class, in UIToolbox.Runtime and in the assemblies that reference it. For other types, call ToolboxConverters.RegisterEnum<KeyCode>() or ToolboxConverters.RegisterHasValue<MyType>() before the binding updates.