How to add scripts to instantiated template.uxml files?

Hi, I can’t seem to find the resources I need, all tutorials go around creating a view by script or having a finished view and accessing its VisualElements. But what we often need to do, is have an empty shell like a scroll view and instantiate a ton of prefabs in there that all have their own script and can act on their own.
This is exactly what I need to replicate with UI Toolkit.

What we would do in normal Unity UGUI is this:

[SerializeField] private Product productTemplate;
[SerializeField] private RectTransform scrollViewContent;

public void SpawnTemplates(int amount)
{
    foreach (int i = 0; i < amount; i++)
        Instantiate(productTemplate, scrollViewContent);
}

pretty easy and straight forward. We have our template (the productTemplate as a Prefab) and then just spam it into a UI element. The Product script is already attached and can do stuff on its own on every individual instance.

So what is the equivalent for UI Elements? I already created my ProductTemplate.uxml to spawn in, and I can already do that:

[MenuItem("Window/UI Toolkit/MyEditor")]
public static void ShowExample()
{
    MyEditorWindow wnd = GetWindow<MyEditorWindow>();
    wnd.titleContent = new GUIContent("MyEditor");
}

public void CreateGUI()
{
    VisualElement root = rootVisualElement;
    VisualTreeAsset uiAsset = TemplateFactory.ProductTemplate;
    VisualElement ui = uiAsset.Instantiate();
    root.Q("ProductList").Add(ui);
}

But how on earth to I add a script to these uxml elements?
I mean they are there, but they are basically useless. If I click their buttons, nothing will happen, they need a script that can handle their individual state somehow.

It would also be fine – even preferred – if the script is already attached somehow, so that every ProductTemplate.uxml automatically comes with the correct behavior. I find a lot on how to manipulate VisualElements via code, but I can’t find a way to give a layout its own script by default. Hope someone can guide me in the right direction!

Best,
Noblauch

Not sure what you mean by 'give a layout its own script". Visual elements aren’t game objects with monobehaviour components. UI Toolkit generally follows a model-view pattern. Visual elements are just there to look good, and your editor window handles the behaviour (or in the case of runtime, one or more monobehaviours can control the UI).

You don’t have to stick to this though. You can write custom visual elements, of which express your intended behaviour: Unity - Manual: Expose custom control to UXML and UI Builder

Note the workflow of creating custom controls is a lot better in 2023+ (or Unity 6, I guess?) versions of Unity.

My general workflow is a bit of a hybrid between the both of these. I do write a lot of custom controls, in general, as I just find it easier to encapsulate the general behaviour into the visual elements.

Hey, thanks for the answer, I think I figured it out in the night already and just wanted to post my solution for others here. And yes: I had to write my own VisualElement script, indeed this seems to be the new prefab flow for UI Elements, which is quite tedious in my opinion, but sure I need to get used to it first.

EDIT:
Better solution in my next post!

So everybody coming from UGUI, this is how I did it, I wrote this code:

public class ProductTemplate : VisualElement
{
    public new class UxmlFactory : UxmlFactory<ProductTemplate> {}

    public void Display(Product product)
    {
        var appIcon = this.Q("AppIcon");
        var productName = this.Q<Label>("ProductName");
        var productIdentifier = this.Q<Label>("ProductIdentifier");

        productName.text = product.Name;
        productIdentifier.text = product.Identifier;
        appIcon.style.backgroundImage = new StyleBackground(product.Icon);
    }
}

And now instead of using a VisualElement as the root object in my .uxml template, I use my script reference (ProductTemplate.cs) like this:

<!-- bla bla bla my layout here -->
</MyFolder.Views.Templates.ProductTemplate>```

Now on how to use it:

```csharp
public void SpawnProducts(Product[] products)
{
    var uxml = TemplateFactory.ProductTemplate;
    var productList = rootVisualElement.Q("ProductList");
    foreach (var product in products)
    {
        var instance = uxml.Instantiate();
        var pt = instance.Q<ProductTemplate>("ProductTemplate");
        pt.Display(product);
        productList.Add(instance);
    }
}

Hey, if you can upgrade now or later to 2023.2+, I recommend having a look at the new binding system for UI Toolkit. Should make your use case a lot less tedious. Here’s the main doc for the feature:

And more video-first intro to binding was made for the last Unite:

I would also recommend reading the following page:

That was a good example source! I ended up refactoring my code to the Element-first approach, because

  • It’s cleaner (requires less steps)
  • Gets rid of the code dependency inside the .uxml document, which is literal hell to maintain when moving namespaces or scripts

For anyone wondering, the example above refactored to the Element-first approach looks like this:
UXML

<ui:UXML xmlns:ui=“UnityEngine.UIElements” xmlns:uie=“UnityEditor.UIElements” editor-extension-mode=“True”>

C#

public class VisualProduct : VisualElement
{
    private Product _product;
 
