Cannot Figure Out Binding Within Listview Details

I’ve created a simple example to illustrate a problem I’m encountering. In the complex case, I have a list of objects, some fields of which have custom UIE-based PropertyDrawers.

In the simple case, I have a trivial MonoBehavior with list of trivial fields:

public class ListTest : MonoBehaviour
{
    [Serializable]
    public class TestType
    {
        public int value;
        public string name;
        public bool isActive;
    }

    public bool UseDefaultEditor;
    public List<TestType> TestList;
}

And a custom editor:

 [CustomEditor(typeof(ListTest))]
    public class ListTestEditor : Editor
    {
        private SerializedProperty m_ListProperty;
        public override VisualElement CreateInspectorGUI()
        {
            var listTestTarget = (ListTest)target;
            if (listTestTarget.UseDefaultEditor)
                return base.CreateInspectorGUI();

            var container = new VisualElement();

            m_ListProperty = serializedObject.FindProperty("TestList");

            container.Add(new PropertyField(serializedObject.FindProperty("UseDefaultEditor")));


            var listView = new ListView(listTestTarget.TestList, (int)EditorGUIUtility.singleLineHeight * 6,
                MakeDetailItem, BindDetailItem);

            listView.selectionType = SelectionType.Single;
            listView.style.flexGrow = 1f;
            listView.style.flexShrink = 0f;
            listView.style.flexBasis = 0f;
            listView.style.minHeight = 300;

            container.Add(listView);
            return container;
        }

        private class DetailItemUserData
        {
            public Label ItemLabel;
            public PropertyField value;
            public PropertyField name;
            public PropertyField isActive;
        }

        private VisualElement MakeDetailItem()
        {
            var container = new BindableElement();
            var userData = new DetailItemUserData
            {
                ItemLabel = new Label(),
                value = CreateStyledPropertyField("value"),
                name = CreateStyledPropertyField("name"),
                isActive = CreateStyledPropertyField("isActive"),
            };
            container.userData = userData;

            // Add fields to the container.
            container.Add(userData.ItemLabel);
            container.Add(userData.value);
            container.Add(userData.name);
            container.Add(userData.isActive);
            container.style.height = 4 * EditorGUIUtility.singleLineHeight;

            return container;
        }

        private void BindDetailItem(VisualElement v, int i)
        {
            var bindable = (BindableElement)v;
            var eventProp = m_ListProperty.GetArrayElementAtIndex(i);
            var userData = (DetailItemUserData)v.userData;
            userData.ItemLabel.text = $"Item[{i}]";
            bindable.BindProperty(eventProp);
        }


        private PropertyField CreateStyledPropertyField(string path)
        {
            var pf = new PropertyField();
            pf.style.height = EditorGUIUtility.singleLineHeight;
            pf.style.flexGrow = 1;
            pf.style.flexShrink = 0;
            pf.bindingPath = path;
            return pf;
        }
    }

The UseDefaultEditor causes the default Inspector to draw the view, and it behaves normally.
When unchecked, the PropertyFields for the list details are blank. Indeed, when I inspect it in the UIE debugger, I see that the PropertyFields are there for each item, but they have no children:


[I tried to put a picture link here but was unsuccessful…I’ve attached a picture of the UIE debugger.

Are PropertyFields not usable in ListView detail items? Am I not doing the binding properly?

4423768--403582--UnityTestListProblem.png

Right now, ListView does not know about the (SerializedObject) bindings system. It has its own data model (the list of Objects) and its own binding process (that binds its data with its recycled elements as you scroll).

That said, you might be able to make this work. The issue you’re running into is that PropertyFields rely on the SerializedObjectBindEvent being sent. They can only know which type of UI fields to create when they bind to a SerializedProperty, and not sooner. Bypassing Bind() and calling BindProperty() directly does not send this event.

Try replacing BindProperty() in your BindDetailItem() with just Bind(). The bindingPath on the PropertyField will find the SerializedProperty again so you just need to pass Bind() your SerializedObject.

If the above works, you’ll want to actively clean up after yourself by manually Unbind()'ing detail items before you Bind() them to a new property.

1 Like

Thank you!

I replaced the BindProperty call with:

            bindable.Unbind();
            bindable.bindingPath = eventProp.propertyPath;
            bindable.Bind(eventProp.serializedObject);

This allowed the PropertyFields to populate correctly, even with custom property drawers for some fields.

3 Likes

Are there any updates on this? Can a ListView be easily bound to a SerializedProperty representing a List<> or Array?

Yes! If you bind an array to a PropertyField, you should get a ListView created for you. And you should be able to bind a ListView to a SerializedProperty now.

Thank you for the reply! Can you please give a short example of binding a ListView to a SerializedProperty? (I’m doing it in the context of a UI Toolkit property drawer)

Since the only non default constructor requires itemSource, I’m not sure whether to use the default constructor and set everything except itemSource or something else.

Thanks!

Edit: About this:

Yes! If you bind an array to a PropertyField, you should get a ListView created for you.

Would this work for an array of a custom ScriptableObject class?

See this:
https://docs.unity3d.com/Manual/UIE-bind-to-list.html

Yep!

I have seen that example, but sadly it relies on the XML style of building a UI. I’m not sure how one would do it through code - what constructor to use when building the ListView? Should itemSource be set? Should the binding be done through .binding, .bindingPath, .Bind(), .BindProperty()?

Edit: I have given up on a custom inspector for now and am going with the default look:

8466281--1125440--upload_2022-9-27_16-52-6.png

Aside from the general friction, I couldn’t be sure (after researching) that I would be able to drag and drop multiple prefabs at the same time into an array using a custom ListView, also while ensuring that only assets of the GameObject type (prefabs) could be dragged (no textures or other types).

You just need to set the bindingPath on the ListView (inside an Inspector). Bind() is called for you by the InspectorWindow. But I recommend you use a PropertyField instead of a naked ListView. You’ll get the ReorderableList like the default Inspector. Unless you don’t want this look.

If you use PropertyFields, this filter will be set for you on the generated ObjectFields. If you create ObjectFields explicitly (and bind them), you should also get the filter, but you might still need to set the ObjectField.objectType to set a filter.

1 Like