Binding and Custom Controls

I have been away from Unity for about 2 years and coming back now, with a new project and I’m now evaluating UI workflows.

My Idea was to go with UI Toolkit as the stylesheets make it easy to stay consistent with the UI and I thought since it now supports runtime bindings and so on …

So here I am, trying to create a custom control with binding.

The documentation Unity - Manual: Bind a custom control features a quite bad example IMHO, as it actually does not really show how custom bindable controls work, it is just defered to the inner ObjectField.

You see, my example is simpler… in that regard:

[UxmlElement("WidgetSwitcher")]
public partial class WidgetSwitcher : BindableElement, INotifyValueChanged<int>
{
    private int _activeWidgetIndex;
    
    public WidgetSwitcher()
    {
        RegisterCallback<GeometryChangedEvent>(_ => UpdateVisibility());
        UpdateVisibility();
    }

    private void UpdateVisibility()
    {
        for (int i = 0; i < childCount; i++)
        {
            this[i].style.display = i == _activeWidgetIndex ? DisplayStyle.Flex : DisplayStyle.None;
        }
    }
    
    public void SetValueWithoutNotify(int newValue)
    {
        _activeWidgetIndex = newValue;
        UpdateVisibility();
    }

    int INotifyValueChanged<int>.value
    {
        get => _activeWidgetIndex;
        set
        {
            if (_activeWidgetIndex == value) return;
            var previous = _activeWidgetIndex;
            SetValueWithoutNotify(value);
            using (var evt = ChangeEvent<int>.GetPooled(previous, value)) SendEvent(evt);
            this.NotifyPropertyChanged("value");
        }
    }
}

A widget switcher, that just switches between it’s children. so It is supposed to bind to a int property.

It’s inheriting from BindableElement and INotifyValueChanged and in the UI Builder I set it to bind to a int property

                <WidgetSwitcher name="WidgetSwitcher" data-source-type="MyDataType, Game02.Runtime" data-source-path="typeIndex">
                    <ui:Button text="Button" />
                    <ui:Label text="Label" />
                    <ui:Toggle label="Toggle" />
                </WidgetSwitcher>

a label next to it shows me, that the int property is set correctly while the WidgetSwitcher just shows me the first child.

So, maybe I need to add a property with a [UxmlAttribute] to my widget and do the binding there?
Sadly for a property I cannot set up the binding in the UI Builder, but for a public field I can, so going with that:

[UxmlElement("WidgetSwitcher")]
public partial class WidgetSwitcher : BindableElement, INotifyValueChanged<int>
{
    [UxmlAttribute("active-widget-index")]
    public int _activeWidgetIndex;
    ...

and with that binding … it “works” … but also not right away. I press play, my UI is loaded and populated and the labels are all fine, but the widget switchers are all showing buttons (index=0), but when I then adjust the size of the Game-Panel inside the editor, then all of the Widgets get refreshed I guess and it shows the correct ones.

But ultimately it cannot be the solution to have it a public field, because then I have no way to do things in the setter (which also likely is the issue with the field approach only updating after receiving a refresh)

I guess I’m doing something super simple wrong, but I have no idea what.

You could use a property

int m_ActiveWidgetIndex 

[UxmlAttribute("active-widget-index")]
public int _activeWidgetIndex 
{
  get => m_ActiveWidgetIndex ;
  set 
  {
    m_ActiveWidgetIndex  = value; 
    UpdateVisibility();
  }
}

You may find it easier to inherit from BaseField<int> for this as it implements a lot of the functionality you need.

so, yeah, I tested it already (before posting and now again), it still does not work. because I cannot setup the binding in a way that would work. (I cannot bind the “active-widget-index”)

and whatever i set in the Bindings/… at the top just does not work.

but then I have an Integer field, with prefix label and a lot of uss classes that I don’t care about

What are you trying to bind to, and is this for Editor or Player?

