Radio Buttons/Controls

How does one create a group of Radio Controls using UIElements? IMGUI has this feature and is able to represent a Toggle as either a Radio or a Checkbox. It also supports the ability to logically group checkboxes together.

What is the UIElements equivalent?

1 Like

There is nothing out of the box, however here is an implementation of a element that will scan all its descendant toggle elements and create a radio button behavior.

// This will catch all toggle change events and add a radio button behavior to them
// It also provides setting/getting the index of the currently selected toggle.
// No styling provided
public class RadioButtonGroup : BindableElement, INotifyValueChanged<int>
{
    public new class UxmlFactory : UxmlFactory<RadioButtonGroup, UxmlTraits> {}

    private UQueryState<Toggle> toggleQuery;
 
    public RadioButtonGroup()
    {
        //we cache this query to avoid gc allocations each time we need to run it.
        toggleQuery = this.Query<Toggle>().Build();
     
        //This means we get notified of all ChangeEvents<bool> in our descendants
        RegisterCallback<ChangeEvent<bool>>((evt) => OnToggleChanged(evt));
    }

 
    public int SelectedIndex
    {
        get
        {
            //We find the first Toggle
            int index = -1;
            bool found = false;
            toggleQuery.ForEach((toggle) =>
            {
                if (!found)
                {
                    ++index;
                    found = toggle.value;
                }
            });

            if(found)
                return index;
 
            return -1;
        }
        set
        {
            int index = -1;
            toggleQuery.ForEach((toggle) =>
            {
                ++index;
                if (index == value)
                {
                    toggle.value = true;
                }
            });
        }
    }
 
    void OnToggleChanged(ChangeEvent<bool> evt)
    {
        Toggle t = evt.target as Toggle;

        if (t != null)
        {
            if (evt.newValue)
            {
                //User selected a new toggle, we need to disable all the others
                int index = -1;
                int newValue = -1;
                int previousValue = -1;
             
                toggleQuery.ForEach((toggle) =>
                {
                    ++index;
                    if (ReferenceEquals(t, toggle))
                    {
                        newValue = index;
                    }else
                    {
                        if (toggle.value)
                        {
                            previousValue = index;
                        }
                     
                        toggle.SetValueWithoutNotify(false);
                    }
                });
             
                evt.StopPropagation();

                using (var newEvent = ChangeEvent<int>.GetPooled(previousValue, newValue))
                {
                    newEvent.target = this;
                    SendEvent(newEvent);
                }
            }
            else if(evt.previousValue)
            {
                //You can't unselect the currently selected toggle
                evt.StopPropagation();
                t.SetValueWithoutNotify(true);
            }
        }
    }

    void INotifyValueChanged<int>.SetValueWithoutNotify(int newValue)
    {
        SelectedIndex = newValue;
    }

    int INotifyValueChanged<int>.value
    {
        get => SelectedIndex;
        set => SelectedIndex = value;
    }
}
3 Likes

Awesome! Thanks @uMathieu !

Can you go a step further and help me understand the purpose of the following elements in your example?

  • INotifyValueChanged<T>
  • UxmlFactory<RadioButtonGroup**, UxmlTraits**> - Is the UxmlTraits class necessary? Could you omit it?
  • UQueryState<T>
  • ChangeEvent<T>.GetPooled

Is there documentation for us to follow somewhere that will inform us about how to properly leverage this stuff?

  1. INotifyValueChange is the base interface for fields that contains a typed value and send ChangeEvent when value changes. This was a bit overkill for this example… so I edited the code so that the class inherits from BindableElement. Implementing IBindable and INotifyValueChange allows the element to be bound to SerializedObject/SerializedProperties.
  2. In the original case, the since the UxmlTraits refered to the VisualElement one, it was redundant. However, now, it exposes the BindableElement attributes to UXML
  3. UQuery is a way to search for elements in a hierarchy by creating uss selectors from code. The QueryState is a saved selector, applied on a root element. Special care was taken to avoid gc allocations while iterating and evaluating the queries. If the basic selectors + ForEach are now enough, you can still use ToList() then have fun with LINQ :slight_smile:
  4. Again, this is made to avoid gc allocations. Since events are often used but only live for a short time, we use an object pool. This is not mandatory, allocating them with new will work, it just puts more strain on the garbage collector.

The only documentation we have for now is the rather short development guide:

and the scripting api documentation:

We’re currently working with the documentation team to improve this. In the meantime don’t hesitate to ask questions here.

