ListView not functioning in Editor the same way as EditorWindow

I am experiencing confusion over the differences between Editor and EditorWindow when working with UIElements. I was working through Example_09 to try and breakdown how ListViews work, and while I managed to get a logically similar script functioning for an EditorWindow, when I modified the script slightly to be a scriptable object everything worked but the list view. The only differences are moving the initialization logic from OnEnabled (for the EditorWindow) to CreateInspectorGUI (for the scriptable Object) and in the ScriptableObject/Editor version a new visual element is created for the root, where in the EditorWindow root is assigned to the built in “rootVisualElement” I’m not fully certain if this is a bug or if there is an oversight in my understanding of how this all works, but some clarification would be appreciated.

This is the EditorWindow

using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
#endif

public class ItemCollection : ScriptableObject
{
    public ItemDescription[] Items = new ItemDescription[0];
}
[System.Serializable]
public class ItemDescription
{
    public string Name;
    public Texture Icon;
}

#if UNITY_EDITOR

[CustomEditor(typeof(ItemCollection))]
public class ItemDescriptionEditor : EditorWindow
{
    VisualElement root;

    VisualTreeAsset visualTree;
    StyleSheet styleSheet;
    ListView wow;

    public ItemDescription[] Items = { new ItemDescription(), new ItemDescription() };

    [MenuItem("Window/ItemCollection")]
    public static void getWin()
    {
        GetWindow<ItemDescriptionEditor>();
    }
    public void OnEnable()
    {
        visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Scripts/Editor/ItemCollection.uxml");
        styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Examples/Editor/styles.uss");
        root = rootVisualElement;
        root.style.flexDirection = FlexDirection.Row;

        VisualElement visualTreeElement = visualTree.CloneTree();

        wow = visualTreeElement.Query<ListView>("Items");
        InitListView(wow);
        wow.itemsSource = Items;//((ItemCollection)target).Items;

        wow.bindItem = BindItem;
        wow.makeItem = MakeItem;

        root.schedule.Execute(Refresh);
        AddListView(wow);

    }
    public void Refresh()
    {
        wow.Refresh();
    }
    private void InitListView(ListView listView)
    {
        listView.selectionType = SelectionType.Multiple;
        listView.styleSheets.Add(styleSheet);
        listView.onItemChosen += obj => Debug.Log(obj);
        listView.onSelectionChanged += objects => Debug.Log(objects);

        listView.style.flexGrow = 1f;
        listView.style.flexShrink = 0f;
        listView.style.flexBasis = 0f;
    }
    private void AddListView(ListView listView)
    {
        var col = new VisualElement();
        col.style.flexGrow = 1f;
        col.style.flexShrink = 0f;
        col.style.flexBasis = 0f;

        col.Add(new Label() { text = listView.viewDataKey });
        col.Add(listView);

        root.Add(col);
    }

    [MenuItem("Assets/CreateObject/ItemDescription")]
    public static void Create()
    {
        Vivi.Utilities.UtilityScriptableObject.CreateAsset<ItemCollection>();
    }


    public void BindItem(VisualElement element, int index)
    {
        (element.ElementAt(0) as Label).text = "Wow";
        element.viewDataKey = "box" + index;
    }
    public VisualElement MakeItem()
    {
        var box = new VisualElement();
        box.style.flexDirection = FlexDirection.Row;
        box.style.flexGrow = 1f;
        box.style.flexShrink = 0f;
        box.style.flexBasis = 0f;
        box.Add(new Label("Wow"));
        box.Add(new Button(() => { }) { text = "Button" });
        return box;
    }
}

#endif

Vs the ScriptableObject and it’s Editor

using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
#endif

public class ItemCollection : ScriptableObject
{
    public ItemDescription[] Items = new ItemDescription[0];
}
[System.Serializable]
public class ItemDescription
{
    public string Name;
    public Texture Icon;
}

#if UNITY_EDITOR

[CustomEditor(typeof(ItemCollection))]
public class ItemDescriptionEditor : Editor
{
    VisualElement root;

    VisualTreeAsset visualTree;
    StyleSheet styleSheet;
    ListView wow;

