UI Toolbox logoUI Toolbox

Window templates

A WindowTemplate is a recipe for data windows. Register it once, then make one window for each data object.

workspace.RegisterTemplate(new WindowTemplate("Ship", Resources.Load<VisualTreeAsset>("UI/ShipDetail"))
{
    Width = 400,
    Height = 160,
    TitleFromData = data => $"{((Ship)data).Name} {((Ship)data).Hull:0}%",
    IdFromData = data => $"Ship_{((Ship)data).Id}",
    Category = "Fleet"
});

var window = workspace.GetOrCreateWindow("Ship", aurora);   // opens and focuses, or makes the window
workspace.GroupAsTabs(workspace.GetWindowsByCategory("Fleet"));
foreach (var ship in workspace.GetWindowsByTemplate("Ship").ToList()) workspace.CloseWindow(ship.Id);
WindowTemplate member Default Meaning
Id The id that CreateWindow takes. Required.
CreateContent null Makes the content of one window from its data. The constructor with UXML assets sets it to new copies of the assets. One asset is the content. Several assets, such as a header, a chart and a footer, go in order into one column.
Width, Height 420, 320 The initial size.
Position null The position of the first window in the viewport. Each window of the template on the canvas moves the next one down and right by one cascade step (26 pixels, six steps). Null uses the workspace cascade.
Title empty The title without TitleFromData. Empty takes the window id.
TitleFromData null Makes the title from the data.
IdFromData null Makes the window id from the data. Null gives "Ship_1", "Ship_2" and so on.
Category empty The category of the windows.
EnableScrolling true Wraps the content in a ScrollView.
Workspace member Meaning
RegisterTemplate(template), GetTemplate(id) Adds or replaces a template, and finds one. A replaced template does not change the windows made from the old one.
CreateWindow(templateId, data, open = true) Makes and registers a window with the data as its DataSource, and opens it. It throws ArgumentException for an unknown template, and InvalidOperationException when the template has no CreateContent or the id is taken.
GetOrCreateWindow(templateId, data) Opens and focuses the window of the data, also a closed one, or makes a new one. With IdFromData, the id finds the window, so a new copy of the same data finds it too. Without it, the window must show the same object.
GetWindowsByTemplate(id), GetWindowsByCategory(category), FindWindowByData(data) The queries. FindWindowByData compares references.
RemoveWindowAsync(id) Closes a window and takes it out of the workspace. After that no query finds it, its id is free, and it does not listen to its data. Use it for the window of data that is gone, and to make a window again after its template changed. It returns false for an unknown id, and when a Closing handler kept the window open.
GroupAsTabs(windows) Puts the windows in one new tab group at the place of the first one, and returns the group window. Open windows leave their chrome or their old group. Closed windows open as tabs, and the first window is the active tab. Tab group windows are skipped.

Templates from JSON

workspace.RegisterTemplates(json) registers the templates of a JSON text, and returns them in the JSON order. The content is a UXML asset in a Resources folder. The title and the id come from format strings.

{ "templates": [
    { "id": "Ship", "content": "UI/ShipDetail", "width": 400, "height": 160,
      "titleFormat": "{Name} {Hull:0}%", "idFormat": "Ship_{Id}", "category": "Fleet" }
] }
WindowTemplateDefinition field Default Meaning
id The template id. Required.
content The Resources path of the UXML, without the extension. Required.
parts empty More Resources paths of UXML files. They go after the content, in order.
width, height 420, 320 The initial size. 0 or less takes the default.
x, y -1 The position of the first window in the viewport. -1 on both uses the workspace cascade; -1 on one takes the margin of 32 pixels.
title empty The title without a title format.
titleFormat, idFormat empty Set TitleFromData and IdFromData. Empty sets null.
category empty The category of the windows.
enableScrolling true Wraps the content in a ScrollView.

Detail windows in UXML

A detail window shows the data of one object, such as a planet that the user clicks in the scene. Design the content once in UXML. Bind each value to a name in the data. No controller code is needed.

