Search Unity

PropertyDrawer with UIElements, changes in array don't refresh inspector

Discussion in 'UI Toolkit' started by GalaadMoutoz, Sep 19, 2019.

  1. GalaadMoutoz

    GalaadMoutoz

    Joined:
    Apr 6, 2018
    Posts:
    6
    Hello,

    I have a custom PropertyDrawer using UIElements for my class BuildingSetElement.

    In it, I'm trying to add/remove elements from a list : the property is called "prefabs".
    I'm using property.InsertArrayElementAtIndex and then serializedObject.ApplyModifedProperties.

    The problem is that the inspector doesn't refresh to show the changes. I have to manually select another object and reselect the previous one.

    Thanks in advance.
    Galaad

    Code (CSharp):
    1. [System.Serializable]
    2. public class BuildingSetElement
    3. {
    4.      public List<GameObject> prefabs = new List<GameObject>();
    5. }
    6.  
    7. [CustomPropertyDrawer(typeof(BuildingSetElement))]
    8. public class BuildingSetElementDrawer : PropertyDrawer
    9. {
    10.     public override VisualElement CreatePropertyGUI(SerializedProperty property)
    11.     {
    12.         //Props
    13.         var prefabsProp = property.FindPropertyRelative("prefabs");
    14.  
    15.         //Structure
    16.         var root = new VisualElement();
    17.         var box = new Box();
    18.         root.Add(box);
    19.  
    20.         //Header
    21.         box.Add(new Label(property.displayName));
    22.  
    23.         //Add Prefabs
    24.         var addPrefabButton = new Button();
    25.         addPrefabButton.Add(new Label("+"));
    26.         addPrefabButton.RegisterCallback<MouseUpEvent>((e) =>
    27.         {
    28.             prefabsProp.InsertArrayElementAtIndex(0);
    29.             property.serializedObject.ApplyModifiedProperties();
    30.         });
    31.         box.Add(addPrefabButton);
    32.  
    33.         //Prefabs
    34.         for (int i = 0; i < prefabsProp.arraySize; i++)
    35.         {
    36.             box.Add(new PropertyField(prefabsProp.GetArrayElementAtIndex(i), "Prefab " + (i + 1)));
    37.  
    38.             int index = i;
    39.             var removePrefabButton = new Button();
    40.             removePrefabButton.Add(new Label("X"));
    41.             removePrefabButton.RegisterCallback<MouseUpEvent>((e) =>
    42.             {
    43.                 prefabsProp.DeleteArrayElementAtIndex(index);
    44.                 property.serializedObject.ApplyModifiedProperties();
    45.             });
    46.  
    47.             box.Add(removePrefabButton);
    48.         }
    49.  
    50.         root.styleSheets.Add(Resources.Load<StyleSheet>("BuildingEditor"));
    51.         return root;
    52.     }
    53. }
     
  2. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    I'm assuming you specifically don't want to use the default list created by binding a
    PropertyField
    to an array:
    upload_2019-9-20_15-37-50.png

    If that's the case, you're a somewhat in uncharted waters. But here's one possible approach:

    Create a
    PropertyField
    that binds to the
    arraySize
    itself. You can set the display style of this field to None so you don't actually see it (but it does need to be added to your
    root
    element). This will create an
    IntegerField
    when it's actually bound.

    This field won't exist within the scope of your
    CreatePropertyGUI()
    so you have to register for the
    GeometryChangeEvent
    on your
    arraySize
    PropertyField
    and only then, register for the
    ChangeEvent<int>
    on the child
    IntegerField
    .

    When you get a change event from the
    arraySize
    field, you have to Clear and recreate
    box
    element so I'd abstract its creation in a function that you call from
    CreatePropertyGUI()
    and from your
    arraySize
    ChangeEvent handler.

    Some inspiration, if you're brave enough, can come from the
    PropertyField
    's own implementation of array fields:
    https://github.com/Unity-Technologi...tor/Mono/UIElements/Controls/PropertyField.cs
    (look for
    SerializedPropertyType.ArraySize
    and
    UpdateArrayFoldout
    )
     
    dsfgddsfgdsgd likes this.
  3. GalaadMoutoz

    GalaadMoutoz

    Joined:
    Apr 6, 2018
    Posts:
    6
    Thank you so much for the comprehensive answer !

    This does work although it's quite convoluted.

    To be honest, it really feels like UIElements in its current state is very clunky and requires a surprisingly big amount of code to achieve simple / common ui look & logic (compared to web frameworks like vue for instance).
    I'm just giving feedback, although I love the idea of UIElements.

    I think for know I'll stick to auto gui with a few custom attribute drawers here and there (thanks NaughtyAttributes).

    Thank you again for the quick answer.
    Have a nice day,
    Galaad
     
  4. uMathieu

    uMathieu

    Unity Technologies

    Joined:
    Jun 6, 2017
    Posts:
    398
    The main thing about CreateInspectorGUI() and UIElements in general is that is is retained mode, so OnGUI is not called at every frame or event. For simple stuff, it's all good and default bindings work, however, when dealing with arrays, you have no choice to go with the default property field, or detect that the size has changed and add/remove fields by hand.

    You could use a ListView to display the array contents. In 19.3, the ListView will be bindable directly. In this other forum post, I posted this stop gap solution that works in the meantime.
    https://forum.unity.com/threads/uie...izedproperty-of-an-array.719570/#post-4827677

    If you want to do it yourself, here are a few alternatives:
    To detect that the size has changed, you can poll it :
    Code (CSharp):
    1. var sizeProperty = serializedObject.FindProperty("prefabs.Array.Size);
    2.  
    3. root.schedule.Execute( () => CheckSize(sizeProperty.intValue)).Every(100); // poll 10 times a second
    You could also, as uDamian suggested, Bind an invisible IntegerField to the "prefabs.Array.Size" and listen to ChangeEvent<int>, or implement a Bindable container where its value is bound to the array size of your "prefabs".

    I prototyped this third option here:
    Code (CSharp):
    1. [CustomEditor(typeof(BuildingSetElement))]
    2. public class BuildingSetElementEditor : Editor
    3. {
    4.     private SerializedProperty prefabsProperty;
    5.    
    6.     public override VisualElement CreateInspectorGUI()
    7.     {
    8.         var root  = new VisualElement();
    9.         root.Add(new Label("BuildingSetElement"));
    10.        
    11.         prefabsProperty = serializedObject.FindProperty("prefabs");
    12.  
    13.         var arrayElement = new ArrayInspectorElement(prefabsProperty, (p, i) => MakePrefabItem(p, i));
    14.         root.Add(arrayElement);
    15.        
    16.         // Since arrayElement will be bound, changing the value should resize the array directly
    17.         var add10Button = new Button( () => AddEntries(arrayElement, 10));
    18.         add10Button.text = "Add 10 elements";
    19.         root.Add(add10Button);
    20.         return root;
    21.     }
    22.  
    23.     VisualElement MakePrefabItem(string propertyPath, int index)
    24.     {
    25.         var box = new VisualElement();
    26.         var pf = new PropertyField(null, $"Prefab {index+1}");
    27.         pf.bindingPath = propertyPath;
    28.        
    29.         // This should belong in uss
    30.         box.style.flexDirection = FlexDirection.Row;
    31.         pf.style.flexGrow = 1;
    32.        
    33.         var removeButton = new Button(() => {
    34.             prefabsProperty.DeleteArrayElementAtIndex(index);
    35.             prefabsProperty.serializedObject.ApplyModifiedProperties();
    36.         });
    37.  
    38.         removeButton.text = "-";
    39.        
    40.         box.Add(pf);
    41.         box.Add(removeButton);
    42.         return box;
    43.     }
    44.    
    45.     void AddEntries(ArrayInspectorElement element, int count)
    46.     {
    47.         //we do this async to bypass event dispatch issues
    48.         element.schedule.Execute(() => element.value += count);
    49.     }
    50. }
    51.  
    52. public class ArrayInspectorElement : BindableElement, INotifyValueChanged<int>
    53. {
    54.     private readonly SerializedObject boundObject;
    55.     private readonly string m_ArrayPropertyPath;
    56.  
    57.     public Func<string,int,  VisualElement> makeItem { get; set; }
    58.  
    59.     public override VisualElement contentContainer => m_Container;
    60.    
    61.     private readonly VisualElement m_Container;
    62.    
    63.     public ArrayInspectorElement(SerializedProperty arrayProperty, Func<string, int,  VisualElement> makeItem)
    64.     {
    65.         var header = new VisualElement();
    66.        
    67.         header.Add(new Label(arrayProperty.displayName));
    68.        
    69.         var addButton = new Button(() =>
    70.         {
    71.             arrayProperty.InsertArrayElementAtIndex(0);
    72.             arrayProperty.serializedObject.ApplyModifiedProperties();
    73.         });
    74.         addButton.text = "+";
    75.         header.Add(addButton);
    76.  
    77.         // This belongs in uss
    78.         header.style.flexDirection = FlexDirection.Row;
    79.         header.style.justifyContent = Justify.SpaceBetween;
    80.  
    81.        
    82.         //We use a content container so that array size = child count      
    83.         // And the child management becomes easier
    84.         m_Container = new VisualElement() {name = "array-contents"};
    85.         this.hierarchy.Add(header);
    86.         this.hierarchy.Add(m_Container);
    87.        
    88.         m_ArrayPropertyPath = arrayProperty.propertyPath;
    89.         boundObject = arrayProperty.serializedObject;
    90.         this.makeItem = makeItem;
    91.  
    92.         var property = arrayProperty.Copy();
    93.         var endProperty = property.GetEndProperty();
    94.  
    95.         //We prefill the container since we know we will need this
    96.         property.NextVisible(true); // Expand the first child.
    97.         do
    98.         {
    99.             if (SerializedProperty.EqualContents(property, endProperty))
    100.                 break;
    101.             if (property.propertyType == SerializedPropertyType.ArraySize)
    102.             {
    103.                 arraySize = property.intValue;
    104.                 bindingPath = property.propertyPath;
    105.                 break;
    106.             }
    107.         }
    108.         while (property.NextVisible(false)); // Never expand children.
    109.  
    110.         UpdateCreatedItems();
    111.         //we assume we don't need to Bind here
    112.     }
    113.    
    114.     VisualElement AddItem(string propertyPath, int index)
    115.     {
    116.         VisualElement child;
    117.         if (makeItem != null)
    118.         {
    119.             child = makeItem(propertyPath, index);
    120.         }
    121.         else
    122.         {
    123.             var pf = new PropertyField();
    124.             pf.bindingPath = propertyPath;
    125.             child = pf;
    126.         }
    127.  
    128.         Add(child);
    129.         return child;
    130.     }
    131.  
    132.     bool UpdateCreatedItems()
    133.     {
    134.         int currentSize = childCount;
    135.  
    136.         int targetSize = this.arraySize;
    137.  
    138.         if (targetSize < currentSize)
    139.         {
    140.             for (int i = currentSize-1; i >= targetSize; --i)
    141.             {
    142.                 RemoveAt(i);
    143.             }
    144.         }else if (targetSize > currentSize)
    145.         {
    146.             for (int i = currentSize; i < targetSize; ++i)
    147.             {
    148.                 AddItem($"{m_ArrayPropertyPath}.Array.data[{i}]", i);
    149.             }
    150.  
    151.             return true; //we created new Items
    152.         }
    153.  
    154.         return false;
    155.     }
    156.  
    157.     private int arraySize = 0;
    158.     public void SetValueWithoutNotify(int newSize)
    159.     {
    160.         this.arraySize = newSize;
    161.  
    162.         if (UpdateCreatedItems())
    163.         {
    164.             //We rebind the array
    165.             this.Bind(boundObject);
    166.         }
    167.     }
    168.  
    169.     public int value
    170.     {
    171.         get => arraySize;
    172.         set
    173.         {
    174.             if (arraySize == value) return;
    175.            
    176.             if (panel != null)
    177.             {
    178.                 using (ChangeEvent<int> evt = ChangeEvent<int>.GetPooled(arraySize, value))
    179.                 {
    180.                     evt.target = this;
    181.                    
    182.                     // The order is important here: we want to update the value, then send the event,
    183.                     // so the binding writes and updates the serialized object
    184.                     arraySize = value;
    185.                     SendEvent(evt);
    186.                    
    187.                     //Then we remove or create + bind the needed items
    188.                     SetValueWithoutNotify(value);
    189.                 }
    190.             }
    191.             else
    192.             {
    193.                 SetValueWithoutNotify(value);
    194.             }
    195.         }
    196.     }
    197. }
    198.  
    The ArrayInspectorElement can now be used to display any array in the inspector.
     
    dsfgddsfgdsgd and oleg_v like this.
  5. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    1,091
    And what if I am? I've spent way too much time figuring out how and what works... Is there any way to simply repaint visual element? Somehow every time I add PropertyField not in CreatePropertyGUI it's completely empty.

    Simple example: hiding one PropertyField if other one is false:

    Code (CSharp):
    1. [Serializable]
    2. public class TestElement
    3. {
    4.     public bool ShowInt = true;
    5.     public int Int = 1;
    6. }
    This won't work properly:
    Code (CSharp):
    1. [CustomPropertyDrawer(typeof(TestElement))]
    2. public class TestElementUIE : PropertyDrawer
    3. {
    4.     public override VisualElement CreatePropertyGUI(SerializedProperty property)
    5.     {
    6.         var container = new VisualElement();
    7.         var showIntField = new PropertyField(property.FindPropertyRelative("ShowInt"));
    8.         container.Add(showIntField);
    9.         showIntField.RegisterCallback<ChangeEvent<bool>>(b => OnShowChange(b.newValue));
    10.         OnShowChange(property.FindPropertyRelative("ShowInt").boolValue);
    11.  
    12.         return container;
    13.  
    14.         void OnShowChange(bool isOn)
    15.         {
    16.             var intField = new PropertyField(property.FindPropertyRelative("Int"));
    17.             intField.name = "Int";
    18.             if (isOn)
    19.             {
    20.                 //this works only at start
    21.                 //otherwise adds empty VisualElement (why ?)
    22.                 container.Add(intField);
    23.             }
    24.             else
    25.             {        
    26.                 //this works fine
    27.                 var toRemove = container.Q("Int");
    28.                 if (toRemove != null)
    29.                     container.Remove(toRemove);
    30.             }
    31.         }
    32.     }
    33. }
    This works:
    Code (CSharp):
    1. [CustomPropertyDrawer(typeof(TestElement))]
    2. public class TestElementUIE : PropertyDrawer
    3. {
    4.     public override VisualElement CreatePropertyGUI(SerializedProperty property)
    5.     {
    6.         var container = new VisualElement();
    7.         var showIntField = new PropertyField(property.FindPropertyRelative("ShowInt"));
    8.         var intField = new PropertyField(property.FindPropertyRelative("Int"));
    9.         container.Add(showIntField);
    10.         showIntField.RegisterCallback<ChangeEvent<bool>>(b => OnShowChange(b.newValue));
    11.         OnShowChange(property.FindPropertyRelative("ShowInt").boolValue);
    12.  
    13.         return container;
    14.  
    15.         void OnShowChange(bool isOn)
    16.         {
    17.             if (isOn)
    18.                 container.Add(intField);
    19.             else
    20.                 container.Remove(intField);
    21.         }
    22.     }
    23. }
    So It seems that
    new PropertyField()
    works only in CreatePropertyGUI.

    Different version, fake repaint:
    Code (CSharp):
    1. [CustomPropertyDrawer(typeof(TestElement))]
    2. public class TestElementUIE : PropertyDrawer
    3. {
    4.     public override VisualElement CreatePropertyGUI(SerializedProperty property)
    5.     {
    6.         var container = new VisualElement();
    7.         var showIntProp = property.FindPropertyRelative("ShowInt");
    8.         var showIntField = new PropertyField(showIntProp);
    9.         container.Add(showIntField);
    10.         var intField = new PropertyField(property.FindPropertyRelative("Int"));
    11.         container.Add(intField);
    12.  
    13.         showIntField.RegisterCallback<ChangeEvent<bool>>(b => Repaint());
    14.  
    15.         return container;
    16.  
    17.         void Repaint()
    18.         {
    19.             intField.style.display = showIntProp.boolValue ?
    20.                 new StyleEnum<DisplayStyle>(DisplayStyle.Flex) :
    21.                 new StyleEnum<DisplayStyle>(DisplayStyle.None);
    22.         }
    23.     }
    24. }
    I wish something like this would work:
    Code (CSharp):
    1. void Repaint()
    2. {
    3.    container = CreatePropertyGUI(property);
    4. }
     
    Last edited: Sep 27, 2019
  6. uMathieu

    uMathieu

    Unity Technologies

    Joined:
    Jun 6, 2017
    Posts:
    398
    PropertyField needs to be bound to a SerializedObject to show something. When creating a visualTree from CreateinspectorGUI(), the editor automatically binds the created tree to the inspected serializedObject. Any element added afterwards will need to be bound manually by calling Bind(serializedObject)

    In your example, I would create both fields during CreateInspectorGUI(), then toggle their visibility with style.display = Display.None/Flex.
     
  7. Catsoft-Studios

    Catsoft-Studios

    Joined:
    Jan 15, 2011
    Posts:
    703
    @uMathieu I've been having a hard time trying to replicate your example using a Property Drawer.

    Taking @GalaadMoutoz 's class example, you can't use a custom editor because the BuildingSetElement class doesn't inherit from Unity.Object. So, the solution is to use a Property Drawer, which I did modifying the attribute ([CustomPropertyDrawer]), inherited class (PropertyDrawer) and overriding the init method (CreatePropertyGUI). The rest is exactly the same.

    Code (CSharp):
    1. [CustomPropertyDrawer(typeof(BuildingSetElement))]
    2. public class BuildingSetElement_PropDrawer : PropertyDrawer
    3. {
    4.     private SerializedProperty prefabsProperty;
    5.  
    6.     public override VisualElement CreatePropertyGUI(SerializedProperty property)
    7.     {
    8.         var root = new VisualElement();
    9.         root.Add(new Label("BuildingSetElement"));
    10.  
    11.         prefabsProperty = property; //serializedObject.FindProperty("prefabs");
    12.  
    13.         var arrayElement = new ArrayInspectorElement(prefabsProperty, (p, i) => MakePrefabItem(p, i));
    14.         root.Add(arrayElement);
    15.  
    16.         // Since arrayElement will be bound, changing the value should resize the array directly
    17.         var add10Button = new Button(() => AddEntries(arrayElement, 10));
    18.         add10Button.text = "Add 10 elements";
    19.         root.Add(add10Button);
    20.         return root;
    21.     }
    22.  
    23.     VisualElement MakePrefabItem(string propertyPath, int index)
    24.     {
    25.         var box = new VisualElement();
    26.         var pf = new PropertyField(null, $"Prefab {index + 1}");
    27.         pf.bindingPath = propertyPath;
    28.  
    29.         // This should belong in uss
    30.         box.style.flexDirection = FlexDirection.Row;
    31.         pf.style.flexGrow = 1;
    32.  
    33.         var removeButton = new Button(() => {
    34.             prefabsProperty.DeleteArrayElementAtIndex(index);
    35.             prefabsProperty.serializedObject.ApplyModifiedProperties();
    36.         });
    37.  
    38.         removeButton.text = "-";
    39.  
    40.         box.Add(pf);
    41.         box.Add(removeButton);
    42.         return box;
    43.     }
    44.  
    45.     void AddEntries(ArrayInspectorElement element, int count)
    46.     {
    47.         //we do this async to bypass event dispatch issues
    48.         element.schedule.Execute(() => element.value += count);
    49.     }
    50. }
    51.  
    52. public class ArrayInspectorElement : BindableElement, INotifyValueChanged<int>
    53. {
    54.     private readonly SerializedObject boundObject;
    55.     private readonly string m_ArrayPropertyPath;
    56.  
    57.     public Func<string, int, VisualElement> makeItem { get; set; }
    58.  
    59.     public override VisualElement contentContainer => m_Container;
    60.  
    61.     private readonly VisualElement m_Container;
    62.  
    63.     public ArrayInspectorElement(SerializedProperty arrayProperty, Func<string, int, VisualElement> makeItem)
    64.     {
    65.         var header = new VisualElement();
    66.  
    67.         header.Add(new Label(arrayProperty.displayName));
    68.  
    69.         var addButton = new Button(() =>
    70.         {
    71.             arrayProperty.InsertArrayElementAtIndex(0);
    72.             arrayProperty.serializedObject.ApplyModifiedProperties();
    73.         });
    74.         addButton.text = "+";
    75.         header.Add(addButton);
    76.  
    77.         // This belongs in uss
    78.         header.style.flexDirection = FlexDirection.Row;
    79.         header.style.justifyContent = Justify.SpaceBetween;
    80.  
    81.  
    82.         //We use a content container so that array size = child count    
    83.         // And the child management becomes easier
    84.         m_Container = new VisualElement() { name = "array-contents" };
    85.         this.hierarchy.Add(header);
    86.         this.hierarchy.Add(m_Container);
    87.  
    88.         m_ArrayPropertyPath = arrayProperty.propertyPath;
    89.         boundObject = arrayProperty.serializedObject;
    90.         this.makeItem = makeItem;
    91.  
    92.         var property = arrayProperty.Copy();
    93.         var endProperty = property.GetEndProperty();
    94.  
    95.         //We prefill the container since we know we will need this
    96.         property.NextVisible(true); // Expand the first child.
    97.         do
    98.         {
    99.             if (SerializedProperty.EqualContents(property, endProperty))
    100.                 break;
    101.             if (property.propertyType == SerializedPropertyType.ArraySize)
    102.             {
    103.                 arraySize = property.intValue;
    104.                 bindingPath = property.propertyPath;
    105.                 break;
    106.             }
    107.         }
    108.         while (property.NextVisible(false)); // Never expand children.
    109.  
    110.         UpdateCreatedItems();
    111.         //we assume we don't need to Bind here
    112.     }
    113.  
    114.     VisualElement AddItem(string propertyPath, int index)
    115.     {
    116.         VisualElement child;
    117.         if (makeItem != null)
    118.         {
    119.             child = makeItem(propertyPath, index);
    120.         }
    121.         else
    122.         {
    123.             var pf = new PropertyField();
    124.             pf.bindingPath = propertyPath;
    125.             child = pf;
    126.         }
    127.  
    128.         Add(child);
    129.         return child;
    130.     }
    131.  
    132.     bool UpdateCreatedItems()
    133.     {
    134.         int currentSize = childCount;
    135.  
    136.         int targetSize = this.arraySize;
    137.  
    138.         if (targetSize < currentSize)
    139.         {
    140.             for (int i = currentSize - 1; i >= targetSize; --i)
    141.             {
    142.                 RemoveAt(i);
    143.             }
    144.         }
    145.         else if (targetSize > currentSize)
    146.         {
    147.             for (int i = currentSize; i < targetSize; ++i)
    148.             {
    149.                 AddItem($"{m_ArrayPropertyPath}.Array.data[{i}]", i);
    150.             }
    151.  
    152.             return true; //we created new Items
    153.         }
    154.  
    155.         return false;
    156.     }
    157.  
    158.     private int arraySize = 0;
    159.     public void SetValueWithoutNotify(int newSize)
    160.     {
    161.         this.arraySize = newSize;
    162.  
    163.         if (UpdateCreatedItems())
    164.         {
    165.             //We rebind the array
    166.             this.Bind(boundObject);
    167.         }
    168.     }
    169.  
    170.     public int value
    171.     {
    172.         get => arraySize;
    173.         set
    174.         {
    175.             if (arraySize == value) return;
    176.  
    177.             if (panel != null)
    178.             {
    179.                 using (ChangeEvent<int> evt = ChangeEvent<int>.GetPooled(arraySize, value))
    180.                 {
    181.                     evt.target = this;
    182.  
    183.                     // The order is important here: we want to update the value, then send the event,
    184.                     // so the binding writes and updates the serialized object
    185.                     arraySize = value;
    186.                     SendEvent(evt);
    187.  
    188.                     //Then we remove or create + bind the needed items
    189.                     SetValueWithoutNotify(value);
    190.                 }
    191.             }
    192.             else
    193.             {
    194.                 SetValueWithoutNotify(value);
    195.             }
    196.         }
    197.     }
    198. }
    However, this doesn't work and throws a "Retrieving array element but no array was provided" error when clicking on the Plus and Minus buttons. Adding 10 elements, adds 10 elements without any content.

    Is there something I'm missing?
     
  8. Catsoft-Studios

    Catsoft-Studios

    Joined:
    Jan 15, 2011
    Posts:
    703
    Oops! My bad. I got the wrong SerializedProperty. Line 11 should be:
    prefabsProperty = property.FindPropertyRelative("prefabs");
     
  9. Quasimodem

    Quasimodem

    Joined:
    May 21, 2013
    Posts:
    52
    I gotta call it like I see it and say that the answers provided by Unity here are really quite poor. uDamian's post contains a critical link that is now dead and he glosses over how to "bind to the arraySize" itself (real example below). uMathieu's code has some agonizing typos in it and his solution is overly burdensome for the type of problem it needs to solve.

    I burned half a day trying to implement their solutions and eventually gave up due to some unknown behavior preventing my layouts from re-rendering after a Clear().

    Anybody else struggling with this issue within a custom inspector and looking for a very quick and dirty workaround, I suggest just resetting your selecting and letting Unity rebuild the whole layout from the top.
    Code (CSharp):
    1. var sizeProperty = new PropertyField(serializedObject.FindProperty("m_rocks.Array.size"));
    2. sizeProperty.RegisterCallback<ChangeEvent<int>>((e) =>
    3. {
    4.      Selection.activeObject = null;
    5.      EditorApplication.delayCall += OnDelayedCall;
    6. });
    7.  
    8. private void OnDelayedCall()
    9. {
    10.      EditorApplication.update -= OnDelayedCall;
    11.      Selection.activeObject = target;
    12. }
     
    AdamBebko and a436t4ataf like this.
  10. MostHated

    MostHated

    Joined:
    Nov 29, 2015
    Posts:
    1,235
    I have also noticed now that for some reason, items within a foldout get rendered twice in 2020+, which did not in 2019 when using the solutions earlier in the post and having not changed any code between them. I have not been able to narrow down what is causing it yet.
     
  11. uMathieu

    uMathieu

    Unity Technologies

    Joined:
    Jun 6, 2017
    Posts:
    398
    Can you go through Help -> Report a Bug... and provide repro steps? It will be the best way to get that fixed quickly
     
    MostHated likes this.
  12. MostHated

    MostHated

    Joined:
    Nov 29, 2015
    Posts:
    1,235
    I should have some time this evening to put something together. I will see what I can do.
     
    uMathieu likes this.
  13. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    Came here via Google because: Editor.Repaint() seems to do literally nothing when using UIToolkit/UIElements? (this was the most relevant page I found).

    The docs still say:

    "Redraw any inspectors that shows this editor."

    ...but this just ... doesn't happen. UIT goes "ha ha! I will completely ignore you!". Is there an official UIT method for "force this inspector to redraw" ?

    In most cases it's relatively easy to workaround the UIT bugs in repainting and force a repaint, but custom inspectors ... I can't see how to force one cleanly, since the API call for it apparently does nothing (doesn't even get invoked).
     
    mike_kon and kevinarbor like this.
  14. AdamBebko

    AdamBebko

    Joined:
    Apr 8, 2016
    Posts:
    168
    Anybody else struggling with this issue within a custom inspector and looking for a very quick and dirty workaround, I suggest just resetting your selecting and letting Unity rebuild the whole layout from the top.
    Code (CSharp):
    1. var sizeProperty = new PropertyField(serializedObject.FindProperty("m_rocks.Array.size"));
    2. sizeProperty.RegisterCallback<ChangeEvent<int>>((e) =>
    3. {
    4.      Selection.activeObject = null;
    5.      EditorApplication.delayCall += OnDelayedCall;
    6. });
    7.  
    8. private void OnDelayedCall()
    9. {
    10.      EditorApplication.update -= OnDelayedCall;
    11.      Selection.activeObject = target;
    12. }
    [/QUOTE]

    Wow thanks.. Can't believe this is the solution, but there you have it.
     
  15. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    I just did the "array size invisible listener" hack on top of this thread plus the same idea for each array element as well to finally get a pseudo-RegisterValueChangeCallback that works sufficiently for PropertyField drawing serialized array. For example I want it so clicking an element and press delete to null the slot should also trigger a callback, not just when resizing the array.

    Here is how it works in the debugger, the "#members" is PropertyField drawing serialized array of assets. The thing below redraws when PropertyField sends change event. INVISIBLE-LISTENER-SIZE is permanently added, while others update when size changes.

    licecap.gif

    Here's the utility class in case anyone want it. Feel free to change namespace or do whatever you want.

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using UnityEditor;
    4. using UnityEditor.UIElements;
    5. using UnityEngine.UIElements;
    6.  
    7. namespace E7.Audito.Editor
    8. {
    9.     /// <summary>
    10.     /// https://forum.unity.com/threads/propertydrawer-with-uielements-changes-in-array-dont-refresh-inspector.747467/
    11.     /// </summary>
    12.     internal static class ArrayPropertyFieldUtility
    13.     {
    14.         internal delegate void OnResize(int newSize);
    15.  
    16.         internal delegate void OnElementChanged(int changedIndex, SerializedProperty newElement);
    17.  
    18.         /// <summary>
    19.         /// Detects size change and property change of each element.
    20.         /// </summary>
    21.         /// <param name="listenerParent">Spawn invisible listeners to this parent.</param>
    22.         /// <param name="listenerChildren">Allocates a list to remember invisible listeners.</param>
    23.         /// <param name="pf">Visual element drawing serialized array that you want to register callback.</param>
    24.         /// <param name="sp">The serialized array which is bound to <paramref name="pf"/>.</param>
    25.         /// <param name="onAnyChange">Called on both array size change and array element change.</param>
    26.         internal static void RegisterArrayChangeCallback(
    27.             VisualElement listenerParent,
    28.             IList<VisualElement> listenerChildren,
    29.             PropertyField pf,
    30.             SerializedProperty sp,
    31.             Action onAnyChange) =>
    32.             RegisterArrayChangeCallback(listenerParent, listenerChildren, pf, sp, size => onAnyChange(),
    33.                 (index, element) => onAnyChange());
    34.  
    35.         /// <summary>
    36.         /// Detects size change and property change of each element.
    37.         /// </summary>
    38.         /// <param name="listenerParent">Spawn invisible listeners to this parent.</param>
    39.         /// <param name="listenerChildren">Allocates a list to remember invisible listeners.</param>
    40.         /// <param name="pf">Visual element drawing serialized array that you want to register callback.</param>
    41.         /// <param name="sp">The serialized array which is bound to <paramref name="pf"/>.</param>
    42.         /// <param name="onResize">Call when array size changed.</param>
    43.         /// <param name="onElementChanged">Call when array element changed.</param>
    44.         internal static void RegisterArrayChangeCallback(
    45.             VisualElement listenerParent,
    46.             IList<VisualElement> listenerChildren,
    47.             PropertyField pf,
    48.             SerializedProperty sp,
    49.             OnResize onResize,
    50.             OnElementChanged onElementChanged)
    51.         {
    52.             if (!sp.isArray)
    53.             {
    54.                 throw new Exception("Property is not serializing an array.");
    55.             }
    56.  
    57.             var basePath = pf.bindingPath;
    58.             var arraySizePath = basePath + ".size";
    59.  
    60.             var invisibleArraySizeListener = new PropertyField
    61.             {
    62.                 name = "INVISIBLE-LISTENER-SIZE",
    63.                 bindingPath = arraySizePath,
    64.                 style =
    65.                 {
    66.                     display = DisplayStyle.None
    67.                 }
    68.             };
    69.             // Array size listener is permanently added to the parent.
    70.             // Element listener gets added and removed depending on the current size.
    71.             invisibleArraySizeListener.RegisterValueChangeCallback(evt =>
    72.             {
    73.                 var newSize = evt.changedProperty.intValue;
    74.                 onResize(newSize);
    75.                 // Automatically increase/decrease the invisible listeners.
    76.                 // Size changed is triggered for sure on the first render?
    77.                 EnsureListenersToSize(newSize, listenerParent, listenerChildren, sp, onElementChanged);
    78.             });
    79.             invisibleArraySizeListener.Bind(sp.serializedObject);
    80.             listenerParent.Add(invisibleArraySizeListener);
    81.  
    82.             static void EnsureListenersToSize(
    83.                 int maxSize,
    84.                 VisualElement listenerParent,
    85.                 IList<VisualElement> current,
    86.                 SerializedProperty sp,
    87.                 OnElementChanged onElementChanged)
    88.             {
    89.                 if (current.Count == maxSize)
    90.                 {
    91.                     return;
    92.                 }
    93.  
    94.                 if (current.Count > maxSize)
    95.                 {
    96.                     var reduce = current.Count - maxSize;
    97.                     for (var i = 0; i < reduce; i++)
    98.                     {
    99.                         current[current.Count - 1].RemoveFromHierarchy();
    100.                         current.RemoveAt(current.Count - 1);
    101.                     }
    102.                 }
    103.                 else
    104.                 {
    105.                     var increase = maxSize - current.Count;
    106.                     var startIndex = current.Count;
    107.                     for (var i = 0; i < increase; i++)
    108.                     {
    109.                         var prop = sp.GetArrayElementAtIndex(i);
    110.                         var invisibleElementListener = new PropertyField(prop)
    111.                         {
    112.                             name = $"INVISIBLE-LISTENER-{startIndex + i}",
    113.                             style =
    114.                             {
    115.                                 display = DisplayStyle.None
    116.                             }
    117.                         };
    118.                         var i1 = i;
    119.                         invisibleElementListener.RegisterValueChangeCallback(evt =>
    120.                         {
    121.                             var newProp = evt.changedProperty;
    122.                             onElementChanged(i1, newProp);
    123.                         });
    124.                         invisibleElementListener.Bind(sp.serializedObject);
    125.                         listenerParent.Add(invisibleElementListener);
    126.                         current.Add(invisibleElementListener);
    127.                     }
    128.                 }
    129.             }
    130.         }
    131.     }
    132. }
     
    dsfgddsfgdsgd and chadfranklin47 like this.
  16. chadfranklin47

    chadfranklin47

    Joined:
    Aug 11, 2015
    Posts:
    229
    @5argon Thanks for your work on this. I can't seem to get it working as you have though, would it be possible to provide some example code using the method? I am fairly new to UIElements.
     
  17. fbmd

    fbmd

    Joined:
    Dec 4, 2016
    Posts:
    16
    Wow thanks.. Can't believe this is the solution, but there you have it.[/QUOTE]

    Wait... the target variable is not defined. Where does this come from? This does not seem to work in Unity 2022.2
     
  18. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    2 years later ran into this thread again (while trying to make UIToolkit do what it's supposed to but doesn't). Since my post in 2021 ... I did report this as a bug, but it got rejected/closed as wontfix. I'm retesting with current LTS (2021+) now, but this never got a fix throughout 2020 LTS as far as I know.