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:

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.
ItemsAddedevent? \$\endgroup\$