<!-- PlanetDetail.uxml -->
<ui:VisualElement>
  <ui:Label>
    <Bindings>
      <tb:FormatBinding property="text" format="{Name}, a {Kind} planet" />
    </Bindings>
  </ui:Label>
  <tb:Gauge min-value="-200" max-value="500">
    <Bindings>
      <ui:DataBinding property="Value" data-source-path="Temperature" binding-mode="ToTarget" />
    </Bindings>
  </tb:Gauge>
  <tb:Graph chart-type="Bar">
    <tb:GraphSeries label="Temperature">
      <Bindings>
        <ui:DataBinding property="Data" data-source-path="TemperatureHistory" binding-mode="ToTarget" />
      </Bindings>
    </tb:GraphSeries>
  </tb:Graph>
</ui:VisualElement>

Put the content in the workspace in one of two ways:

Workspace.ShowData(id, data) shows the data. A template id opens the window of the template for the data, as GetOrCreateWindow does. A window id sets the data as the DataSource of that window and opens it. An unknown id throws ArgumentException.

OpenWindowOnClick is a component that calls ShowData when the user clicks a scene object:

Field Default Description
Window The id of an WindowTemplateElement or of an Window.
Data null The window data. Null takes the first other component on the GameObject, such as a Planet script.
Document null The UIDocument of the workspace. Null finds the first document with an Workspace.
Camera null The camera of the click ray. Null takes Camera.main. A user click uses the camera of the first component that sees the click.
Prewarm true Builds the window of a template hidden at Start, with Workspace.Prewarm, so the first click opens the window fast.

First open

The first window with bindings in a play session is slow: about 4 to 5 seconds in the editor, in one frame. The Mono runtime prepares the UI Toolkit binding, conversion and style code on its first use. MDI code does not cause it; a plain DataBinding on a Label has the same cost. Later windows open fast. The Windows Standalone player (Mono) has a much smaller cost: without a prewarm, the first open took about 40 ms plus one frame of about 320 ms; with the prewarm, 15 ms plus one frame of 74 to 120 ms. An IL2CPP player build was not measured.

Workspace.Prewarm(templateId, data) moves this cost to scene load:

Call it yourself for a template that code opens, for example in Start: workspace.Prewarm("planet", anyPlanet).

In the editor, Enter Play Mode Options with the domain reload off (Project Settings > Editor) pays the cost once for each editor session, not once for each play session. The MDI static state (OpenWindowOnClick.Opened, ToolboxAudio.PlayClip and its sound host, ToolboxText.Lookup and ToolboxText.Changed, the ElementImage host) resets itself at each play session. The Localization example also resets its table and the culture. Reset the static state of your own code with [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)].

DataFormat parses each format once and looks up each member once for each type. FormatBinding sets text directly, without the property bag. When the text is the same as the last frame, FormatBinding keeps the old string, and double, float, int and long values format without a temporary string. Some garbage stays for a plain C# source that is read each frame:

To stop this garbage, give the source IDataSourceViewHashProvider or INotifyBindablePropertyChanged. Then the bindings read the source only after it changes. Graph computes its view, ticks and labels in loops over reused buffers, so a redraw, a hit test or a View read makes no garbage. PieChart.SetData keeps a slice whose label matches the item at its place.

The sample scene Assets/UIToolbox/Examples/Scenes/Planets.unity has five planets (SamplePlanet) and two ships (SampleShip) that orbit planets. A planet opens PlanetDetail.uxml and a ship opens ShipDetail.uxml, each from its own WindowTemplateElement in Planets.uxml. The toolbar has a main menu (a ToolBarMenu with submenus) and buttons for four example screens: System and Fleet (a DataGrid of the scene objects with a Details button), Log (clicks, menu actions and window states) and Settings (a PropertyGrid with the time scale and the shared planet window). PlanetsController fills the screens and handles the menu.