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. |
WindowDescriptor.TitleFromDatasets the title when the data source changes, and when data that implementsINotifyBindablePropertyChangedreports a change. For plain data, callRefreshTitle(). The descriptor listens to the data, so the data keeps the descriptor alive while it shows it.WindowDescriptor.WindowTemplateis the template of a window, or null.Categoryis free text; a UXML window sets it withcategory.- To open new windows straight into a group, make them with
open: false, then callGroupAsTabs. - Layouts save window ids. Make the template windows before
LoadLayoutAsync, with the same ids, to restore their places.IdFromDatakeeps the ids stable.
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. |
- A format replaces
{path}and{path:format}with the value at the path in the data, in the current culture. The path is a public field or property;{Crew.Captain}reads a nested one.{{and}}give one brace. A null value gives nothing. In place of a path, an arithmetic expression of numbers and paths with+ - * / %and parentheses gives a number, such as{Hull * 100:0}or{(Id + 3) * 2}. A path that the data does not have, and an expression that does not compute, stay as they are.DataFormat.Format(format, data)is the same function, andDataFormat.Of(format)makes aTitleFromDataorIdFromDatafunction in code. RegisterTemplatesmakes all templates before it registers one. It throwsArgumentExceptionfor text that is not JSON, and for an entry with no id, no content or a missing UXML asset; then it registers nothing. A missing field keeps its default.WindowTemplateDefinition.ListFromJson(json)andListToJson(definitions)read and write the list, so a tool can edit the definitions. TheTemplateDemoexample scene edits one definition in aPropertyGridand as JSON text, shows the same template as C# code, and makes the open windows again after each change. Its other tabs make a template in UXML (WindowTemplateElement) and one in C# (WindowTemplate.CreateContent).- A UXML file can also hold other files with
<ui:Template>and<ui:Instance>. UI Builder makes them. Usepartswhen the JSON must choose the files. - An expression computes numbers only. It has no comparisons, no functions and no text operations. Use
TitleFromDatain code for them.
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.
<ui:DataBinding property="Value" data-source-path="Temperature" binding-mode="ToTarget" />sets one property from one value. The Toolbox charts take bindings, see Data-bound windows.<tb:FormatBinding property="text" format="Radius {RadiusKm:N0} km, {Moons} moons" />sets a text from a format with several values. It uses the same placeholders and expressions astitle-format. A plain C# data source is read each frame. A source withIDataSourceViewHashProviderorINotifyBindablePropertyChangedis formatted again only after it changes.
<!-- 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:
<tb:WindowTemplateElement id="planet" content="project://database/.../PlanetDetail.uxml" title-format="{Name}" id-format="planet{Name}" />registers a template. Each object gets its own window with a new copy of the content. The attributes areid,content,width,height,x,y,title,title-format,id-format,categoryandenable-scrolling.xandyplace the first window in the viewport, and the next windows cascade from it. The element takes no space. ItsTemplateproperty is the registered template. In UI Builder, it does not register: it draws a window with a copy of the content at its initial position and size.<tb:Window id="planetDetail" title="Planet" title-format="{Name}"><ui:Instance template="PlanetDetail" /></tb:Window>is one window. It shows the last object.
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. |
- The GameObject needs a collider. The click must hit this object first.
- A click on a window, a toolbar or other UI in front of the object does nothing.
OpenWindowOnClick.IsOverUI(workspace, screenPosition)is the test. - A user click casts one ray for all
OpenWindowOnClickcomponents. The component that the ray hits first opens its window. Many clickable objects cost one ray for each click. OpenWindowOnClick.ClickAt(screenPosition, camera)does the click in code, for example for a touch or a test. It returns the component that opened its window, or null.TryOpenAt(screenPosition)opens the window only when the ray hits this object first.- The static event
OpenWindowOnClick.Openedsends the component and the window after each open. - In the Inspector, Known Ids lists the
WindowTemplateElementandWindowids in the UXML ofDocument(or of the scene documents whenDocumentis empty). A pick sets Window. A warning shows when Window is not in the UXML. - A
MonoBehaviourcan be the data. Bindings read its public fields, and its properties with[CreateProperty].
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:
- It builds a window of the template for the data, hidden, and plays its open transition. It removes the window after a few frames.
- The hidden window is not registered. It raises no window events and plays no sound.
- A second call for the same template does nothing. An unknown id throws
ArgumentException. OpenWindowOnClickcalls it atStartwhenPrewarmis true andWindowis a template id.
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:
- The reflection read boxes a number or an enum, and a public field read allocates in Mono. A property of a reference type does not allocate.
- An enum value and a converter that makes a string (
ToolboxConvertershex, duration,N0, bytes, upper case) make a new string at each read. AnimateBindingboxes the value that it reads.
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.