Search Unity

UIElements ListView with serializedProperty of an array

Discussion in 'UI Toolkit' started by Sangemdoko, Jul 31, 2019.

  1. Sangemdoko

    Sangemdoko

    Joined:
    Dec 15, 2013
    Posts:
    222
    Hi,

    I am having problems with displaying a ListView from a serializedProperty of an array of a custom class.

    The reason I want this is to make a customPropertyDrawer for my property.
    The main limitation is that ListView requires an IList "itemSource".

    I tried getting the array value using Reflection (FieldInfo.GetValue) but the array I get is always of size 0, even though the property.arraySize is not.

    I assumed that what is happening is that since I am using a customClass and the elements were null and that the get value function would remove any null elements in the array.

    But I'm not sure that's the case because I tried FieldInfo.SetValue and that did not increase the serializedProperty array size.

    Has anyone managed to get something similar to work? How did you achieve it?

    I'll continue to try and get my propertyDrawer to work and I'll post my solution if I can get it to work
     
    marcospgp likes this.
  2. imaewyn

    imaewyn

    Joined:
    Apr 23, 2016
    Posts:
    211
    Code (CSharp):
    1. [CustomEditor(typeof(List_SO))]
    2. public class SO_UI : Editor
    3. {    
    4.    SerializedProperty _listProperty;
    5.  
    6.     public override VisualElement CreateInspectorGUI()
    7.     {
    8.         serializedObject.Update();
    9.         _listProperty = serializedObject.FindProperty("firstLists");
    10.  
    11.         var root = new VisualElement();
    12.         root.Add(new Button(() => Test()));
    13.         for (int i = 0; i < _listProperty.arraySize; i++)
    14.         {
    15.             SerializedProperty property = _listProperty.GetArrayElementAtIndex(i);
    16.             root.Add(new PropertyField(property));
    17.         }
    18.         serializedObject.ApplyModifiedProperties();
    19.         return root;
    20.     }
    21.     public void Test()
    22.     {
    23.         Debug.Log("new void test");
    24.         _listProperty.arraySize += 1;
    25.     }
    26. }

    try to use something it
     
  3. uMathieu

    uMathieu

    Unity Technologies

    Joined:
    Jun 6, 2017
    Posts:
    398
    Proper ListView binding will be released in 19.3. I the meantime, I updated @imaewyn example code to use both a list view and a propertyfield to edit an array.

    Assuming you have this List_SO MonoBehaviour:

    public class List_SO : MonoBehaviour
    {
    public List<string> firstLists = new List<string>();
    }


    Here is the modified editor code. Keep in mind this is a workaround until you can call ListView.Bind() directly in 19.3 as this is less efficient:

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using UnityEngine.UIElements;
    4. using UnityEditor.UIElements;
    5. using System.Collections;
    6. using System;
    7. using System.Collections.Generic;
    8.  
    9. [CustomEditor(typeof(List_SO))]
    10. public class SO_UI : Editor
    11. {
    12.     SerializedProperty _listProperty;
    13.  
    14.     public override VisualElement CreateInspectorGUI()
    15.     {
    16.         _listProperty = serializedObject.FindProperty("firstLists");
    17.  
    18.         var root = new VisualElement();
    19.         var b = new Button(() => Test());
    20.         b.text = "Add";
    21.  
    22.         root.Add(b);
    23.         root.Add(new PropertyField(_listProperty));
    24.  
    25.         ListView lv = new ListView();
    26.         root.Add(lv);
    27.         lv.itemHeight = 20;
    28.         new ListViewBindingWhileWaitingForRealBindings(lv, _listProperty, true);
    29.  
    30.         //TODO: provide these styles in uss instead
    31.         lv.style.flexGrow = 1;
    32.         lv.style.minHeight = 200;
    33.  
    34.         return root;
    35.     }
    36.     public void Test()
    37.     {
    38.         _listProperty.arraySize += 1;
    39.         _listProperty.serializedObject.ApplyModifiedProperties();
    40.     }
    41. }
    42.  
    43.  
    44. // Somewhat inefficient workaround while waiting for real ListView binding support in 19.3
    45. class ListViewBindingWhileWaitingForRealBindings
    46. {
    47.     SerializedObjectList m_DataList;
    48.  
    49.     SerializedObject boundObject;
    50.     SerializedProperty boundProperty;
    51.     SerializedProperty m_ArraySize;
    52.     int m_ListViewArraySize;
    53.     bool m_DisplayArraySize;
    54.  
    55.     ListView listView { get; set;}
    56.  
    57.     public ListViewBindingWhileWaitingForRealBindings(ListView listView, SerializedProperty prop, bool displayArraySize)
    58.     {
    59.         boundProperty = prop;
    60.         boundObject = prop.serializedObject;
    61.  
    62.         m_DataList = new SerializedObjectList(prop, displayArraySize);
    63.         m_ArraySize = m_DataList.ArraySize;
    64.         m_ListViewArraySize = m_DataList.ArraySize.intValue;
    65.         m_DisplayArraySize = displayArraySize;
    66.  
    67.         this.listView = listView;
    68.  
    69.         if (listView.makeItem == null)
    70.         {
    71.             listView.makeItem = () => MakeListViewItem();
    72.         }
    73.  
    74.         if (listView.bindItem == null)
    75.         {
    76.             listView.bindItem = (v, i) => BindListViewItem(v, i);
    77.         }
    78.  
    79.         listView.itemsSource = m_DataList;
    80.  
    81.         listView.schedule.Execute( () => Update()).Every(100); //we poll a few times a second
    82.     }
    83.  
    84.     VisualElement MakeListViewItem()
    85.     {
    86.         return new PropertyField();
    87.     }
    88.  
    89.     void BindListViewItem(VisualElement ve, int index)
    90.     {
    91.         var field = ve as IBindable;
    92.         if (field == null)
    93.         {
    94.             //we find the first Bindable
    95.             field = ve.Query().Where(x => x is IBindable).First() as IBindable;
    96.         }
    97.  
    98.         if (field == null)
    99.         {
    100.             //can't default bind to anything!
    101.             throw new InvalidOperationException("Can't find BindableElement: please provide BindableVisualElements or provide your own Listview.bindItem callback");
    102.         }
    103.  
    104.         object item = listView.itemsSource[index];
    105.         var itemProp = item as SerializedProperty;
    106.         field.bindingPath = itemProp.propertyPath;
    107.         ve.Bind(itemProp.serializedObject);
    108.     }
    109.  
    110.     void UpdateArraySize()
    111.     {
    112.         m_DataList.RefreshProperties(boundProperty, m_DisplayArraySize);
    113.         m_ArraySize = m_DataList.ArraySize;
    114.         m_ListViewArraySize = m_ArraySize.intValue;
    115.         listView.Refresh();
    116.     }
    117.  
    118.  
    119.     public void Update()
    120.     {
    121.         try
    122.         {
    123.             if (boundObject != null && boundProperty != null)
    124.             {
    125.                 boundObject.UpdateIfRequiredOrScript();
    126.  
    127.                 int currentArraySize = m_ArraySize.intValue;
    128.  
    129.                 if (currentArraySize != m_ListViewArraySize)
    130.                 {
    131.                     UpdateArraySize();
    132.                 }
    133.             }
    134.         }
    135.         catch (ArgumentNullException)
    136.         {
    137.             //this can happen when serializedObject has been disposed of
    138.         }
    139.     }
    140. }
    141.  
    142. internal class SerializedObjectList : IList
    143. {
    144.     public SerializedProperty ArraySize { get; private set; }
    145.  
    146.     List<SerializedProperty> properties;
    147.     public SerializedObjectList(SerializedProperty parentProperty, bool includeArraySize)
    148.     {
    149.         RefreshProperties(parentProperty, includeArraySize);
    150.     }
    151.  
    152.     public void RefreshProperties(SerializedProperty parentProperty, bool includeArraySize)
    153.     {
    154.         var property = parentProperty.Copy();
    155.         var endProperty = property.GetEndProperty();
    156.  
    157.         property.NextVisible(true); // Expand the first child.
    158.  
    159.         properties = new List<SerializedProperty>();
    160.         do
    161.         {
    162.             if (SerializedProperty.EqualContents(property, endProperty))
    163.                 break;
    164.  
    165.             if (property.propertyType == SerializedPropertyType.ArraySize)
    166.             {
    167.                 ArraySize = property.Copy();
    168.                 if (includeArraySize)
    169.                 {
    170.                     properties.Add(ArraySize);
    171.                 }
    172.             }
    173.             else
    174.             {
    175.                 properties.Add(property.Copy());
    176.             }
    177.         }
    178.         while (property.NextVisible(false)); // Never expand children.
    179.  
    180.         if (ArraySize == null)
    181.         {
    182.             throw new ArgumentException("Can't find array size property!");
    183.         }
    184.     }
    185.  
    186.     public object this[int index]
    187.     {
    188.         get
    189.         {
    190.             return properties[index];
    191.         }
    192.         set
    193.         {
    194.             throw new NotImplementedException();
    195.         }
    196.     }
    197.  
    198.     public bool IsReadOnly => true;
    199.  
    200.     public bool IsFixedSize => true;
    201.  
    202.     public int Count => properties.Count;
    203.  
    204.     bool ICollection.IsSynchronized
    205.     {
    206.         get { return (properties as ICollection).IsSynchronized; }
    207.     }
    208.  
    209.     object ICollection.SyncRoot
    210.     {
    211.         get { return (properties as ICollection).SyncRoot; }
    212.     }
    213.  
    214.     public int Add(object value)
    215.     {
    216.         throw new NotImplementedException();
    217.     }
    218.  
    219.     public void Clear()
    220.     {
    221.         throw new NotImplementedException();
    222.     }
    223.  
    224.     public bool Contains(object value)
    225.     {
    226.         return IndexOf(value) >= 0;
    227.     }
    228.  
    229.     public void CopyTo(Array array, int index)
    230.     {
    231.         throw new NotImplementedException();
    232.     }
    233.  
    234.     public IEnumerator GetEnumerator()
    235.     {
    236.         return properties.GetEnumerator();
    237.     }
    238.  
    239.     public int IndexOf(object value)
    240.     {
    241.         var prop = value as SerializedProperty;
    242.  
    243.         if (value != null && prop != null)
    244.         {
    245.             return properties.IndexOf(prop);
    246.         }
    247.         return -1;
    248.     }
    249.  
    250.     public void Insert(int index, object value)
    251.     {
    252.         throw new NotImplementedException();
    253.     }
    254.  
    255.     public void Remove(object value)
    256.     {
    257.         throw new NotImplementedException();
    258.     }
    259.  
    260.     public void RemoveAt(int index)
    261.     {
    262.         throw new NotImplementedException();
    263.     }
    264. }
    265.  
     
    Misnomer, dan-kostin and imaewyn like this.
  4. Sangemdoko

    Sangemdoko

    Joined:
    Dec 15, 2013
    Posts:
    222
    Thank you @UnityMat.
    The ListView.Bind will be available in 19.3. Will it be retrofitted to 2019.1 and 2019.2 too? or will it be only 2019.3 onwards?
     
  5. uMathieu

    uMathieu

    Unity Technologies

    Joined:
    Jun 6, 2017
    Posts:
    398
    It most likely won't be backported to 19.2. In the meantime, you can use this workaround, Updating it when 19.3 is released should be straighforward.
     
  6. Sangemdoko

    Sangemdoko

    Joined:
    Dec 15, 2013
    Posts:
    222
    Knowing that will help me plan things in anticipation. Thank you for the help!
     
  7. tonycoculuzzi

    tonycoculuzzi

    Joined:
    Jun 2, 2011
    Posts:
    301
    Was this still planned for 2019.3? I'm not seeing it in 2020.1.0

    Edit: Ah never mind, I found it!
     
    Last edited: Aug 8, 2020
  8. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    Please do share where you found it! I was about to implement binding myself.
     
  9. uMathieu

    uMathieu

    Unity Technologies

    Joined:
    Jun 6, 2017
    Posts:
    398
    Here is a sample on how to use the ListView with SerializedObjects
    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using UnityEditor;
    4. using UnityEngine;
    5. using UnityEngine.UIElements;
    6. using UnityEditor.UIElements;
    7. using UnityEngine.Serialization;
    8.  
    9.  
    10. public class ListViewBindingExample : EditorWindow
    11. {
    12.     [MenuItem("Window/UI Toolkit/ListViewBindingExample")]
    13.     public static void ShowExample()
    14.     {
    15.         ListViewBindingExample wnd = GetWindow<ListViewBindingExample>();
    16.         wnd.titleContent = new GUIContent("ListViewBindingExample");
    17.     }
    18.  
    19.     [Serializable]
    20.     public struct CustomStruct
    21.     {
    22.         public string StringValue;
    23.         public float FloatValue;
    24.         public CustomStruct(string strValue, float fValue)
    25.         {
    26.             StringValue = strValue;
    27.             FloatValue = fValue;
    28.         }
    29.     }
    30.    
    31.     public enum DisplayArrayType
    32.     {
    33.         IntegerNumbers,
    34.         CustomStructs
    35.     }
    36.    
    37.     [SerializeField]
    38.     private DisplayArrayType m_DisplayArrayType;
    39.  
    40.     [SerializeField] private List<int> m_Numbers;
    41.     [SerializeField] private List<CustomStruct> m_CustomStructs;
    42.  
    43.    
    44.     private ListView m_ListView;
    45.     private SerializedObject serializedObject;
    46.    
    47.     private SerializedProperty m_ArraySizeProperty;
    48.     private SerializedProperty m_ArrayProperty;
    49.  
    50.     private int m_ListViewInsertIndex = -1; // To make sure we insert the ListView at the right place in our visualTree
    51.    
    52.     public void CreateGUI()
    53.     {
    54.         CreateDataIfNecessary();
    55.  
    56.         serializedObject = new SerializedObject(this);
    57.         VisualElement root = rootVisualElement;
    58.  
    59.         var rowContainer = new VisualElement();
    60.         rowContainer.style.flexDirection = FlexDirection.Row;
    61.         rowContainer.style.justifyContent = Justify.FlexStart;
    62.  
    63.         var dataSelector = new EnumField();
    64.         dataSelector.bindingPath = nameof(m_DisplayArrayType);
    65.        
    66.         dataSelector.RegisterValueChangedCallback(SwitchDisplayedData);
    67.  
    68.         root.Add(dataSelector);
    69.  
    70.         AddButton(rowContainer, "Default", () => CreateListView(false, false));
    71.         AddButton(rowContainer, "Custom MakeItem", () => CreateListView(true, false));
    72.         AddButton(rowContainer, "Custom MakeItem+BindItem", () => CreateListView(true, true));
    73.  
    74.         root.Add(rowContainer);
    75.        
    76.         CreateListView(false, false);
    77.        
    78.         rowContainer = new VisualElement();
    79.         rowContainer.style.flexDirection = FlexDirection.Row;
    80.         rowContainer.style.justifyContent = Justify.FlexEnd;
    81.         root.Add(rowContainer);
    82.  
    83.        
    84.         AddButton(rowContainer, "-", DecreaseArraySize);
    85.         AddButton(rowContainer, "+", IncreaseArraySize);
    86.        
    87.         root.Bind(serializedObject);
    88.     }
    89.  
    90.     private void CreateDataIfNecessary()
    91.     {
    92.         if (m_Numbers == null)
    93.         {
    94.             m_Numbers = new List<int>() {1, 2, 3};
    95.         }
    96.  
    97.         if (m_CustomStructs == null)
    98.         {
    99.             m_CustomStructs = new List<CustomStruct>();
    100.             for (var i = 0; i < 3; ++i)
    101.             {
    102.                 m_CustomStructs.Add(new CustomStruct($"Value number {i}", i + 0.5f));
    103.             }
    104.         }
    105.     }
    106.  
    107.     private void IncreaseArraySize()
    108.     {
    109.         m_ArraySizeProperty.intValue++;
    110.         serializedObject.ApplyModifiedProperties();
    111.     }
    112.  
    113.     private void DecreaseArraySize()
    114.     {
    115.         if (m_ArraySizeProperty.intValue > 0)
    116.         {
    117.             m_ArraySizeProperty.intValue--;
    118.             serializedObject.ApplyModifiedProperties();
    119.         }
    120.     }
    121.  
    122.  
    123.     void AddButton(VisualElement container, string label, Action onClick)
    124.     {
    125.         container.Add(new Button(onClick) {text = label});
    126.     }
    127.  
    128.     private void SwitchDisplayedData(ChangeEvent<Enum> evt)
    129.     {
    130.         var newValue = (DisplayArrayType) evt.newValue;
    131.  
    132.         if (m_DisplayArrayType != newValue)
    133.  
    134.         {
    135.             // Because we're hooked before the bindings system, we'll receive value changes before it.
    136.             // So we need to affect the value right away before recreating our list
    137.             m_DisplayArrayType = newValue;
    138.             CreateListView(false, false);
    139.         }
    140.     }
    141.  
    142.     void CreateListView(bool customMakeItem, bool customBindItem)
    143.     {
    144.         if (m_ListView != null)
    145.         {
    146.             //We clean ourselves up
    147.             m_ListView.Unbind();
    148.             m_ListView?.RemoveFromHierarchy();
    149.         }
    150.        
    151.         m_ListView = new ListView();
    152.         m_ListView.showBoundCollectionSize = false;
    153.  
    154.         m_ListView.name = "List-" + m_DisplayArrayType.ToString();
    155.        
    156.         if (m_DisplayArrayType == DisplayArrayType.CustomStructs)
    157.         {
    158.             m_ListView.bindingPath = nameof(m_CustomStructs);
    159.             m_ListView.itemHeight = 60;
    160.         }
    161.         else
    162.         {
    163.             m_ListView.bindingPath = nameof(m_Numbers);
    164.             m_ListView.itemHeight = 20;
    165.         }
    166.  
    167.         m_ArrayProperty = serializedObject.FindProperty(m_ListView.bindingPath);
    168.         m_ArraySizeProperty = serializedObject.FindProperty(m_ListView.bindingPath + ".Array.size");
    169.        
    170.         m_ListView.style.flexGrow = 1;
    171.  
    172.         if (customMakeItem || customBindItem)
    173.         {
    174.             m_ListView.name = m_ListView.name + "-custom-item";
    175.             // You can have only make item (default bindItem should work)
    176.             if (m_DisplayArrayType == DisplayArrayType.CustomStructs)
    177.             {
    178.                 m_ListView.makeItem = () => CreateCustomStructListItem(customBindItem);
    179.             }
    180.             else
    181.             {
    182.                 m_ListView.makeItem = () => CreateNumberListItem(customBindItem);
    183.             }
    184.         }
    185.  
    186.         if (customBindItem)
    187.         {
    188.             m_ListView.name += "+bind";
    189.  
    190.             // The default bindItem will find the first IBindable type and bind the property to it
    191.             // If you really have specific use cases you might want to set your own bindItem
    192.          
    193.  
    194.             m_ListView.bindItem = ListViewBindItem;
    195.         }
    196.  
    197.         if (m_ListViewInsertIndex < 0)
    198.         {
    199.             m_ListViewInsertIndex = rootVisualElement.childCount;
    200.             rootVisualElement.Add(m_ListView);
    201.                
    202.         }
    203.         rootVisualElement.Insert(m_ListViewInsertIndex, m_ListView);
    204.         m_ListView.Bind(serializedObject);
    205.     }
    206.  
    207.     private void AddRemoveItemButton(VisualElement row, bool enable)
    208.     {
    209.         var button = new Button() {text = "-"};
    210.         button.RegisterCallback<ClickEvent>((evt) =>
    211.         {
    212.             var clickedElement = evt.target as VisualElement;
    213.  
    214.             if (clickedElement != null && clickedElement.userData is int index)
    215.             {
    216.                 m_ArrayProperty.DeleteArrayElementAtIndex(index);
    217.                 serializedObject.ApplyModifiedProperties();
    218.             }
    219.         });
    220.  
    221.         if (enable)
    222.         {
    223.             button.tooltip = "Remove this item from the list";
    224.         }
    225.         else
    226.         {
    227.             button.SetEnabled(false);
    228.             row.tooltip = "Item removing is only available with custom BindItem";
    229.         }
    230.         row.Add(button);
    231.     }
    232.    
    233.     VisualElement CreateCustomStructListItem(bool removeButtonAvailable)
    234.     {
    235.         var keyFrameContainer = new BindableElement(); //BindableElement so the default bind can assign the item's root property
    236.         var lbl = new Label("Custom Item UI");
    237.         lbl.AddToClassList("custom-label");
    238.         var row = new VisualElement();
    239.         row.style.flexDirection = FlexDirection.Row;
    240.         row.style.justifyContent = Justify.SpaceBetween;
    241.         row.Add(lbl);
    242.  
    243.         AddRemoveItemButton(row, removeButtonAvailable);
    244.  
    245.         keyFrameContainer.Add(row);
    246.         keyFrameContainer.Add(new TextField() {bindingPath = nameof(CustomStruct.StringValue)});
    247.         keyFrameContainer.Add(new FloatField() {bindingPath = nameof(CustomStruct.FloatValue)});
    248.         return keyFrameContainer;
    249.     }
    250.    
    251.     VisualElement CreateNumberListItem(bool removeButtonAvailable)
    252.     {
    253.         var row = new VisualElement(); //BindableElement so the default bind can assign the item's root property
    254.         row.style.flexDirection = FlexDirection.Row;
    255.         row.style.justifyContent = Justify.SpaceBetween;
    256.  
    257.         row.Add(new Label()); // default bind need this to be the first Bindable in the tree
    258.         AddRemoveItemButton(row, removeButtonAvailable);
    259.         return row;
    260.     }
    261.    
    262.     void ListViewBindItem(VisualElement element, int index)
    263.     {
    264.         var label = element.Q<Label>(className: "custom-label");
    265.         if (label != null)
    266.         {
    267.             label.text = "Custom Item UI (Custom Bound)";
    268.         }
    269.  
    270.         var button = element.Q<Button>();
    271.         if (button != null)
    272.         {
    273.             button.userData = index;
    274.         }
    275.  
    276.         //we find the first Bindable
    277.         var field = element as IBindable;
    278.         if (field == null)
    279.         {
    280.             //we dig through children
    281.             field = element.Query().Where(x => x is IBindable).First() as IBindable;
    282.         }
    283.  
    284.         // Bound ListView.itemsSource is a IList of SerializedProperty
    285.         var itemProp = m_ListView.itemsSource[index] as SerializedProperty;
    286.  
    287.         field.bindingPath = itemProp.propertyPath;
    288.  
    289.         element.Bind(itemProp.serializedObject);
    290.     }
    291. }
    292.  
     
  10. omerperry

    omerperry

    Joined:
    Nov 18, 2020
    Posts:
    2
    This is not working properly with nested serialized arrays. (Unity 2020.3.24f1)

    I have a list of nodes, each node has a list of consequences.

    When binding
    nodes
    on a ListView - I get a list of all the nodes.
    But when binding (using the same code above) on
    nodes.Array.data[i].consequences
    (where i is the node number) - it fails with "Can't find array size property!". Maybe it's worth mentioning that nodes is a list of managed references.

    Here's the stack trace, Unfortunately I don't have the source code to understand what's going on.
    Code (csharp):
    1. ArgumentException: Can't find array size property!
    2. UnityEditor.UIElements.Bindings.SerializedObjectList.RefreshProperties (UnityEditor.SerializedProperty parentProperty, System.Boolean includeArraySize) (at <1ada6c7052bb42378c5ec1bd01fc4723>:0)
    3. UnityEditor.UIElements.Bindings.SerializedObjectList..ctor (UnityEditor.SerializedProperty parentProperty, System.Boolean includeArraySize) (at <1ada6c7052bb42378c5ec1bd01fc4723>:0)
    4. UnityEditor.UIElements.Bindings.ListViewSerializedObjectBinding.SetBinding (UnityEngine.UIElements.ListView listView, UnityEditor.UIElements.Bindings.SerializedObjectBindingContext context, UnityEditor.SerializedProperty prop) (at <1ada6c7052bb42378c5ec1bd01fc4723>:0)
    5. UnityEditor.UIElements.Bindings.ListViewSerializedObjectBinding.CreateBind (UnityEngine.UIElements.ListView listView, UnityEditor.UIElements.Bindings.SerializedObjectBindingContext context, UnityEditor.SerializedProperty prop) (at <1ada6c7052bb42378c5ec1bd01fc4723>:0)
    6. UnityEditor.UIElements.Bindings.SerializedObjectBindingContext.BindListView (UnityEngine.UIElements.ListView listView, UnityEditor.SerializedProperty prop) (at <1ada6c7052bb42378c5ec1bd01fc4723>:0)
    7. UnityEditor.UIElements.Bindings.SerializedObjectBindingContext.CreateBindingObjectForProperty (UnityEngine.UIElements.VisualElement element, UnityEditor.SerializedProperty prop) (at <1ada6c7052bb42378c5ec1bd01fc4723>:0)
    8. UnityEditor.UIElements.Bindings.SerializedObjectBindingContext.BindPropertyRelative (UnityEngine.UIElements.IBindable field, UnityEditor.SerializedProperty parentProperty) (at <1ada6c7052bb42378c5ec1bd01fc4723>:0)
    9. UnityEditor.UIElements.Bindings.SerializedObjectBindingContext.BindTree (UnityEngine.UIElements.VisualElement element, UnityEditor.SerializedProperty parentProperty) (at <1ada6c7052bb42378c5ec1bd01fc4723>:0)
    10. UnityEditor.UIElements.Bindings.SerializedObjectBindingContext.ContinueBinding (UnityEngine.UIElements.VisualElement element, UnityEditor.SerializedProperty parentProperty) (at <1ada6c7052bb42378c5ec1bd01fc4723>:0)
    11. UnityEditor.UIElements.Bindings.SerializedObjectBindingContext.Bind (UnityEngine.UIElements.VisualElement element) (at <1ada6c7052bb42378c5ec1bd01fc4723>:0)
    12. UnityEditor.UIElements.Bindings.DefaultSerializedObjectBindingImplementation+BindingRequest.Bind (UnityEngine.UIElements.VisualElement element) (at <1ada6c7052bb42378c5ec1bd01fc4723>:0)
    13. UnityEngine.UIElements.VisualTreeBindingsUpdater.Update () (at <4c8a9874288b4fb78fa7fdbcd8065b00>:0)
    14. UnityEngine.UIElements.VisualTreeUpdater.UpdateVisualTreePhase (UnityEngine.UIElements.VisualTreeUpdatePhase phase) (at <4c8a9874288b4fb78fa7fdbcd8065b00>:0)
    15. UnityEngine.UIElements.Panel.UpdateBindings () (at <4c8a9874288b4fb78fa7fdbcd8065b00>:0)
    16. UnityEngine.UIElements.UIElementsUtility.UnityEngine.UIElements.IUIElementsUtility.UpdateSchedulers () (at <4c8a9874288b4fb78fa7fdbcd8065b00>:0)
    17. UnityEngine.UIElements.UIEventRegistration.UpdateSchedulers () (at <4c8a9874288b4fb78fa7fdbcd8065b00>:0)
    18. UnityEditor.RetainedMode.UpdateSchedulers () (at <1ada6c7052bb42378c5ec1bd01fc4723>:0)
     
  11. TimPhil

    TimPhil

    Joined:
    Nov 26, 2016
    Posts:
    1
    The sample code was really helpful to see what is possible with ListView. But I'm trying to apply what it shows to a situation which seems almost identical, and I just can't seem to get it to work at all.

    I have a basic character speech system based on a 'Dialogue' scriptable object which contains a list of DialogueLine instances:

    Code (CSharp):
    1. [CreateAssetMenu (menuName="UI dialogue/Dialogue")]
    2. public class Dialogue : ScriptableObject
    3. {
    4.     public string dialogueTitle;
    5.     public string dialogueSummary;
    6.     [SerializeField] public List<DialogueLine> lines;
    7. }
    8.  
    9. [System.Serializable]
    10. public class DialogueLine
    11. {
    12.     public string charaName;
    13.     public string lineText;
    14.     public bool charaOnRight;
    15. }
    I'm trying to create a custom editor window to view a Dialogue object, including a ListView displaying its DialogueLines:

    Code (CSharp):
    1. public class BasicSpeechWindow : EditorWindow
    2. {
    3.     [SerializeField] private Dialogue sampleDialogue;
    4.     [SerializeField] private List<DialogueLine> sampleList;  
    5.    
    6.     [MenuItem("MoonWorld Tools/BasicSpeechWindow")]
    7.     public static void ShowExample()
    8.     {
    9.         BasicSpeechWindow wnd = GetWindow<BasicSpeechWindow>();
    10.         wnd.titleContent = new GUIContent("Basic Speech Window");
    11.     }
    12.  
    13.     public void CreateGUI()
    14.     {
    15.         VisualElement root = rootVisualElement;
    16.         VisualElement label = new Label("Basic Speech Window");
    17.         root.Add(label);
    18.  
    19.         // These TextFields bind correctly
    20.         var titleText = new TextField("Dialogue title");
    21.         titleText.bindingPath = "dialogueTitle";
    22.         root.Add(titleText);
    23.  
    24.         var summaryText = new TextField("Dialogue summary");
    25.         summaryText.bindingPath = "dialogueSummary";
    26.         root.Add(summaryText);
    27.  
    28.         var serializedDialogue = new SerializedObject(sampleDialogue);
    29.         root.Bind(serializedDialogue);
    30.  
    31.         // This ListView doesn't bind at all
    32.         var dialogueListView = new ListView();
    33.         dialogueListView.showBoundCollectionSize = false;
    34.         dialogueListView.bindingPath = nameof(sampleDialogue.lines);
    35.        
    36.         sampleList = sampleDialogue.lines;
    37.         dialogueListView.bindingPath = nameof(sampleList);
    38.         dialogueListView.Bind(serializedDialogue);
    39.        
    40.         root.Add(dialogueListView);
    41.     }
    42. }
    I've been trying to apply the techniques used to display CustomStructs in uMathieu's example code, but I can't see what I'm doing differently! There's almost certainly an obvious answer, but I've been struggling with this for over a week, so any pointers would be gratefully received!
     
  12. uMathieu

    uMathieu

    Unity Technologies

    Joined:
    Jun 6, 2017
    Posts:
    398
    You were almost there @TimPhil ! Here my working version:
    Code (CSharp):
    1.  
    2. public void CreateGUI()
    3. {
    4.     VisualElement root = rootVisualElement;
    5.     VisualElement label = new Label("Basic Speech Window");
    6.     root.Add(label);
    7.  
    8.     // These TextFields bind correctly
    9.     var titleText = new TextField("Dialogue title");
    10.     titleText.bindingPath = "dialogueTitle";
    11.     root.Add(titleText);
    12.  
    13.     var summaryText = new TextField("Dialogue summary");
    14.     summaryText.bindingPath = "dialogueSummary";
    15.     root.Add(summaryText);
    16.  
    17.  
    18.     // You were almost there :)
    19.     var dialogueListView = new ListView();
    20.     dialogueListView.showBoundCollectionSize = false;
    21.     dialogueListView.bindingPath = "lines";
    22.     dialogueListView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;  // FixedHeight is faster but doesn't play well with foldouts!
    23.     dialogueListView.style.flexGrow = 1; // Your ListView needs to take all the remaining space
    24.    
    25.     // uncomment this to use the custom editor defined below
    26.     // dialogueListView.makeItem = () => new DialogElement();
    27.    
    28.     root.Add(dialogueListView);
    29.  
    30.     var serializedDialogue = new SerializedObject(sampleDialogue);
    31.    
    32.     // We add all of our elements to the tree
    33.     // Then we bind once with at the common ancestor level
    34.     root.Bind(serializedDialogue);
    35. }
    36.  
    37. class DialogElement : BindableElement
    38. {
    39.     public DialogElement()
    40.     {
    41.         Add(new TextField()
    42.         {
    43.             bindingPath = nameof(DialogueLine.charaName),
    44.             label = "Character"
    45.         });
    46.        
    47.         Add(new TextField()
    48.         {
    49.             bindingPath = nameof(DialogueLine.lineText),
    50.             multiline = true,
    51.             label = "Says"
    52.         });
    53.        
    54.         Add(new Toggle()
    55.         {
    56.             bindingPath = nameof(DialogueLine.charaOnRight),
    57.             label = "On Right"
    58.         });
    59.     }
    60. }
    61.  
     
    Last edited: Feb 25, 2022
    marcospgp likes this.
  13. beevik_

    beevik_

    Joined:
    Sep 27, 2020
    Posts:
    101
    I created a shorter, pared down version of Mathieu's example code containing only the custom struct binding parts. I'm posting it just in case anyone else finds it useful.

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using UnityEditor;
    4. using UnityEditor.UIElements;
    5. using UnityEngine;
    6. using UnityEngine.UIElements;
    7.  
    8. public class ListViewBindingExample : EditorWindow
    9. {
    10.     [MenuItem("Window/UI Toolkit/ListView Binding Example")]
    11.     public static void ShowExample()
    12.     {
    13.         var window = GetWindow<ListViewBindingExample>();
    14.         window.titleContent = new GUIContent("ListView Binding Example");
    15.         window.Show();
    16.     }
    17.  
    18.     [Serializable]
    19.     public struct Item
    20.     {
    21.         public string StringValue;
    22.         public float FloatValue;
    23.  
    24.         public Item(string strValue, float fValue)
    25.         {
    26.             StringValue = strValue;
    27.             FloatValue = fValue;
    28.         }
    29.     }
    30.  
    31.     [SerializeField]
    32.     private List<Item> m_ItemList;
    33.  
    34.     private ListView m_ItemListView;
    35.  
    36.     private SerializedObject m_SerializedObject;
    37.     private SerializedProperty m_ArrayProperty;
    38.     private SerializedProperty m_ArraySizeProperty;
    39.  
    40.     public void CreateGUI()
    41.     {
    42.         // Initialize the item list data that will be bound to the UI.
    43.         if (m_ItemList == null)
    44.         {
    45.             m_ItemList = new List<Item>();
    46.             for (int i = 0; i < 3; ++i)
    47.             {
    48.                 m_ItemList.Add(new Item($"Value number {i}", i + 0.5f));
    49.             }
    50.         }
    51.  
    52.         // Create a serialized object from this window so we can bind data to
    53.         // it.
    54.         m_SerializedObject = new SerializedObject(this);
    55.  
    56.         // Create the list view and bind it.
    57.         m_ItemListView = new ListView();
    58.         m_ItemListView.name = "item-list";
    59.         m_ItemListView.showBoundCollectionSize = false;
    60.         m_ItemListView.fixedItemHeight = 80;
    61.         m_ItemListView.style.flexGrow = 1;
    62.         m_ItemListView.makeItem = MakeItem;
    63.         m_ItemListView.bindItem = BindItem;
    64.         m_ItemListView.bindingPath = nameof(m_ItemList);
    65.         m_ItemListView.Bind(m_SerializedObject);
    66.         rootVisualElement.Add(m_ItemListView);
    67.  
    68.         // Create the footer row containing "+" and "-" buttons.
    69.         VisualElement row = new VisualElement();
    70.         row.style.flexDirection = FlexDirection.Row;
    71.         row.style.justifyContent = Justify.FlexEnd;
    72.         row.Add(new Button(IncreaseArraySize) { text = "+" });
    73.         row.Add(new Button(DecreaseArraySize) { text = "-" });
    74.         rootVisualElement.Add(row);
    75.  
    76.         // Find the custom item list's array properties.
    77.         m_ArrayProperty = m_SerializedObject.FindProperty(nameof(m_ItemList));
    78.         m_ArraySizeProperty = m_SerializedObject.FindProperty(nameof(m_ItemList) + ".Array.size");
    79.     }
    80.  
    81.     private VisualElement MakeItem()
    82.     {
    83.         // Create a row to hold a label and "-" button.
    84.         var row = new VisualElement();
    85.         row.style.flexDirection = FlexDirection.Row;
    86.         row.style.justifyContent = Justify.SpaceBetween;
    87.  
    88.         // Create the label.
    89.         var label = new Label("Custom Item");
    90.         label.AddToClassList("custom-label");
    91.         row.Add(label);
    92.  
    93.         // Create the "-" button.
    94.         var button = new Button { text = "-", tooltip = "Remove this item from the list" };
    95.         button.RegisterCallback<ClickEvent>((evt) =>
    96.         {
    97.             VisualElement element = evt.target as VisualElement;
    98.             if (element != null && element.userData is int index)
    99.             {
    100.                 m_ArrayProperty.DeleteArrayElementAtIndex(index);
    101.                 m_SerializedObject.ApplyModifiedProperties();
    102.             }
    103.         });
    104.         row.Add(button);
    105.  
    106.         // Add the row and the item field editors to a bindable element container.
    107.         var container = new BindableElement();
    108.         container.Add(row);
    109.         container.Add(new TextField() { bindingPath = nameof(Item.StringValue) });
    110.         container.Add(new FloatField() { bindingPath = nameof(Item.FloatValue) });
    111.  
    112.         return container;
    113.     }
    114.  
    115.     private void BindItem(VisualElement element, int index)
    116.     {
    117.         // Assign the array index to the user data of the "-" button.
    118.         var button = element.Q<Button>();
    119.         if (button != null)
    120.         {
    121.             button.userData = index;
    122.         }
    123.  
    124.         // Find the first bindable element.
    125.         var field = element as IBindable;
    126.         if (field == null)
    127.         {
    128.             field = (IBindable)element.Query()
    129.                 .Where(x => x is IBindable)
    130.                 .First();
    131.         }
    132.  
    133.         // Bind the list view source element to the visual element.
    134.         var itemProp = (SerializedProperty)m_ItemListView.itemsSource[index];
    135.         field.bindingPath = itemProp.propertyPath;
    136.         element.Bind(itemProp.serializedObject);
    137.     }
    138.  
    139.     private void IncreaseArraySize()
    140.     {
    141.         m_ArraySizeProperty.intValue++;
    142.         m_SerializedObject.ApplyModifiedProperties();
    143.     }
    144.  
    145.     private void DecreaseArraySize()
    146.     {
    147.         if (m_ArraySizeProperty.intValue > 0)
    148.         {
    149.             m_ArraySizeProperty.intValue--;
    150.             m_SerializedObject.ApplyModifiedProperties();
    151.         }
    152.     }
    153. }
     
    adamgryu likes this.
  14. marcospgp

    marcospgp

    Joined:
    Jun 11, 2018
    Posts:
    194
    It's confusing how the documentation says
    itemSource
    must be provided but does not mention what to do when one wants to bind an existing
    SerializedProperty
    to the
    ListView
    .

    There's even an example of binding, but it is exclusive to XML, so doesn't help when one wants to bind through code.

    If
    itemSource
    is no longer necessary when calling
    .Bind()
    , why isn't there a constructor that reflects that? It's not very elegant to use the default (empty) constructor and then assign everything manually.

    Also, @beevik_ , are you sure one needs to set both
    .bindingPath
    and
    .Bind()
    ? Shouldn't
    .BindProperty()
    be enough (and make the code simpler)?
     
  15. adamgryu

    adamgryu

    Joined:
    Mar 1, 2014
    Posts:
    188
    I was trying to bind a ListView to a custom struct only via C#. Like @marcospgp says, the documentation is only for XML and I was having trouble adapting it to strictly C#.

    After looking through this thread, I found that the main thing I was missing from my intuition was that my item's container element needed to be a BindableElement. It's not clear from the example page, so it might be worth updating it with an example like this.

    For those looking for an even more succinct example of how to use ListView with a SerializedProperty binding via C#:
    Code (CSharp):
    1. [Serializable]
    2. public struct MyStruct {
    3.     public float myField;
    4. }
    5.  
    6. ...
    7.  
    8. public ListView CreateListView() {
    9.     var listView = new ListView();
    10.     listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
    11.     listView.showFoldoutHeader = true;
    12.     listView.headerTitle = "Elements";
    13.     listView.makeItem = () => {
    14.         var propertyField = new PropertyField();
    15.         propertyField.bindingPath = "myField";
    16.  
    17.         var container = new BindableElement(); // This MUST be a bindable element!
    18.         container.Add(propertyField);
    19.         return container;
    20.     };
    21.     return listView;
    22.     // NOTE: Don't forget to add the list view to your tree!
    23. }
    24.  
    25. ...
    26.  
    27. // Later when binding...
    28. listView.BindProperty(serializedObj.FindProperty("myListOfStructs"));
     
    Last edited: Aug 24, 2023