1
\$\begingroup\$

In the new UI Toolkit that is shipped with Unity 2021+ there's a ListView component that can be added using the UI Builder, which has an option to enable the footer buttons.

in UXML: show-add-remove-footer="true"

However clicking the + will add a null to the list and I'd like to override that behaviour.

Adding a custom function to the button will have it triggered after Unity's default function. Is there a way to remove all registered callbacks on a button?

I tried to look for the source of this ListView class but could not find it.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Could you implement the functionality you want in a listener subscribed to the ItemsAdded event? \$\endgroup\$
    – DMGregory
    Apr 11, 2023 at 13:22
  • \$\begingroup\$ I was thinking about it, but i'd need to give it a shot, it just that i hoped it was possible to prevent the adding of a null item to the data source and immediately add a new item with some default values instead :) \$\endgroup\$
    – TheWolfNL
    Apr 11, 2023 at 16:22

2 Answers 2

2
\$\begingroup\$

As suggested by DMGregory, using itemsAdded resulted in a workable solution.

After creating a new "UI Toolkit > UI Document" in a scene, and adding the below uxml as the source asset, and adding the script as a component gives the desired result.

UXML:

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
    <ui:ListView focusable="true" show-add-remove-footer="true" header-title="Items" show-foldout-header="true" reorderable="true" style="flex-grow: 1; background-color: rgb(210, 210, 210);" />
</ui:UXML>

Test.cs

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UIElements;

public class test : MonoBehaviour
{
    class Item {
        public string name;
    }


    void OnEnable() {
        // Quickly setup a list of Items
        List<Item> source = new List<Item>();
        source.Add(new Item(){ name = "One" });
        source.Add(new Item(){ name = "Two" });
        source.Add(new Item(){ name = "Three" });

        // Find the listview and add functionalities
        var document = GetComponent<UIDocument>();
        var listView = document.rootVisualElement.Q<ListView>();
        listView.makeItem = () => new Label();
        listView.bindItem = (existingElement, index) => (existingElement as Label).text = $"Entry Number {index} - {source[index].name}";

        // set our list as item source
        listView.itemsSource = source;

        // since by default a null element is added to the itemsSource
        // we need to remove that in a creative manner
        listView.itemsAdded += (items) => { 
            var index = items.First();
            Debug.Log($"Added: {items} - {index}"); 
            source.RemoveAt(index);
            source.Add(new Item(){ name = $"Random-{index}" });
        };
        listView.itemsRemoved += (items) => {
            var index = items.First();
            Debug.Log($"Removed: {items} - {index}");
        };
    }
}

Result:

enter image description here

Where the buttons at the bottom work as expected.

UPDATE:

A different way of achieving this would be to check for null items in the bindItem function. Which feels less hacky.

listView.bindItem = (e, index) => {
    if (source[index] == null) {
        source[index] = new Item(){ name = $"Random-{index}" };
    }
    (e as Label).text = $"{source[index].name}";
};

Note

If on delete you need to know which item was deleted, you need to keep a copy of the source list.

listView.itemsSource = new List<Item>(source);

As the itemsRemoved will trigger after it has been removed, the index that is passed would then no longer contain an item.

\$\endgroup\$
1
\$\begingroup\$

You do that by replacing the method stored in the delegate makeItem of the listview with your own method that returns the new VisualElement you want to appear in the list.

The example for ListView in the documentation includes an example that shows how you provide such a method while you create a new ListView via code. Keep in mind that if you do that, you should also provide an implementation of the .bindItem method. The .makeItem method creates the item element, and the .bindItem method is supposed to set up its data. Which will also happen on already created item elements when the player scrolls through a list.

If you want to do it with a ListView you have created as an UXML asset, that should look something like this:

void OnEnable() {
    var document = GetComponent<UIDocument>();
    var listView = document.rootVisualElement.Q<ListView>("myListView");
    listView.makeItem = () => {
        // your code that gets executed on making a new item.
        // Must return a VisualElement, which can be as simple or complex as you want.
        return new Label();
    };
    listView.bindItem = (existingElement, index) => {
        (existingElement as Label).text = $"Entry Number {index}";
    };
}
\$\endgroup\$
5
  • \$\begingroup\$ Hey Philip, thnx for your answer but that's not what i was looking for, I already had that part sorted. If in UI Builder you add a listview and enable show-add-remove-footer it will add the buttons at the bottom that i want to override. \$\endgroup\$
    – TheWolfNL
    Apr 11, 2023 at 16:20
  • 1
    \$\begingroup\$ @TheWolfNL Sorry, but I still don't understand what you mean. What exactly do you want to "override" with the buttons? What happens, when you do what exactly, and what do you want to happen instead? Please try to be precise and specific in your description. \$\endgroup\$
    – Philipp
    Apr 11, 2023 at 16:31
  • \$\begingroup\$ Hey Philipp, I think the big difference was that i was using the itemsSource and the button at the bottom adds a null item to that source. But if in the bind function you'd try to access a property of the item that would fail because the 'item' is null instead. \$\endgroup\$
    – TheWolfNL
    Apr 11, 2023 at 19:13
  • \$\begingroup\$ @TheWolfNL Ah, so you weren't talking about the content of the document but about the source list that is backing it? \$\endgroup\$
    – Philipp
    Apr 11, 2023 at 19:21
  • 1
    \$\begingroup\$ Yes, though the problem becomes more evident when inside the bind function you access the source list's item based on the provided index. which using the default approach would be a null item, so accessing a property on that would fail. Checking for null and then creating a new item is a solution that actually feels less hacky, so I added it to my answer as well, for others :) \$\endgroup\$
    – TheWolfNL
    Apr 11, 2023 at 19:43

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .