Custom inspector built with UIBuilder. How to hook up buttons?

So this post is kind of two fold.

First from a user standpoint, I feel there is a large piece missing from UI Builder. In uGUI you could wire up your events directly in the inspector. Shouldn’t this functionality also exist in UIBuilder? Or is the underlying system so different that this isn’t practical?

Which leads to the second point, trying to understand HOW the underlying system works. I have made a simple inspector using UI Builder. It has some property fields and a couple buttons for now. Nothing amazing. Now I want to hook those buttons up to do something. I can’t seem to figure it out though. I tried some code that seems like it should work based on the docs, but doesn’t appear to do anything. It doesn’t error, or throw any warnings, it just does nothing when I click on the button.

    [CustomEditor(typeof(Poisson))]
    public class PoissonEditor : UnityEditor.Editor
    {
        public override VisualElement CreateInspectorGUI()
        {
            var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/PoissonInspector.uxml");
            var root = visualTree.CloneTree();
            root.styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/PoissonInspector.uss"));
           
            root.Query<Button>("sample").First().RegisterCallback<MouseDownEvent>(SamplePoints);
            return root;
        }

        private void SamplePoints(MouseDownEvent evt)
        {
            Debug.Log("Mouse Down");
            Poisson p = target as Poisson;

            if (p != null)
            {
                p.CreatePoints();
            }
        }
    }

For reference here is the super basic uxml as well:

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
    <ui:VisualElement style="flex-grow: 1;">
        <Style src="PoissonInspector.uss" />
        <uie:PropertyField name="density" view-data-key="density" binding-path="density" label="Point Density" />
        <uie:PropertyField name="minRadius" binding-path="minRadius" label="Minimum Point Radius" />
        <uie:PropertyField name="maxRadius" binding-path="maxRadius" label="Maximum Point Radius" />
        <uie:PropertyField name="size" binding-path="size" label="Area" />
        <uie:PropertyField name="prefab" binding-path="prefab" label="Prefab to Spawn" />
        <uie:PropertyField name="image" binding-path="image" label="Image Influence" />
        <ui:Button text="Generate Points" name="sample" binding-path="sample-points" />
    </ui:VisualElement>
</ui:UXML>

So it turns out that for whatever super confusing reason button events are handled differently than how the docs show events are done. The following code works:

    [CustomEditor(typeof(Poisson))]
    public class PoissonEditor : UnityEditor.Editor
    {
        public override VisualElement CreateInspectorGUI()
        {
            var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/PoissonInspector.uxml");
            var root = visualTree.CloneTree();
            root.styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/PoissonInspector.uss"));

            root.Query<Button>("sample").First().clicked += SamplePoints;
            root.Query<Button>("clear").First().clicked += ClearPoints;
            return root;
        }

        private void ClearPoints()
        {
            Poisson p = target as Poisson;

            if (p != null)
            {
                p.Clear();
            }
        }

        private void SamplePoints()
        {
            Poisson p = target as Poisson;

            if (p != null)
            {
                p.CreatePoints();
            }
        }
    }

but it does allow more options for instances
toggle.RegisterCallback<ChangeEvent>(x =>
{
Method0(some key);
Method1(some other value);
};

but you have to watch yourself with this, if you assign the value in a loop, it will be the last value of the loop (if a for int loop then the last value + 1) when adding button manual using the style or itinerating through the tree.

for (int n = 0; n < list.Length; ++n)
{
Button btn = new Button() {text = “something”};
btn.clickable.clicked += () =>
{
Debug.Log(n);
}
}

if the list is 2 long then it will always print 2 (the last value + 1)
so you will have to code it like this
for (int n = 0; n < list.Length; ++n)
{
Button btn = new Button() {text = “something”};
btn.clickable.clicked += () =>
{
int j = n;
Debug.Log(j);
}
}

I ran into exactly this problem too. Surprisingly it works with right click though. Its frustrating to do something that should be simple (changing MouseUp to MouseDown) and have it not work.

What is frustrating to me is just the inconsistency. Why are some events one way and click events another, more complicated, way?

Hi, a built in ClickEvent will be available starting with 2020.1.
This will make it possible to register a callback on elements like the rest of the API.

1 Like

On the same topic, the ability to hook up the callbacks through inspector (not through code) will be deprecated? :frowning: Couldn’t find any info anywhere regarding that.

Yeah that part of my original post I think was lost in the shuffle, but would like to know if there are plans to allow editor side (design side) hook up of UI events like you can currently do with uGUI.

I wouldn’t say deprecated is the right word, it might be different but it’s hard to tell right now how it gonna ends up.

Could you please go a little over why this is important to you? Giving some context would also help: are you the one implementing the UI or setting up logic? The size of the team working on the project?

Just some context. One of the goals of UIElements was to be able to use plain source assets for the look and feel of your UI, separate from functionality.

The UI look-and-feel assets (UXML and USS) are more like texture assets than ScriptableObject assets. You can load them at any time, from anywhere, and just paste them on the screen as many times (copies) as you want.

When you want to attach some functionality, like make a Button call a function, you explicitly look for that Button in your current UI on screen and if you find it, you attach yourself to the correct events (not saying that current inconsistent way of doing this with .clickable.clicked = func is good, just the higher level query+events pattern). Whoever registered this callback should not care where the UI is, where it was loaded from, when it was loaded, nor where the Button is in the UI hierarchy.

The UI Builder, in its initial form, will focus on creating the look and feel of the UI, not the logic. You can use the UI Builder to set up markers in the UI so the code can easily find specific functional elements (like give them a unique name), but the assets you create stay independent from the code.

There are also performance benefits to keeping logic all in C# and away from string-based UXML files. Optimizations can be made to the code but if you reference functions in UXML, you have to rely on runtime-reflection every time you load these files.

You also don’t get help from your IDE when looking for where a function is being call and when you use automatic refactoring tools, they will not affect any references to your code in your UXML files.

So while convenient, the uGUI Events/callback system has its problems - problems that we didn’t want to bring into UIElements, as-is, for the reasons above.

That said, we do want the UI creation process to be a good experience and I don’t have trouble seeing why being able to simply assign a callback to a button is really nice. It is. And I think we should have this capability in the future. We just need to do it a bit differently, like having the UI Builder generate the event registration C# code for your (instead of just using reflection at runtime).

2 Likes