    [System.Serializable]
    internal struct MyStruct : IEquatable<MyStruct>
    {
        [CreateProperty] public string name => setting.name;
        [CreateProperty] public int typeIndex => (int)myEnum.type;
        ...
    }

A labels text property can be set-up to bind to either “name” or “typeIndex” and work fine. Since one of them work just fine, it cannot be the binding source.

Also: binding to a field get’s also populated with the correct value, but does not update on it’s own (at least it looks like that to me), so that also shows it cannot be the binding source.

Ok sounds like you will need a Two Way binding. You can configure it in the UI Builder or via script.
Right click on the activeWidgetIndex field in the UI Builder inspector and choose “Add Binding”. You can now configure its data source and the type of binding.

Or via script: Unity - Manual: Create a runtime binding in C# scripts

You dont need INotifyValueChanged or BindableElement, they are both for Editor binding.

It should look something like this:

<WidgetSwitcher name="WidgetSwitcher" data-source-type="MyDataType, Game02.Runtime" data-source-path="typeIndex">
     <Bindings>
        <ui:DataBinding property="active-widget-index" binding-mode="TwoWay" />
    </Bindings>
    <ui:Button text="Button" />
    <ui:Label text="Label" />
    <ui:Toggle label="Toggle" />
</WidgetSwitcher>

WHERE: The data source part informs the system where the data is.
WHAT: The Binding part configures the field and tells it what data to bind from the source.

as you can see in the screenshot I added, there is no “Add Binding” when using a property. When using a field I cannot call UpdateVisibility() when it will change.

added the <Bindings> .... xml manually does also not make it work.

Why would I need a two way binding? I just want to bind from my datasource to the ui. so a ToTarget binding should work perfectly fine (as it does with the label).

Just to test, I’ve set it to two way. And it behaves exactly the same: it does not work until i resized the Game Panel.

Okay, good to know. That makes the code smaller …

(this is probably one of the messiests documentation pages for unity.)

I can’t really set it up via C# (sure I can, but then I would question why to use the UI Builder at all, and if that is the result of this evaluation then UI Toolkit is just dead for this project, as I can’t tell our UI designer to do half of it in C#)

I have a List<MyStruct> that is used as source for a ListView, within the ListViews child template I’m having the issues. (The label works fine though…)

so if i wanna do the binding from C# I’d have to iterate over all the children of the ListView after they are created … or not use a ListView at all… (again, then I don’t need UI Toolkit)

Alright I figured it out.

To have a property to support bindings it needs the [CreateProperty] attribute. This isn’t mentioned anywhere explicitly. While there is a code snippet in “Unity - Manual: Create a runtime binding in C# scripts” but it is in the context of using an VisualElement itself as a DataSource (which isn’t what I’m doing - technically)

so, this works (leaving it here for future reference):

using Unity.Properties;
using UnityEngine.UIElements;

[UxmlElement("WidgetSwitcher")]
public partial class WidgetSwitcher : VisualElement
{
    private int _activeWidgetIndex;
    
    [CreateProperty]
    [UxmlAttribute("active-widget-index")]
    public int activeWidgetIndex
    {
        get => _activeWidgetIndex;
        set {
            if (_activeWidgetIndex == value) return;
            _activeWidgetIndex = value;
            UpdateVisibility();
        }
    }
    
    public WidgetSwitcher()
    {
        RegisterCallback<GeometryChangedEvent>(_ => UpdateVisibility());
        UpdateVisibility();
    }

    private void UpdateVisibility()
    {
        for (int i = 0; i < childCount; i++)
        {
            this[i].style.display = i == _activeWidgetIndex ? DisplayStyle.Flex : DisplayStyle.None;
        }
    }
}

then i can setup the binding in the ui builder and the setter is used. also no TwoWay binding needed.

thank you for the working example
i wish it would be in the documentation, like in the section (Structure UI / Custom controls / Bind custom controls to data - with a subsection for runtime binding)

and a reference to that example in Data binding / Runtime data binding / Get started with runtime binding → additional resources

You can provide feedback on the docs by clicking the “Report a problem on this page” at the bottom. Choose an option, pick provide more information and it will give you a text box where you can add some more details. These get turned into documentation tasks for the team.