How to bind sub-elements and methods to custom controls?

Hi everyone,

I want to practice better separation of logic and data in my Unity 6.3 application, in which I use UI Toolkit.

Example: I have a set of three buttons that I need to use repeatedly. I was wondering if I can use custom controls to set up the connection of the buttons to the logic I want to execute on button click, since unfortunately, UI Toolkit cannot bind the buttons to methods via the UI Builder like uGUI can via the inspector. I already use custom controls for more complex UI elements anyway.

Now, for my custom control code to bind the button click events, it needs to know the buttons first. However, I could not figure out an elegant way to do so. I could use Q<>(), but that requires string lookup. I’d rather bind the buttons in the UI Toolkit inspector.

Here’s a piece of code to illustrate what I tried:

    [UxmlElement]
    public partial class MyClass : GroupBox {
        [UxmlAttribute] public VisualTreeAsset someTemplate; // this works, someTemplate shows up in UI Toolkit inspector

        [UxmlObjectReference("giveMeButton")] public Button giveMeButton; // this complains that Button is not a UxmlObject type
        [UxmlAttribute] public Button giveMeButton; // this complains that there is no UxmlAttributeConverter

        public MyClass() {
            RegisterCallback<AttachToPanelEvent>(e => {
                Button b = this.Q<Button>("MyButton"); // this works but I want to get rid of the string lookup
                b?.RegisterCallback<ClickEvent>(evt => Debug.Log("BOOOM"));
            });
        }
    }

That led me to looking at converters and I found Unity - Scripting API: UxmlAttributeConverter<T0> which is supposed to list the available converters. However, it does not list a converter for VisualTreeAsset, and yet I can use that, so I guess the list is incomplete.

Is what I attempted above possible? Also, does it make sense? My motives are the following:

  • I do not want to bind all the click button logic in a UI manager.
  • I want to have the logic for different UI blocks separate from each other.
  • I do not want to create a controller class for each of my UI elements, since in cases where I need a custom control anyway, that would cause me to have a class for the custom control, a class for the data and a class for the logic, which would all have to be managed by my UI manager, which is a bit much. Hence the idea to have the logic in the custom control code.

Fortunately. That was the biggest pain point in UGUI. Something ain’t responding? Check somewhere deep in the hierarchy whether the event is still registered. Likely you ran some refactoring, changing the method name or signature and bam, broken UI just like that.

There’s no Inspector binding for UI elements as far as I know. And it’s not bad. Consider that the assigned reference might just as well become “missing” as renaming a string might fail finding that element. It’s actually very rare that an element gets renamed in my experience.

What I’ve done is expose the button’s name in the Inspector so I can change it without having to recompile if the need arises.

My current setup:

HUD.uxml

This has anchors for Center, four corners and four edges. I drop other UXML in there as template containers.

For instance the Menu.uxml is dropped into the Edges/Left element. The debug info goes into Corners/BottomRight.

I have a single HUD object in the scene with a HUD script which is practically empty (it only turns the cursor on/off in the menu/pause screens). Then I have scripts for each panel on the same object, ie MenuController, DebugInfoController, SkillProgressController. They all get the UI document and then query it for their elements. This happens up front in Awake/OnEnable.

To hide panels I simply set their flex style to None, I don’t remove, destroy, or deactivate any UI elements.

So each panel is self contained, doesn’t care where exactly within the HUD it is, and I haven’t done a single VisualElement subclass in this project and very rarely in all past projects.

Subclassing elements is mostly meaningful if you have a very specialized, reusable control element such as a dropdown with specific styling requirements and what not. I would not subclass an element just to group elements, handle callbacks, handle animations and styling changes. This can all comfortably be done by a controller script, a regular MonoBehaviour that has access to either its panel’s UI document or is able to find “itself” in the UI document hierarchy - but for them to know their own “root” element is mainly to avoid naming clashes when using Q<>.

The dependency hierarchy would be:

  • UI Manager (whatever that may be or do)
    • PanelScript
      • CustomElement
        • CustomElementData
        • CustomElementLogic

So you’ll have a wrapper class for the custom control, and for the most part the logic should go in there not a separate script. Outside scripts interact with the element’s main script, not with its logic nor data classes. The Data class is only necessitated for runtime data binding, and would likely be a regular C# class.

But let’s talk about WHY you subclass VisualElement? What does the subclass accomplish that you couldn’t do otherwise? A VisualElement is warranted only if it needs specialized but reusable logic or custom rendering that cannot be accomplished with styling alone. As to data, this should be provided from the outside via bindings.

If you make a Messagebox with a title, message, OK, Cancel, Try Again buttons and some styling: that’s a UXML template, not a subclass. If you need a list where each item has a border frame, an image, a label, and a checkbox: that’s a UXML template, not a subclass. If you need progress bar for a resource with labels for absolute values which slides in/out of the screen whenever the user acquires the resource: that’s a UXML template, not a subclass. All of these would have a companion controller script handling their concerns.

If you need to have an element that renders a 2D icon from the player’s 3d character with its equipment and post fx applied: that’s a subclass.

In the GameObject inspector, when you have a prefab with two MonoBehaviours and one MonoBehaviour has a field that takes the other MonoBehaviour, this connection will not break on rename. Also, you can rename GameObjects and MonoBehaviours that hold references to these GameObjects will not lose the references. So I don’t know why you think it would go missing. I was hoping there was something similar for UI documents.

For cases where subclassing VisualElement is not necessary, I don’t understand your wrapper class approach. To me, that looks even more complicated than having just a controller class.

For the rest, I take it that you agree that having the logic inside the custom control class is a good idea. It is a little hard to understand, since your post contains so much opinion and comments on other topics. This contradicts not creating custom controls for the logic only though.

All my UI is generated at runtime, so I already have a UI manager that creates instances of controls and data classes and links them. I could have it create instances of controller classes and link them, too, and it looks like that would be the cleanest approach. The controller would need to know both the data and the control, since it would bind control events to logic that uses data. If I did that, I would not put any logic inside custom control classes though, because that would be inconsistent.

If anyone who reads this knows that there actually is a way to reference UI elements in the UI builder inspector, I’d still like to know. Or maybe there are plans to add this in future versions. Until then, I’ll go with Q<>() as before and use that from inside the custom control’s class.

If this is about referencing child elements, can you do what Unity does for all their custom controls?

[UxmlElement]
public partial class SomeElement : VisualElement
{
	public SomeElement()
	{
		_button = new Button();
		Add(_button);
	}
	
	private readonly Button _button;
}

If you’re coding custom controls when you might as well take advantage of being able to establish references in the constructor.

I could do that, but I’d have to create all the controls in code like in your code example, requiring me to also take care of setting the correct classes for USS in code. That would leave me with just another string connection that might break. I would also lose the benefits of the UI Builder for more complex hierarchies.

You can just define const or static strings for USS styles.

You’ll find it doesn’t really cause issues. There’s no avoiding using strings in some form.