    public new class UxmlFactory : UxmlFactory<VisualProduct> { }
    public VisualProduct() { }

    public VisualProduct(Product product)
    {
        var template = TemplateFactory.VisualProduct;
        template.CloneTree(this);
        Display(product);
    }
 
    public void Display(Product product)
    {
        _product = product;
        var appIcon = this.Q("AppIcon");
        var productName = this.Q<Label>("ProductName");
        var productIdentifier = this.Q<Label>("ProductIdentifier");
    
        productName.text = product.Name;
        productIdentifier.text = product.Identifier;
        appIcon.style.backgroundImage = new StyleBackground(product.IconTexture);
    }
}

Usage in parent script:

private void SpawnProducts(Product[] products)
{
    var productList = rootVisualElement.Q("ProductList");
    foreach (var product in products)
    {
        var visualProduct = new VisualProduct(product);
        productList.Add(visualProduct);
    }
}

Cheers!

!!! Warning !!!

While incorporating this technique into my workflow, it turned out that this approach is not clean and causes issues along the line.
The main problem is, that the above described example produces an extra XML element around your actual UXML code.

Let me share a simple example with expected and actual UXML generated.
Template UXML

<ui:UXML xmlns:ui="UnityEngine.UIElements" editor-extension-mode="True">
    <ui:VisualElement name="BridgeTabButton" class="bridge-tab-button">
        <ui:VisualElement name="Icon" class="bridge-tab-button-icon" />
        <ui:Label name="Label" text="Store Name" />
    </ui:VisualElement>
</ui:UXML>

C#

public class BridgeTabButton : Button
{
    public new class UxmlFactory : UxmlFactory<BridgeTabButton> { }
    public BridgeTabButton() { }

    public BridgeTabButton(IStoreBridge storeBridge)
    {
        var template = ViewFactory.BridgeTabButton;
        template.CloneTree(this);
        Display(storeBridge);
    }

    private void Display(IStoreBridge storeBridge);
}

I would now expect, that the provided “Prefab” (.uxml template) gets the BridgeTabButton script attached to the first element, something like this:
Expected Result

<UXML xmlns:ui="UnityEngine.UIElements">
  <BridgeTabButton name="BridgeTabButton" class="bridge-tab-button">
    <ui:VisualElement name="Icon" class="bridge-tab-button-icon" />
    <ui:Label name="Label" text="AppStore" class="unity-text-element unity-label" />
  </BridgeTabButton>
</UXML>

But now see what actually happens, which was not a problem for simple objects:
Actual Result

<UXML xmlns:ui="UnityEngine.UIElements">
  <BridgeTabButton class="unity-text-element unity-button">
    <ui:VisualElement name="BridgeTabButton" class="bridge-tab-button">
      <ui:VisualElement name="Icon" class="bridge-tab-button-icon" />
      <ui:Label name="Label" text="AppStore" class="unity-text-element unity-label" />
    </ui:VisualElement>
  </BridgeTabButton>
</UXML>

As you can see, the BridgeTabButton was added around my template .uxml, firstly breaking the layout hierarchy and not respecting the styling of the class .bridge-tab-button. And secondly introducing extra unwanted styling on top, which finally breaks the complete appearance of the element by adding class=“unity-text-element unity-button”. This happens, because BridgeTabButton derives from Button which gets the automatic styling treatment.

If there was a simple solution to this, I’d share it. But from the length of this post you can already see, that there is no obvious fix. I tried for hours, and this finding punched me back to the state of not knowing How to add scripts to instantiated template.uxml files.

Hope someone can suggest alternatives.
It feels very annoying that there doesn’t seem to be a default way on how to spawn Prefabs like we are used to in UGUI. It’s a process so basic and commonly used, that it really frustrates me and blocks my progress with UI Toolkit.

A dirty workaround, which doesn’t make the prefab previewable anymore and moves part of the template UMXL code to the C# class would be this:

Template UXML

<ui:UXML xmlns:ui="UnityEngine.UIElements" editor-extension-mode="True">
    <ui:VisualElement name="Icon" class="bridge-tab-button-icon" />
    <ui:Label name="Label" text="Store Name" />
</ui:UXML>

C#

public class BridgeTabButton : Button
{
    public new class UxmlFactory : UxmlFactory<BridgeTabButton> { }
    public BridgeTabButton() { }

    public BridgeTabButton(IStoreBridge storeBridge)
    {
        var template = ViewFactory.BridgeTabButton;
        template.CloneTree(this);
        name = "BridgeTabButton";
        AddToClassList("bridge-tab-button");
        Display(storeBridge);
    }

    private void Display(IStoreBridge storeBridge);
}

But this way you have to remove the actual prefabs root from the .uxml and recreate it via code. This unfortunately mixes code and layout up again which was cleanly separated before. Also you aren’t able to work with the template in UI Builder anymore, because the layout is now broken by default.

It does work, but nothing feasible in production unfortunately…