you are still creating GC because of the lambda closures (they capture locals, this requires at least 2 heap allocations – one for the closure and one for the delegate)

you could add IndexOf and similar APIs for these use cases, or implement IEnumerable<T> to allow foreach (var e in query) {...} (compiler can foreach without boxing if the IEnumerable pattern is fully visible to it)
example foreach without GC

struct a {
        public b GetEnumerator() {return new b();}
    }
    struct b {
        public int Current {get;set;}
        public bool MoveNext() {return true;}
    }
    public static void Main()
    {
       
        foreach (var c in new a()) {
            Console.Write("ok");
            break;
        }
    }
1 Like

I haven’t profiled to verify this but it does match with my expectations.

@uMathieu have you guys done any profiling that shows that this pattern (closures) actually doesn’t allocate?

Sure. But couldn’t you also do this (which is more clear and more maintainable)?

int index = 0;
UQueryState<Toggle> toggle = toggleQuery.AtIndex(index);

while (toggle != null)
{
    if (ReferenceEquals(t, toggle))
    {
        newValue = index;
    }
    else
    {
        if (toggle.value)
        {
            previousValue = index;
        }
       
        toggle.SetValueWithoutNotify(false);
    }

    toggle = toggleQuery.AtIndex(++index);
}

What? How?

How does the binding system work? What are the inputs/outputs? What connections are made?

I have no idea how to link what you’ve written to the [not terribly helpful] documentation.

What? What does that sentence even mean?

What are BindableElement attributes? How are they different from VisualElement ones? If you’re extending the BindableElement class wouldn’t it still be redundant to specify the UXMLTraits there due to the fact that one would assume that the BindableElement class would have already handled that internally? Or is this necessary because we’re totally shadowing the internal version? If it’s the shadowing thing, isn’t this a bit of a code smell thing? It’s not terribly discoverable - you’d kind of just have to “know”…

Got it.

Some comments in that code snippet about what you’re actually doing and why would be very helpful, I think - particularly for anyone else who happens to say “how do you do a radio button with UIElements”. Lots of learning opportunities in there and without the comments, you’re just kind of left scratching your head about what the code is and does and why any of it is necessary…

1 Like

AtIndex traverses the tree on each invocation (https://github.com/Unity-Technologies/UnityCsReference/blob/02d565cf3dd0f6b15069ba976064c75dc2705b08/Modules/UIElements/UQuery.cs#L260)
and personally I think iterating via foreach is better for readability/maintenance (unless you actually need the index for other calculations)

at this point it may be better using ToList(results) with a preallocated list and iterate over that.

Well, isn’t that just a shiny detail that belongs in the documentation.

I agree. I’m just not a fan of adding and maintaining all the scaffolding code required to support it when you can get by without it.

Ahh, I didn’t realize that you could pass in a List for it to fill. That would definitely be preferable to the while+AtIndex…

Using this in live code (for UIToolkit that was launched without this core class) I discovered that - of course! - it corrupts Unity prefabs: I think because it doesn’t actively do anything with the binding part. I’m currently experimenting with reverse-guessing WTF is needed to make binding work (from my previous investigations into the Binding docs, that were only published this year, more than two years after this thread (!)), hoping it’s straightforward.

TL;DR: do not use the original code snippet provided at top of page, it will corrupt your prefabs (they don’t save properly, and they actively overwrite (randomly) ‘real’ values with fake ones. I believe it’s something with serialized data getting pre-empted. It looks to me like it can be easily fixed by simply adding the missing calls to Bind() in the right places. If you can figure out exactly what those are (as far as I know: tehre is still literally no way to debug Binding problems?)

I appear to have got it working, by taking the example code in the new docs and extrapolating in obvious ways.

However this now triggers an annoying dumb (dumb because: it seems to be wrong: all the code is correct, everything is working (where it didn’t before!), so UIToolkit is generating a fake error case here?) message from UIToolkit, a “Warning” that spams the console - “Field type is not compatible with Enum property” - which appears to be this: EnumField is not compatible with Enum property

I’m 99% sure that one of the most common cases for RadioButtonGroup is to represent an Enum (I mean … that’s pretty much literally what it is: a visual representation of a multi-value type where only one value can be active at once). But apparently some (undocumented?) magic is required to make UIToolkit stop complaining that you’re not using a ‘UIToolkit blessed’ EnumField. Presumably something can be implemented to make this message go away - any ideas?