ListView strange index Problem when scrolling

I have a custom Editor for a scriptableObject. This scriptableObject has List i wanted to display and manipulate within a ListView. I tried to model this after the tank example.

When the list gets big enough that the scrollbar gets visible and i scroll an entrie which is moved out of sight gets inserted at another place in my list.

At this point my list has only empty entries except the last shown with test:
5356917--541440--upload_2020-1-10_15-7-57.png
after scrolling up it this entrie is somehow also in a random top entrie:
5356917--541443--upload_2020-1-10_15-9-33.png
and the bottom one is still present.

Here my code:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;

[CustomEditor(typeof(TemplatePathsSO))]
public class TemplatePathsSOEditor : Editor
{
    private TemplatePathsSO templatePathsSO;
    private VisualElement root;
    private VisualTreeAsset visualTree;


    private ListView templateList;

    public void OnEnable(){
        templatePathsSO = (TemplatePathsSO)target;

        root = new VisualElement();
        visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/UI/TemplatePathsSOUI.uxml");
    }

    public override VisualElement CreateInspectorGUI()
    {
        root.Clear();
        Button plusButton = new Button();
        plusButton.name = "plusButton";
        plusButton.text = "+";
        if (plusButton != null)
        {
            plusButton.clickable.clicked += () =>
            {
                templatePathsSO.templatePaths.Insert(0, new TemplatePath());
                templateList.Refresh();
            };
        }
        root.Add(plusButton);

        visualTree.CloneTree(root);

        templateList = root.Q<ListView>("templateList");
        if (templateList != null)
        {
            templateList.selectionType = SelectionType.None;

            if (templateList.makeItem == null)
                templateList.makeItem = MakeItem;
            if (templateList.bindItem == null)
                templateList.bindItem = BindItem;

            templateList.itemsSource = templatePathsSO.templatePaths;
            templateList.Refresh();
        }

        return root;
    }

      private VisualElement MakeItem()
    {
        var element = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/UI/TemplatePathUI.uxml").CloneTree();

        element.style.flexDirection = FlexDirection.Row;
        element.style.height = 20;

        Button plusButton = new Button();
        plusButton.name = "plusButton";
        plusButton.text = "+";
        element.Add(plusButton);

        Button minusButton = new Button();
        minusButton.name = "minusButton";
        minusButton.text = "-";
        element.Add(minusButton);

        return element;
    }

    private void BindItem(VisualElement element, int index)
    {
        TemplatePath templatePath = templatePathsSO.templatePaths[index];

        TextField templateName;
        templateName = element.Q<TextField>("templateName");
        templateName.value = templatePath.templateName;
        templateName.RegisterValueChangedCallback( ctx => {
            templatePath.templateName = (string) ctx.newValue;
        });

        TextField pathString;
        pathString = element.Q<TextField>("pathString");
        pathString.value = templatePath.pathString;
        pathString.RegisterValueChangedCallback( ctx => {
            templatePath.pathString = (string) ctx.newValue;
        });

        var selectButton = element.Q<Button>("selectPath");
        if (selectButton != null)
        {
            selectButton.clickable.clicked += () =>
            {
                string path = EditorUtility.OpenFilePanel("Choose Template", "Assets", "txt");
                if (path.Length > 0)
                {
                    templatePath.pathString = path;
                    pathString.value = templatePath.pathString;
                }
            };
        }

        var plusButton = element.Q<Button>("plusButton");
        if (plusButton != null)
        {
            plusButton.clickable.clicked += () =>
            {
                templatePathsSO.templatePaths.Insert(index + 1, new TemplatePath());
                templateList.Refresh();
            };
        }

        var minusButton = element.Q<Button>("minusButton");
        if (minusButton != null)
        {
            minusButton.clickable.clicked += () =>
            {
                templatePathsSO.templatePaths.RemoveAt(index);
                templateList.Refresh();
            };
        }
    }
}

Hello,

Which Unity version are you using ?

hey antoine,

im using 2019.3.0f3

another thing i encounter is that i have to use EditorUtility.SetDirty(templatePathsSO); after each change i do over a binding. Without it looses the values when i close unity. But this doesn’t fix my scroll issue.

I have the same problem in Unity 2019.3.0f3 when using a scriptable object for my ListView. In the bind item method, it seems that the wrong index is used.

Any updates here? I encountered the same problem. I use Unity 2021.1.28f1 and the 1.0.0-preview.18 UI Toolkit and 1.0.0-preview.18 UI Builder packages.

The problem in the example posted by the OP I think is that callbacks are registered multiple times on the same field. In a ListView, elements are recycled as users scroll. The bindItem is called again as we scroll with a new index. So it is the same field that is getting the RegisterValueChangedCallback. Callbacks should instead be register in the makeItem phase, then in the bindItem you could store relevant information in userData. Or else, it is also possible to unregister the callbacks in the unbindItem phase.

When binding to a ScriptableObject with BindProperty() or Bind(), there is usually no need to override the bindItem. The binding system will take care of generating the required controls. But if you do, make sure you take into account that the list might contain an additional item at index 0 for the array size. To remove that item from the data list, you can set showBoundCollectionSize to false

Thanks a lot, this solved my issue.