Search Unity

Cannot Figure Out Binding Within Listview Details

Discussion in 'UI Toolkit' started by johnseghersmsft, Apr 13, 2019.

  1. johnseghersmsft

    johnseghersmsft

    Joined:
    May 18, 2015
    Posts:
    28
    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:
    Code (CSharp):
    1. public class ListTest : MonoBehaviour
    2. {
    3.     [Serializable]
    4.     public class TestType
    5.     {
    6.         public int value;
    7.         public string name;
    8.         public bool isActive;
    9.     }
    10.  
    11.     public bool UseDefaultEditor;
    12.     public List<TestType> TestList;
    13. }
    And a custom editor:
    Code (CSharp):
    1.  [CustomEditor(typeof(ListTest))]
    2.     public class ListTestEditor : Editor
    3.     {
    4.         private SerializedProperty m_ListProperty;
    5.         public override VisualElement CreateInspectorGUI()
    6.         {
    7.             var listTestTarget = (ListTest)target;
    8.             if (listTestTarget.UseDefaultEditor)
    9.                 return base.CreateInspectorGUI();
    10.  
    11.             var container = new VisualElement();
    12.  
    13.             m_ListProperty = serializedObject.FindProperty("TestList");
    14.  
    15.             container.Add(new PropertyField(serializedObject.FindProperty("UseDefaultEditor")));
    16.  
    17.  
    18.             var listView = new ListView(listTestTarget.TestList, (int)EditorGUIUtility.singleLineHeight * 6,
    19.                 MakeDetailItem, BindDetailItem);
    20.  
    21.             listView.selectionType = SelectionType.Single;
    22.             listView.style.flexGrow = 1f;
    23.             listView.style.flexShrink = 0f;
    24.             listView.style.flexBasis = 0f;
    25.             listView.style.minHeight = 300;
    26.  
    27.             container.Add(listView);
    28.             return container;
    29.         }
    30.  
    31.         private class DetailItemUserData
    32.         {
    33.             public Label ItemLabel;
    34.             public PropertyField value;
    35.             public PropertyField name;
    36.             public PropertyField isActive;
    37.         }
    38.  
    39.         private VisualElement MakeDetailItem()
    40.         {
    41.             var container = new BindableElement();
    42.             var userData = new DetailItemUserData
    43.             {
    44.                 ItemLabel = new Label(),
    45.                 value = CreateStyledPropertyField("value"),
    46.                 name = CreateStyledPropertyField("name"),
    47.                 isActive = CreateStyledPropertyField("isActive"),
    48.             };
    49.             container.userData = userData;
    50.  
    51.             // Add fields to the container.
    52.             container.Add(userData.ItemLabel);
    53.             container.Add(userData.value);
    54.             container.Add(userData.name);
    55.             container.Add(userData.isActive);
    56.             container.style.height = 4 * EditorGUIUtility.singleLineHeight;
    57.  
    58.             return container;
    59.         }
    60.  
    61.         private void BindDetailItem(VisualElement v, int i)
    62.         {
    63.             var bindable = (BindableElement)v;
    64.             var eventProp = m_ListProperty.GetArrayElementAtIndex(i);
    65.             var userData = (DetailItemUserData)v.userData;
    66.             userData.ItemLabel.text = $"Item[{i}]";
    67.             bindable.BindProperty(eventProp);
    68.         }
    69.  
    70.  
    71.         private PropertyField CreateStyledPropertyField(string path)
    72.         {
    73.             var pf = new PropertyField();
    74.             pf.style.height = EditorGUIUtility.singleLineHeight;
    75.             pf.style.flexGrow = 1;
    76.             pf.style.flexShrink = 0;
    77.             pf.bindingPath = path;
    78.             return pf;
    79.         }
    80.     }
    81.  
    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?
     

    Attached Files:

  2. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    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.
     
    johnseghersmsft likes this.
  3. johnseghersmsft

    johnseghersmsft

    Joined:
    May 18, 2015
    Posts:
    28
    Thank you!

    I replaced the BindProperty call with:
    Code (CSharp):
    1.             bindable.Unbind();
    2.             bindable.bindingPath = eventProp.propertyPath;
    3.             bindable.Bind(eventProp.serializedObject);
    4.  
    This allowed the PropertyFields to populate correctly, even with custom property drawers for some fields.
     
    Crouching-Tuna and enrickla like this.
  4. marcospgp

    marcospgp

    Joined:
    Jun 11, 2018
    Posts:
    194
    Are there any updates on this? Can a ListView be easily bound to a SerializedProperty representing a List<> or Array?
     
  5. uDamian

    uDamian

    Unity Technologies

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

    marcospgp

    Joined:
    Jun 11, 2018
    Posts:
    194
    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?
     
    Last edited: Sep 23, 2022
  7. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
  8. marcospgp

    marcospgp

    Joined:
    Jun 11, 2018
    Posts:
    194
    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:

    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).
     
    Last edited: Sep 27, 2022
  9. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    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.
     
    marcospgp likes this.