Adding 'clicked' callback to Button that is part of a bound PropertyField in a ListView

Hey all! I’m breaking my head on how to do something that I thought would be pretty easy! (insert sweating emoji)

I have a ListView that is meant to display a property that is a List<> and the inner element is a struct (not super important). The list binds fine, and the comments display.

This is what they look like:

9843642--1416462--Screenshot 2024-05-19 alle 16.25.05.png

Now, I want to assign a method to that little Trash button. How do I do that?

The challenge seems to lie in the fact that elements of the list are not available at the moment the Inspector is constructed, so I tried to use GeometryChangedEvent on the list, and it’s just thrown a bunch of times… so I ended up going for the list’s bindItem method.

This works, I hook into that:

_commentsList.bindItem += OnBindCommentItem;
_commentsList.unbindItem += OnUnbindCommentItem;
private void OnBindCommentItem(VisualElement element, int i)
{
    PropertyField propField = (PropertyField)element;
    propField.BindProperty(_commentsProperty.GetArrayElementAtIndex(i));
    propField.userData = i;
    propField.RegisterCallback<GeometryChangedEvent>(OnCommentReady);
}
private void OnUnbindCommentItem(VisualElement element, int i)
{
    PropertyField propField = (PropertyField)element;
    propField.Unbind();
    propField.UnregisterCallback<GeometryChangedEvent>(OnCommentReady);
}
private void OnCommentReady(GeometryChangedEvent evt)
{
    ((PropertyField)evt.target).UnregisterCallback<GeometryChangedEvent>(OnCommentReady);
   
    VisualElement comment = (VisualElement)evt.target;
    int index = (int)comment.userData;
    comment.Q<Button>("DeleteCommentBtn").clicked += () => RemoveComment(index);
    Debug.Log($"Comment {index} ready");
}

This all works fine the first time. It also works for newly-added elements, because OnBindCommentItem is called, so once the GeometryChangedEvent is fired, the button receives its callback.

However, if click one of the trash buttons, and one element is removed from the list, the listview rebuilds (as I would expect) and elements are rebinded, but it seems like GeometryChangedEvents are not fired, because perhaps the elements don’t change shape/size/position?

How am I supposed to know when the element is ready then, to be able to reliably add the button callback?

Thanks for any help.

I assume you have your own visual element, or perhaps a property drawer, for this struct? Why not hook into the makeItem delegate and create the visual elements yourself? Then you can ensure they’re there when it comes to binding, so you can just Q<T> for the button.

Perhaps your own custom visual element is a good choice here.

1 Like

Yes, I do, but it’s done using a UXML. So it’s a PropertyField that uses the UXML as a template (in a PropertyDrawer’s
CreatePropertyGUI), automatically binds itself to the property in the list.
So in a way it’s a bit of a loss to have to hook into makeItem and have to build it all by code, when I have a template ready.

Or, one way is to hook into makeItem and create a PropertyField for that element, and with that, add a listener on top. That PropertyField would use the UXML anyway - since it’s defined as the PropertyDrawer for that type of property.

But I’m still trying to figure out the flow of events. It seems that when I select an object that requires that Inspector, then I immediately select another of the same type, Unity recycles the ListView and its elements rebinding them to the new ones, and the order of events (for me) falls apart. I’ll make some more tests and report what I see.

Thanks for the answer, in the meantime, @spiney199 !

Nope, doesn’t work.

Let’s say I have an array of 2 of these elements. Initially it all works fine. But if I add one element to this list, I can see that bindItem, unbindItem get called a few times. But makeItem (the function in which I assign the clicked listener) is only called for the new element, item with id 2. As such, now this item is setup correctly… but all the others are not! Is it because they are recycled?

How do I endsure these recycled elements are setup correctly?

My thought was to introduce your own visual element where the button is already present:

[UxmlElement]
public partial class BinnableElement : VisualElement
{
    public MyStructElement()
    {
        name = NAME;
        _propertyField = new PropertyField()
        {
            name = NAME + "__propertyField"
        };
        this.Add(_propertyField);
       
        _trashButton = new Button()
        {
            name = NAME + "__trash-button"
        };
        this.Add(_trashButton);
    }
   
    private readonly PropertyField _propertyField;
   
    private readonly Button _trashButton;
   
    public const string NAME = "binnable-element";
   
    public PropertyField PropertyField => _propertyField;
   
    public Button TrashButton => _trashButton;
}

Thus when you instance it, the button is immediately available, and you can subscribe to it right away.

Thanks for the idea, and the code, but… with all the tooling that there’s is (UXML and UI Builder), and Unity adding in Unity 6 the ability to link a .UXML file to a PropertyDrawer’s default references
… all of this and then I have to throw it away and manually instantiate elements in C#? I can’t believe there’s no other way.

Plus as I was saying, in whatever way I build these repeated elements, there’s something that UI Toolkit does to recycle them, so it doesn’t reliably call makeItem when I jump between multiple objects of this type. So even if I were to define a custom component, I would be back at square 1: how do I find the Button contained in it and assign a listener to it? I can’t embed the callback assignment in the component itself, it has no context of what’s around it, it has to be done by the Inspector instantiating it.

My response is such because my personal workflow is to do nearly all structuring via C# code. That’s just my preference. If there is a way via UXML, etc, I wouldn’t know because I have never really explored that side of UI Toolkit. I just find it easier to work nearly entirely in code. And honestly, writing UXML manually sounds like torture to me…

You get the custom element in your bind/unbind/destroy callbacks. Just cast it to your custom visual element type and Bob’s your uncle.

1 Like

Sorry, I didn’t mean that doing it in C# is a bad avenue, just that given that there is also a visual way, it’s surprising how hard it is when you choose that.

And by the way, I didn’t mean write UXML by hand, no :slight_smile: I meant that I use UI Builder, as I’m spending a lot of time on the visuals and I want the component/Inspector to have a native look.