    public ItemDescription[] Items = { new ItemDescription(), new ItemDescription() };

    public override VisualElement CreateInspectorGUI()
    {
        visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Scripts/Editor/ItemCollection.uxml");
        styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Examples/Editor/styles.uss");
        root = new VisualElement();
        root.style.flexDirection = FlexDirection.Row;

        VisualElement visualTreeElement = visualTree.CloneTree();

        wow = visualTreeElement.Query<ListView>("Items");
        InitListView(wow);
        wow.itemsSource = Items;//((ItemCollection)target).Items;

        wow.bindItem = BindItem;
        wow.makeItem = MakeItem;

        root.schedule.Execute(Refresh);
        AddListView(wow);

        return root;
    }
    public void Refresh()
    {
        wow.Refresh();
    }
    private void InitListView(ListView listView)
    {
        listView.selectionType = SelectionType.Multiple;
        listView.styleSheets.Add(styleSheet);
        listView.onItemChosen += obj => Debug.Log(obj);
        listView.onSelectionChanged += objects => Debug.Log(objects);

        listView.style.flexGrow = 1f;
        listView.style.flexShrink = 0f;
        listView.style.flexBasis = 0f;
    }
    private void AddListView(ListView listView)
    {
        var col = new VisualElement();
        col.style.flexGrow = 1f;
        col.style.flexShrink = 0f;
        col.style.flexBasis = 0f;

        col.Add(new Label() { text = listView.viewDataKey });
        col.Add(listView);

        root.Add(col);
    }

    [MenuItem("Assets/CreateObject/ItemDescription")]
    public static void Create()
    {
        Vivi.Utilities.UtilityScriptableObject.CreateAsset<ItemCollection>();
    }


    public void BindItem(VisualElement element, int index)
    {
        (element.ElementAt(0) as Label).text = "Wow";
        element.viewDataKey = "box" + index;
    }
    public VisualElement MakeItem()
    {
        var box = new VisualElement();
        box.style.flexDirection = FlexDirection.Row;
        box.style.flexGrow = 1f;
        box.style.flexShrink = 0f;
        box.style.flexBasis = 0f;
        box.Add(new Label("Wow"));
        box.Add(new Button(() => { }) { text = "Button" });
        return box;
    }
}

#endif

I’d like to know the answer to this as well.

I think you just forgot to add your visualTree element to your root element. So root is just an empty element and that’s what you’re returning from CreateInspectorGUI().

That said, I would just return visualTree itself. No need for another root element.

I don’t think that’s the problem. In my case I did return the root element on the CreateInspectorGUI() method.

I checked the UIElements debugger. The list view element is there it just doesn’t contain any of the elements I added to it during creation.

Ok, I see. I missed the root.Add() in AddListView(). So, as you correctly noticed, you need to call wow.Refresh() every time the data (Items) changes. From the code above, it seems you’re only calling it once, using:

root.schedule.Execute(Refresh);

If you want to keep calling it, you have to use:

root.schedule.Execute(Refresh).Every(500); // ms

But this is really expensive and you will notice your ListView focus constantly changing while you interact with it.

I highly recommend using the PropertyField bound to your Items array field. It will create a list control for you that properly updates when there are changes. For the UI of individual items, you can use CustomPropertyDrawers to define a custom UI for ItemDescription.

ListView is really just useful for when you have hundreds items in your list and you want to optimize the UI. If you use ListView in the Inspector window, you’ll also end up with double scroll bars (one for the ListView and one for the entire Inspector), which is not ideal.

I would love to use property field bound to my array, however I need to get rid of the size field. Is that possible? (Also can I remove it from being inside a foldout?)

Unfortunately, it’s not that simple. UIElements is quite flexible, so you can remove another element’s IntegerField, or even move all children of one element into another. But if you did this with the generated array UI inside the PropertyField (after it’s bound), you will break it’s refresh process. It has logic to regenerate itself if the number of items in the array changes.

One option is to replicate the array bind implementation from the PropertyField. You can find its source code here:
https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/UIElements/Controls/PropertyField.cs

1 Like