Search Unity

Bug ListView not functioning in Editor the same way as EditorWindow

Discussion in 'UI Toolkit' started by VivianArchiact, Jul 24, 2019.

  1. VivianArchiact

    VivianArchiact

    Joined:
    Jun 13, 2018
    Posts:
    1
    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
    Code (CSharp):
    1. using UnityEngine;
    2. #if UNITY_EDITOR
    3. using UnityEditor;
    4. using UnityEngine.UIElements;
    5. using UnityEditor.UIElements;
    6. #endif
    7.  
    8. public class ItemCollection : ScriptableObject
    9. {
    10.     public ItemDescription[] Items = new ItemDescription[0];
    11. }
    12. [System.Serializable]
    13. public class ItemDescription
    14. {
    15.     public string Name;
    16.     public Texture Icon;
    17. }
    18.  
    19. #if UNITY_EDITOR
    20.  
    21. [CustomEditor(typeof(ItemCollection))]
    22. public class ItemDescriptionEditor : EditorWindow
    23. {
    24.     VisualElement root;
    25.  
    26.     VisualTreeAsset visualTree;
    27.     StyleSheet styleSheet;
    28.     ListView wow;
    29.  
    30.     public ItemDescription[] Items = { new ItemDescription(), new ItemDescription() };
    31.  
    32.     [MenuItem("Window/ItemCollection")]
    33.     public static void getWin()
    34.     {
    35.         GetWindow<ItemDescriptionEditor>();
    36.     }
    37.     public void OnEnable()
    38.     {
    39.         visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Scripts/Editor/ItemCollection.uxml");
    40.         styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Examples/Editor/styles.uss");
    41.         root = rootVisualElement;
    42.         root.style.flexDirection = FlexDirection.Row;
    43.  
    44.         VisualElement visualTreeElement = visualTree.CloneTree();
    45.  
    46.         wow = visualTreeElement.Query<ListView>("Items");
    47.         InitListView(wow);
    48.         wow.itemsSource = Items;//((ItemCollection)target).Items;
    49.  
    50.         wow.bindItem = BindItem;
    51.         wow.makeItem = MakeItem;
    52.  
    53.         root.schedule.Execute(Refresh);
    54.         AddListView(wow);
    55.  
    56.     }
    57.     public void Refresh()
    58.     {
    59.         wow.Refresh();
    60.     }
    61.     private void InitListView(ListView listView)
    62.     {
    63.         listView.selectionType = SelectionType.Multiple;
    64.         listView.styleSheets.Add(styleSheet);
    65.         listView.onItemChosen += obj => Debug.Log(obj);
    66.         listView.onSelectionChanged += objects => Debug.Log(objects);
    67.  
    68.         listView.style.flexGrow = 1f;
    69.         listView.style.flexShrink = 0f;
    70.         listView.style.flexBasis = 0f;
    71.     }
    72.     private void AddListView(ListView listView)
    73.     {
    74.         var col = new VisualElement();
    75.         col.style.flexGrow = 1f;
    76.         col.style.flexShrink = 0f;
    77.         col.style.flexBasis = 0f;
    78.  
    79.         col.Add(new Label() { text = listView.viewDataKey });
    80.         col.Add(listView);
    81.  
    82.         root.Add(col);
    83.     }
    84.  
    85.     [MenuItem("Assets/CreateObject/ItemDescription")]
    86.     public static void Create()
    87.     {
    88.         Vivi.Utilities.UtilityScriptableObject.CreateAsset<ItemCollection>();
    89.     }
    90.  
    91.  
    92.     public void BindItem(VisualElement element, int index)
    93.     {
    94.         (element.ElementAt(0) as Label).text = "Wow";
    95.         element.viewDataKey = "box" + index;
    96.     }
    97.     public VisualElement MakeItem()
    98.     {
    99.         var box = new VisualElement();
    100.         box.style.flexDirection = FlexDirection.Row;
    101.         box.style.flexGrow = 1f;
    102.         box.style.flexShrink = 0f;
    103.         box.style.flexBasis = 0f;
    104.         box.Add(new Label("Wow"));
    105.         box.Add(new Button(() => { }) { text = "Button" });
    106.         return box;
    107.     }
    108. }
    109.  
    110. #endif


    Vs the ScriptableObject and it's Editor



    Code (CSharp):
    1. using UnityEngine;
    2. #if UNITY_EDITOR
    3. using UnityEditor;
    4. using UnityEngine.UIElements;
    5. using UnityEditor.UIElements;
    6. #endif
    7.  
    8. public class ItemCollection : ScriptableObject
    9. {
    10.     public ItemDescription[] Items = new ItemDescription[0];
    11. }
    12. [System.Serializable]
    13. public class ItemDescription
    14. {
    15.     public string Name;
    16.     public Texture Icon;
    17. }
    18.  
    19. #if UNITY_EDITOR
    20.  
    21. [CustomEditor(typeof(ItemCollection))]
    22. public class ItemDescriptionEditor : Editor
    23. {
    24.     VisualElement root;
    25.  
    26.     VisualTreeAsset visualTree;
    27.     StyleSheet styleSheet;
    28.     ListView wow;
    29.  
    30.     public ItemDescription[] Items = { new ItemDescription(), new ItemDescription() };
    31.  
    32.     public override VisualElement CreateInspectorGUI()
    33.     {
    34.         visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Scripts/Editor/ItemCollection.uxml");
    35.         styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Examples/Editor/styles.uss");
    36.         root = new VisualElement();
    37.         root.style.flexDirection = FlexDirection.Row;
    38.  
    39.         VisualElement visualTreeElement = visualTree.CloneTree();
    40.  
    41.         wow = visualTreeElement.Query<ListView>("Items");
    42.         InitListView(wow);
    43.         wow.itemsSource = Items;//((ItemCollection)target).Items;
    44.  
    45.         wow.bindItem = BindItem;
    46.         wow.makeItem = MakeItem;
    47.  
    48.         root.schedule.Execute(Refresh);
    49.         AddListView(wow);
    50.  
    51.         return root;
    52.     }
    53.     public void Refresh()
    54.     {
    55.         wow.Refresh();
    56.     }
    57.     private void InitListView(ListView listView)
    58.     {
    59.         listView.selectionType = SelectionType.Multiple;
    60.         listView.styleSheets.Add(styleSheet);
    61.         listView.onItemChosen += obj => Debug.Log(obj);
    62.         listView.onSelectionChanged += objects => Debug.Log(objects);
    63.  
    64.         listView.style.flexGrow = 1f;
    65.         listView.style.flexShrink = 0f;
    66.         listView.style.flexBasis = 0f;
    67.     }
    68.     private void AddListView(ListView listView)
    69.     {
    70.         var col = new VisualElement();
    71.         col.style.flexGrow = 1f;
    72.         col.style.flexShrink = 0f;
    73.         col.style.flexBasis = 0f;
    74.  
    75.         col.Add(new Label() { text = listView.viewDataKey });
    76.         col.Add(listView);
    77.  
    78.         root.Add(col);
    79.     }
    80.  
    81.     [MenuItem("Assets/CreateObject/ItemDescription")]
    82.     public static void Create()
    83.     {
    84.         Vivi.Utilities.UtilityScriptableObject.CreateAsset<ItemCollection>();
    85.     }
    86.  
    87.  
    88.     public void BindItem(VisualElement element, int index)
    89.     {
    90.         (element.ElementAt(0) as Label).text = "Wow";
    91.         element.viewDataKey = "box" + index;
    92.     }
    93.     public VisualElement MakeItem()
    94.     {
    95.         var box = new VisualElement();
    96.         box.style.flexDirection = FlexDirection.Row;
    97.         box.style.flexGrow = 1f;
    98.         box.style.flexShrink = 0f;
    99.         box.style.flexBasis = 0f;
    100.         box.Add(new Label("Wow"));
    101.         box.Add(new Button(() => { }) { text = "Button" });
    102.         return box;
    103.     }
    104. }
    105.  
    106. #endif
     
  2. CosmoMan

    CosmoMan

    Joined:
    Jul 12, 2019
    Posts:
    2
    I'd like to know the answer to this as well.
     
  3. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    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.
     
  4. CosmoMan

    CosmoMan

    Joined:
    Jul 12, 2019
    Posts:
    2
    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.
     
  5. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    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:
    Code (CSharp):
    1. root.schedule.Execute(Refresh);
    If you want to keep calling it, you have to use:
    Code (CSharp):
    1. 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.
     
  6. Sibz9000

    Sibz9000

    Joined:
    Feb 24, 2018
    Posts:
    149
    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?)
     
  7. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    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-Technologi...tor/Mono/UIElements/Controls/PropertyField.cs
     
    Sibz9000 likes this.