UIToolkit Custom Controls Runtime Constructible Without Loosing uxml uss references.

I am trying to use a constructor to create a custom control named Popup.
I want to set the text of the popup by passing a string via the constructor.
exactly in the same way that a Toggle VisualElement lets you do.

Example.
Unity’s Custom Control:
Toggle myToggle = new Toggle(“Text for toggle via a constructor”);
My Custom Control:
Popup myPopup = new Popup(“Text in the centre of the red box from c#”);

I want to be able to construct VisualElements this way so that I can create X number of VisualElements depending on data received from SQL all with different member variable data.

But what happens is that the myPopup instance has no Uxml or USS references at all.
Unity gives this error:


Note: This error is on line 22 of popup c# example below.

Please someone explain what I need to do.

Below is an example project created to try to solve this.

  • The Main window has three visual elements, Header, Body, Footer.
  • The Header has a button that when pressed should place a Popup within the Body.
  • The Footer has a Toggle as an example of unity being able to do what I want to do.

MainWindow: Uxml

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
    <Style src="project://database/Assets/UI/MainWindow.uss?fileID=7433441132597879392&guid=c91c9754ad6b2de4fbf95507110cc6a7&type=3#MainWindow" />
    <MainWindow class="screen">
        <ui:VisualElement name="Container" class="screen">
            <ui:VisualElement name="Header" class="header">
                <ui:Button text="Spawn Popup" display-tooltip-when-elided="true" name="HeaderButton" style="top: 25%; width: 50%; left: 25%; height: 75px; font-size: 32px;" />
            </ui:VisualElement>
            <ui:VisualElement name="Body" class="body" style="background-color: rgb(77, 64, 64);" />
            <ui:VisualElement name="Footer" class="footer" />
        </ui:VisualElement>
    </MainWindow>
</ui:UXML>

MainWindow: Picture

Popup: UXML

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
    <Style src="project://database/Assets/UI/Popup/Popup.uss?fileID=7433441132597879392&guid=fb7fb0e47e1b69e4e9637badbc4e54de&type=3#Popup" />
    <Popup int-attr="5" class="size">
        <ui:VisualElement name="container" class="size container">
            <ui:Label text="Change Me" display-tooltip-when-elided="true" name="PopupLabel" class="label" />
        </ui:VisualElement>
    </Popup>
</ui:UXML>

Popup: Picture
8421354--1114149--upload_2022-9-7_14-50-41.png

MainWindow:

public class MainWindow : VisualElement
{
    Button headerButton;
    public new class UxmlFactory : UxmlFactory<MainWindow, UxmlTraits> {}

    public MainWindow()
    {
        this.RegisterCallback<GeometryChangedEvent>(OnGeometryChange);
    }
    void OnGeometryChange(GeometryChangedEvent evt)
    {
        headerButton = this.Q<Button>("HeaderButton");
        headerButton.RegisterCallback<ClickEvent>(ev => OnHeaderButtonClicked());

        LookAtUnityBeingAbleToDoWhatIWantToDo();

        this.UnregisterCallback<GeometryChangedEvent>(OnGeometryChange);
    }

    private void OnHeaderButtonClicked()
    {
        // This should work the same way as a toggle.
        Popup popup = new Popup("Text in the middle of Red Box"); // losses its uxml and uss references.
        popup.style.flexGrow = 1;
        this.Q("Body").Add(popup);
        Debug.Log("Pressed But nothing spawned T.T ");
    }

    private void LookAtUnityBeingAbleToDoWhatIWantToDo()
    {
        // This is the goal... but I want the looks to be defined in uxml + uss
        Toggle myToggle = new Toggle("Setting a label via a constructor");
        this.Q("Footer").Add(myToggle);
    }

Popup:

public class Popup : VisualElement
{
    Label popupLabel;
    string labelText = "Some Default Text";

    public new class UxmlFactory : UxmlFactory<Popup, UxmlTraits> {}
    public Popup()
    {
        Debug.Log("Default Constructor");
        this.RegisterCallback<GeometryChangedEvent>(OnGeometryChange);
    }
    public Popup(string text) : this()
    {
        Debug.Log("The Constructor Being Used");
        labelText = text;
    }

    void OnGeometryChange(GeometryChangedEvent evt)
    {
        popupLabel = this.Q<Label>("PopupLabel");
        popupLabel.text = labelText;
        this.UnregisterCallback<GeometryChangedEvent>(OnGeometryChange);
    }
}

When you create your instance manually in code, you’ll also have to hook the UXML/USS styles manually. For example, our Toggle class does this internally in its constructor: labelElement.AddToClassList(labelUssClassName)

In your situation, instead of instantiating the popup with new Popup(), I would instantiate it from the UXML VisualTreeAsset.Instantiate(). Then you can query the Label to change it’s string.

Project Details
I am making a UI only application, it is a ROS2 robot control application. (Not a game)
The goal is to always utilize uxml and uss files and to never need to programmatically build out a “Custom Control”

Question
To save your time, skip to - To Instantiate a Custom Control section
Please could you fact check that section?

Creating a Custom Control - Example

  • Create a C# script, a uxml file a and a uss file. (Green1, Blue2, Yellow3)

  • Open UI Builder.

  • Make the C# Custom Control a child element of the uxml file (Drag Green1.2 into Blue2)

  • Add your design as a Child to the C# Custom control (The tree of children inside Green 1.1)

  • Add the uss file and apply your styles to each child element. (Yellow 3)

Notes
Look at 1.3, This is a preview of what 1.2 C#CustomControl Script will look like. Notice that it is empty.
This is because 1.2 does not have any reference to the tree from the uxml (Blue2)
In other words.
“Only Visual Elements created via C# code within the constructor of 1.2 will appear in the preview of 1.3”
try it by adding:

this.Add(new Toggle("Toggle added via c#"));

In the c# file constructor and then preview the CustomControl 1.3

To Instantiate a Custom Control
The Only ways to instantiate a C#CustomControl that will keep its reference to a uxml/uss file is to get a reference to the uxml parent that has a C#CustomControl as a child element:

  1. Using Direct Path
VisualTreeAsset uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/UI/Example.uxml");
VisualElement tree = uxml.Instantiate();
  1. Using Resources Api - Requires a folder named “Resources” that contains the uxml file.
var uxml = Resources.Load<VisualTreeAsset("Example.uxml");
VisualElement tree = uxml.Instantiate();

Grumble
Would be nice if there was a way to Instantiate Custom Controls without Direct Paths or Resources folder.

The answer to my original question
Refer to MainWindow.cs on line 21 at the top of this thread.
Example code to spawn five popups and set the Label text:

private void OnHeaderButtonClicked()
    {
        // Get a reference to uxml file
        VisualTreeAsset uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/UI/Popup.uxml");
        int numberOfPopupToSpawn = 5;

        for (int i = 0; i < numberOfPopupToSpawn; i++)
        {
            // Create a new tree with references to uxml/uss
            var popup = uxml.Instantiate();
            popup.Q<Label>("PopupLabel").text = "Method that returns a title by ID";
            // In this example "this" = another custom control
            this.Q("Body").Add(popup);
        }
    }

Further Reading
If you are wondering how to programmatically create Custom Controls with uss references, this series is for you.

CAREFUL!
AssetDatabase.LoadAssetAtPath does not work during runtime.