Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Bug Bound ListView out of range issue

Discussion in 'UI Toolkit' started by Tomayopan, May 4, 2022.

  1. Tomayopan

    Tomayopan

    Joined:
    Dec 9, 2020
    Posts:
    11
    Unity Version: 2019.4.26f1

    I tried to create a ListView and setting the bindingPath to a serializedObject's array property (In this case, an Array of Components), the code below for this is here

    Code (CSharp):
    1. public class MyComponent : MonoBehaviour
    2. {
    3.     string customName;
    4. }
    5.  
    6. public class Data : ScriptableObject
    7. {
    8.     [SerializeField]
    9.     private MyComponent[] myComponents;
    10. }
    11.  
    12. [CustomEditor(typeof(Data))]
    13. public class DataEditor : Editor
    14. {
    15.     public override VisualElement CreateInspectorGUI() => CreateListView();
    16.  
    17.     private ListView CreateListView()
    18.     {
    19.         ListView stateMachinesView = new ListView();
    20.         stateMachinesView.itemHeight = (int)(EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing);
    21.         stateMachinesView.makeItem = CreateField;
    22.         stateMachinesView.bindItem = BindField;
    23.         stateMachinesView.bindingPath = "myComponents";
    24.         stateMachinesView.style.height = 100;
    25.  
    26.         stateMachinesView.Bind(serializedObject);
    27.  
    28.         VisualElement CreateField() => new ObjectField();
    29.  
    30.         void BindField(VisualElement arg1, int arg2)
    31.         {
    32.             SerializedProperty prop = stateMachines.FindPropertyRelative($"Array.data[{arg2}]");
    33.             ObjectField field = arg1 as ObjectField;
    34.  
    35.             field.objectType = typeof(MyComponent);
    36.             field.bindingPath = prop.propertyPath;
    37.  
    38.             if (prop != null && prop.objectReferenceValue)
    39.             {
    40.                 MyCompponent myComp = prop.objectReferenceValue as MyComponent;
    41.                 field.label = myComp.customName;
    42.             }
    43.             else
    44.             {
    45.                 field.label = $"Element {arg2}";
    46.             }
    47.             field.BindProperty(serializedObject);
    48.         }
    49.  
    50.         return stateMachinesView;
    51.     }
    52. }
    53.  
    While this produces a somewhat proper list view, scrolling down to the bottom throws a nullReferenceException when the objectField's bindingPath is set to the serializedProperty's binding path, this is because the serializedProperty itself is null. which mainly meant that the FindPropertyRelative call is returning null on a specific index.

    After doing some debugging by logging "arg2", i discovered that the Listview would attempt to access [serializedProperty.arraySize + 1] elements from the source, this is an issue, as seen in the image below

    (Notice how the array size is set to 9, but there are a total of 10 messages in the console instead of just 9)

    Is there anything that can be done to fix this issue? When the ListView tries to display the broken element it "Mimicks" another element from the list, (Image above shows the broken element, which is after "Element 8" and the label has it as "Element 2") it trying to simply hide the ListView from the inspector by setting the display to none doesnt work and causes more problems (Elements missing entirely, shown below)

     
  2. griendeau_unity

    griendeau_unity

    Unity Technologies

    Joined:
    Aug 25, 2020
    Posts:
    230
    ListView recycles items as it scrolls, so if there's a problem with the bindItem phase, the previous items will remain. That explains why you would see Element 2 in place of Element 10 after an exception was thrown.

    In your case, I would suggest to fully rely on the binding system. The makeItem is creating an ObjectField, which is what would be created by the binding from what I can see. So you could remove the makeItem and bindItem altogether.

    Otherwise, I think the index out of range is due to the list including the array size, which is then overriden by your makeItem and bindItem. You can remove it by setting showBoundCollectionSize to false, as it looks like you already have a field for that above the list for that purpose.
     
  3. Tomayopan

    Tomayopan

    Joined:
    Dec 9, 2020
    Posts:
    11
    Just managed to resume working on this, and indeed, by simply binding the ListView to the Array SerializedProperty makes it work properly, which definetly works properly.

    however, is there anything that can be done to modify the Fields that ListView creates? in BindField i used to modify the Objectfield's label and tooltips based off the value of MyComponent's customName. How could i keep that functionality without having to set the BindItem property to a custom Action?
     
  4. griendeau_unity

    griendeau_unity

    Unity Technologies

    Joined:
    Aug 25, 2020
    Posts:
    230
    One option is to use custom PropertyDrawers for that. See https://docs.unity3d.com/ScriptReference/PropertyDrawer.html
    The PropertyFields in the ListView will use them if there's one defined.

    Otherwise, yes, for that I'm afraid you'll need custom
    bindItem
    . Make sure to set
    showBoundCollectionSize
    to false to avoid getting the index offset caused by the array size item and you should be good.
     
  5. Tomayopan

    Tomayopan

    Joined:
    Dec 9, 2020
    Posts:
    11
    While "shouBoundCollectionSize" does not exist in 2019.4, i've managed to find a workaround.

    i'll post the workaround here in this github gist, may it help anyone who stumbles upon this issue
    https://gist.github.com/Nebby1999/cd44c8b7f9f0d9693f04e8049ed43496