Search Unity

Resolved Reorderable ListView with nested ScriptableObjects (editor authoring) - only partial reorder

Discussion in 'UI Toolkit' started by Tom-Atom, Jun 19, 2021.

  1. Tom-Atom

    Tom-Atom

    Joined:
    Jun 29, 2014
    Posts:
    153
    Hi,
    my question is about reordering of ListView items when item is dragged. I have following setup:

    1] TestSO - ScriptableObject with list of Test objects:
    Code (CSharp):
    1. [CreateAssetMenu(fileName = "testSO", menuName = "Test SO")]
    2. public class TestSO : ScriptableObject {
    3.  
    4.     public List<Test> _testList;
    5. }
    2] Test - C# class with some test properties and 2 nested classes (Inner and InnerSO). Inner is plain C# class and InnerSO is another nested object.
    Code (CSharp):
    1. [Serializable]
    2. public class Test {
    3.    
    4.     public string StringValue;
    5.     public float FloatValue;
    6.  
    7.     public Inner Inner;
    8.  
    9.     public InnerSO InnerSO;
    10. }
    3] Inner class:
    Code (CSharp):
    1. [Serializable]
    2. public class Inner {
    3.  
    4.     public string strVal;
    5.     public int intVal;
    6. }
    4] InnerSO:
    Code (CSharp):
    1. [CreateAssetMenu(fileName = "innerSO", menuName = "Inner SO")]
    2. public class InnerSO : ScriptableObject {
    3.  
    4.     public string strVal;
    5.     public int intVal;
    6. }
    Now, I want to show _testList from TestSO in ListView and allow user to reoder items with mouse dragging.
    Whole code for my ListView is below and I started from this example.
    - when I want to display properties of Inner class in ListView item, I can drill down with FindProperty() or FindPropertyRelative(). Or I can set binding path like this with dot between property names (see MakeItem()):
    item.Add(new TextField() { bindingPath = "Inner.strVal" });

    - but if nested object is ScriptableObject, then FindPropertyRelative returns null and binding with dot doesn't work too. So, I can create nested BindableElement in MakeItem() like this...:

    // InnerSO - nested Scriptable object
    var nested = new BindableElement();
    nested.name = "nested";
    nested.Add(new TextField() { bindingPath = "strVal" });
    nested.Add(new IntegerField() { bindingPath = "intVal" });
    item.Add(nested);

    ...and bind it in BindItem() like this:

    BindableElement nested = element.Q<BindableElement>("nested");
    if (nested != null) {
    SerializedProperty p = itemProp.FindPropertyRelative("InnerSO");
    SerializedObject newSO = new SerializedObject(p.objectReferenceValue as InnerSO);
    nested.Bind(newSO);
    }


    Up until now everything seems to work. This is image of list with test data:
    List1.png

    Now,if user reorders 3rd and 2nd item with dragging, result is this:
    List2.png
    Items were reodrered, except for nested ScriptableObject, that is bound to nested BindableElement.

    Is this correct behaviour? Or is it bug?
    Or am I missing something? Am I binding nested ScriptableObjects correctly?
    How to bind complex object with nested ScriptableObjects as ListView item and manage correct reordering?

    Here is full listing of ListViewTest class:
    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEditor.UIElements;
    3. using UnityEngine;
    4. using UnityEngine.UIElements;
    5.  
    6. public class ListViewTest : EditorWindow {
    7.  
    8.     private ListView _listView;
    9.     private SerializedObject _serializedObject;
    10.  
    11.     // ---------------------------------------------------------
    12.     [MenuItem("Level Editor/ListView Test")]
    13.     public static void ShowExample() {
    14.         ListViewTest wnd = GetWindow<ListViewTest>();
    15.         wnd.titleContent = new GUIContent("ListView Test");
    16.     }
    17.  
    18.     // ---------------------------------------------------------
    19.     public void CreateGUI() {
    20.  
    21.         VisualElement root = rootVisualElement;
    22.  
    23.         ObjectField of = new ObjectField("Insert TestSO");
    24.         of.objectType = typeof(TestSO);
    25.         of.RegisterValueChangedCallback(evt => {
    26.             TestSO testSO = evt.newValue as TestSO;
    27.             if (testSO) {
    28.                 _serializedObject = new SerializedObject(testSO);
    29.                 root.Bind(_serializedObject);
    30.                 BindListView();
    31.             }
    32.         });
    33.         root.Add(of);
    34.  
    35.         _listView = new ListView();
    36.         _listView.showBoundCollectionSize = false;
    37.         _listView.name = "Test listView";
    38.         rootVisualElement.Add(_listView);
    39.     }
    40.  
    41.     // ---------------------------------------------------------
    42.     void BindListView() {
    43.  
    44.         _listView.Unbind();
    45.  
    46.         _listView.bindingPath = "_testList";
    47.         _listView.itemHeight = 150;
    48.         _listView.style.flexGrow = 1;
    49.  
    50.         // make
    51.         _listView.makeItem = MakeItem;
    52.         // bind
    53.         _listView.bindItem = BindItem;
    54.  
    55.         _listView.Bind(_serializedObject);
    56.     }
    57.  
    58.     // ---------------------------------------------------------
    59.     VisualElement MakeItem() {
    60.        
    61.         var item = new BindableElement(); //BindableElement so the default bind can assign the item's root property
    62.        
    63.         var lbl = new Label("Custom Item");
    64.         item.Add(lbl);
    65.  
    66.         // TestSO properties
    67.         item.Add(new TextField() { bindingPath = "StringValue" });
    68.         item.Add(new FloatField() { bindingPath = "FloatValue" });
    69.        
    70.         // Inner (plain C# Serializable class)
    71.         item.Add(new TextField() { bindingPath = "Inner.strVal" });
    72.         item.Add(new IntegerField() { bindingPath = "Inner.intVal" });
    73.  
    74.         // InnerSO - nested Scriptable object
    75.         var nested = new BindableElement();
    76.         nested.name = "nested";
    77.         nested.Add(new TextField() { bindingPath = "strVal" });
    78.         nested.Add(new IntegerField() { bindingPath = "intVal" });
    79.         item.Add(nested);
    80.  
    81.         return item;
    82.     }
    83.    
    84.     // ---------------------------------------------------------
    85.     void BindItem(VisualElement element, int index) {
    86.  
    87.         //we find the first Bindable
    88.         var field = element as IBindable;
    89.         if (field == null) {
    90.             //we dig through children
    91.             field = element.Query().Where(x => x is IBindable).First() as IBindable;
    92.         }
    93.  
    94.         // Bound ListView.itemsSource is a IList of SerializedProperty
    95.         var itemProp = _listView.itemsSource[index] as SerializedProperty;
    96.  
    97.         field.bindingPath = itemProp.propertyPath;
    98.  
    99.         element.Bind(itemProp.serializedObject);
    100.  
    101.         BindableElement nested = element.Q<BindableElement>("nested");
    102.         if (nested != null) {
    103.             SerializedProperty p = itemProp.FindPropertyRelative("InnerSO");
    104.             SerializedObject newSO = new SerializedObject(p.objectReferenceValue as InnerSO);
    105.             nested.Bind(newSO);
    106.         }
    107.     }
    108. }
    109.  
     
  2. Tom-Atom

    Tom-Atom

    Joined:
    Jun 29, 2014
    Posts:
    153
    Bump?

    Changed title to Bug - I expect to move whole item when reordering (including all nested bindings).
     
  3. uBenoitA

    uBenoitA

    Unity Technologies

    Joined:
    Apr 15, 2020
    Posts:
    220
    Hi Tom,

    It's hard for me to fully understand what is happening in your code without the part that does the reordering, which I wasn't able to find in your sample or in the example link you provided. Also, what do you assign to _listView.itemsSource exactly? The best for me would be to have a Unity project that reproduces your issue and open it to play around with it and better see what's going on.

    In the meantime, one difference I can see is that the regular C# bindings might work just because their entire serializedProperty path is completely local to the TestSO whose list you are displaying, whereas the innerSO bindings use a completely independent path of their own. In the case of the C# bindings, even if there's a delay between the binding and the actual data being moved, the paths are still going to end up aligned properly in the end because the path itself contains all it needs to find the data it wants. For the innerSO though, if you've assigned the new reference after redoing the binding, or if there's any kind of unforeseen delay between the two, it might be that the reference wasn't updated to the right one, and the serializedProperty path won't be helping.

    One thing you could try, to find your bug, is to validate your setup with a button called "Rebind everything", which on click would go through your list, Unbind everything and then Bind your items one by one, from a clean state. If that works, then have a button that rebinds only one of the bugged entries, and see if it works. If it does, then it looks like you have a race condition somewhere, some kind of delay that assigns your data before it's ready. You could try rebinding your innerSO with a 1-frame delay (through visualElement.scheduler.Execute).

    I hope this helps.
     
  4. Tom-Atom

    Tom-Atom

    Joined:
    Jun 29, 2014
    Posts:
    153
    @uBenoitA first, thanks for answer!

    Code above is complete! Reordering is done by ListView by setting its property reorderable to true (see https://docs.unity3d.com/2020.1/Documentation/ScriptReference/UIElements.ListView-reorderable.html).
    In attachment I added all files needed - just put it into new project. There is Scripts folder with all scripts and ScriptableObjects with all created scriptable objects (TestSO is the main).

    Open window in top menu (Level Editor -> ListView Test) it will ask for TestSO. Drag it from assets into slot and then bindings beggins. TestSO is scriptable object that has list of Test objects (_testList) and this list is shown in list view. This works well.

    In turn Test object contains string and int variables and two ref objects - Inner and InnerSO. First is normal C# class, second is ScriptableObject

    Yes, this is my suspicion :) But problem is:
    - ListView.bindItem callback is called only when new data are bound or when new item is added into list. Called first makeItem, then bindItem. There is no call when items are reordered,
    - I can't see any callback when ListView is reordered. So, I do not know where to hook or listen to adjust "sub-bindings" for item
    - if I reorder items in ListView, I can see it works well in inspector - items are fully reordered. But only in ListView it is messed like on screenshot in original post.

    And this is core of my question/bug: If I drag one VisualElement in ListView (and "sub-bindings" are done on child element of this VisualElements), I would expect to move whole VisualElement (including these sub elements with "sub-binding" as they are part of VisualElements tree, that was reordered). I can see in inspector, that in data everything went well - it is reordered completely including reference to InnerSO. But not in ListView.
    Second question is: if this is not (for some reason other than bug) provided by ListView by default, how can I hook or listen to some post-reorder event, so I can rebind "sub-bindings" by myself.

    While my example is stripped down, I think it is real world case. Simply binding list of something little bit more complex than plain list of integers or simple custom object without further sub references.

    If you need additional explanation let me know. Meanwhile, you can test attached file in new project to see it live (follow images in original post).
     

    Attached Files:

  5. uBenoitA

    uBenoitA

    Unity Technologies

    Joined:
    Apr 15, 2020
    Posts:
    220
    Hi, thanks for the files and thorough explanations! I've opened your project and tested it, using a very recent build of Unity (2022.1.0a2). I'm happy to report that I didn't get the bug you're having, i.e. my list is getting reordered correctly in the ListViewTest window, just like it does in the inspector window. Furthermore, I've added a Debug.Log in the BindItem method and I can see that in this version of the editor, BindItem is called on all the visible elements of the list when I reorder any of them.

    So, it's very possible that what you have was a bug that got fixed in a more recent version of Unity. Perhaps you could download Unity 2021.2.0b1 (it's publicly available I believe) and check if your problem still exists?

    One thing that's true, though, it seems indeed you have no way to react specifically to items being moved. We have an internal
    itemIndexChanged
    event that's called when items are reordered, but it's not exposed in the ListView API. I've relayed the discussion to the rest of the team so we can decide what to do about this, if it's considered to be a missing feature.
     
  6. Tom-Atom

    Tom-Atom

    Joined:
    Jun 29, 2014
    Posts:
    153
    @uBenoitA thanks for investigation!

    I can confirm, that in 2021.2.0b1 everything works - ListView is reordered correctly.
     
    martinpa_unity likes this.