Beginner in Script and UI Toolkit - ListView and ScriptableObject

Hi,

I would like to have a view list of items in the Editor.
These items are all in one file scriptableObject ItemDatabase in order to save them.
In the editor, I want to edit, move, remove and add items.

I taked the sample UIElements ListView with serializedProperty of an array and I am trying to replace the existing struct CustomStruct by my scriptableObject.

I don’t know how to use the binding path with scriptableObject.

Maybe It’s a bad idea and I should use more SerializedObject, like say here :

functions changed for me and important row :

bindingPath - row 86
bindingPath - row 143
FindOptions() - row 182

[System.Serializable]
public class Item : ScriptableObject
{
    public int Id = default;
    public string name = default;
    public float size = default;
}
[System.Serializable]
public class ItemDatabase : ScriptableObject
{
   public List<Item> items = new List<Item>();
}
public class ListViewWithItems : EditorWindow
{

    [SerializeField]

    private List<Item> m_CustomStructs;

    private ListView m_ListView;
    private SerializedObject serializedObject;

    private SerializedProperty m_ArraySizeProperty;
    private SerializedProperty m_ArrayProperty;

    private int m_ListViewInsertIndex = -1; // To make sure we insert the ListView at the right place in our visualTree
    VisualElement root;
    ItemDatabase ItemDatabase;
    string path = "Assets/Editor/";
    string objectName = "yoMan.asset";

    public void CreateGUI()
    {
        FindOptions();

        serializedObject = new SerializedObject(this);
        root = rootVisualElement;

        var rowContainer = new VisualElement();
        rowContainer.style.flexDirection = FlexDirection.Row;
        rowContainer.style.justifyContent = Justify.FlexStart;

        root.Add(rowContainer);

        CreateListView();

        root.Bind(serializedObject);
    }

    void AddButton(VisualElement container, string label, Action onClick)
    {
        container.Add(new Button(onClick) { text = label });
    }


    void CreateListView()
    {
        if (m_ListView != null)
        {
            //We clean ourselves up
            m_ListView.Unbind();
            m_ListView?.RemoveFromHierarchy();
        }

        m_ListView = new ListView();
        m_ListView.showBoundCollectionSize = false;

        m_ListView.name = "OneList";
        m_ListView.bindingPath = nameof(m_CustomStructs);
        m_ListView.itemHeight = 60;


        m_ArrayProperty = serializedObject.FindProperty(m_ListView.bindingPath);
        m_ArraySizeProperty = serializedObject.FindProperty(m_ListView.bindingPath + ".Array.size");

        m_ListView.style.flexGrow = 1;


        m_ListView.name = m_ListView.name + "-custom-item";
        m_ListView.makeItem = () => CreateCustomStructListItem();
        m_ListView.name += "+bind";
        m_ListView.bindItem = ListViewBindItem;
    

        if (m_ListViewInsertIndex < 0)
        {
            m_ListViewInsertIndex = rootVisualElement.childCount;
            rootVisualElement.Add(m_ListView);

        }
        rootVisualElement.Insert(m_ListViewInsertIndex, m_ListView);
        m_ListView.Bind(serializedObject);
    }

    private void AddRemoveItemButton(VisualElement row)
    {
        var button = new Button() { text = "-" };
        button.RegisterCallback<ClickEvent>((evt) =>
        {
            var clickedElement = evt.target as VisualElement;

            if (clickedElement != null && clickedElement.userData is int index)
            {
                m_ArrayProperty.DeleteArrayElementAtIndex(index);
                serializedObject.ApplyModifiedProperties();
            }
        });

        button.tooltip = "Remove this item from the list";
        row.Add(button);
    }

    VisualElement CreateCustomStructListItem()
    {
        var keyFrameContainer = new BindableElement(); //BindableElement so the default bind can assign the item's root property
        var lbl = new Label("Custom Item UI");
        lbl.AddToClassList("custom-label");
        var row = new VisualElement();
        row.style.flexDirection = FlexDirection.Row;
        row.style.justifyContent = Justify.SpaceBetween;
        row.Add(lbl);

        AddRemoveItemButton(row);

        keyFrameContainer.Add(row);
        keyFrameContainer.Add(new TextField( {  } ));
        //keyFrameContainer.Add(new TextField() { bindingPath = nameof(CustomStruct.StringValue });
        //keyFrameContainer.Add(new FloatField() { bindingPath = nameof(CustomStruct.FloatValue) });
        return keyFrameContainer;
    }



    void ListViewBindItem(VisualElement element, int index)
    {
        var label = element.Q<Label>(className: "custom-label");
        if (label != null)
        {
            label.text = "Custom Item UI (Custom Bound)";
        }

        var button = element.Q<Button>();
        if (button != null)
        {
            button.userData = index;
        }

        //we find the first Bindable
        var field = element as IBindable;
        if (field == null)
        {
            //we dig through children
            field = element.Query().Where(x => x is IBindable).First() as IBindable;
        }

        // Bound ListView.itemsSource is a IList of SerializedProperty
        var itemProp = m_ListView.itemsSource[index] as SerializedProperty;

        field.bindingPath = itemProp.propertyPath;

        element.Bind(itemProp.serializedObject);
    }


    private void FindOptions()
    {

        ItemDatabase = AssetDatabase.LoadAssetAtPath<ItemDatabase>(path + objectName);

        if (ItemDatabase == null)
        {
            ItemDatabase = CreateInstance<ItemDatabase>();
            m_CustomStructs = ItemDatabase.items;
            AssetDatabase.CreateAsset(ItemDatabase, path + objectName);
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();

            Item item = CreateInstance<Item>();
            item.name = "toto";
            m_CustomStructs.Add(item);
            item = CreateInstance<Item>();
            item.name = "titi";
            m_CustomStructs.Add(item);

        }

    }




    [MenuItem("testo/ListViewWithItems")]
    public static void ShowExample()
    {
        ListViewWithItems wnd = GetWindow<ListViewWithItems>();
        wnd.titleContent = new GUIContent("ListViewWithItems");
    }
}

Hi,
did you get anywhere with this…? I am trying to link scriptable ojects, using events and displayed in a runtime UITK focussed UI.

I have found some good tutorials, but none which combine, scriptable objects, events and UI toolkit in one.

Any constructive comment most appreciated.

Yo same here.

I eventually gave up on using a ScriptableObject and restructured my data into a struct (based on the GameSwitches ListView example in the docs)…and that just magically worked.

Hoping to eventually hear more info on how I can use a ScriptableObject as the underlying data for a ListView inside an custom Editor…I couldn’t get the darn thing to serialize my inputs in the editor no matter what I did.

If you want to a collection of scriptable objects, but draw the inspector for said object, you can just create and bind it to an InspectorElement, rather than try to manually try to draw the fields. Potentially combine that with an Object field as well so as to be able to assign them/see what scriptable object is assigned.