Assertion failed on expression: 'IsInSyncWithParentSerializedObject()'

Hello guys,

I am struggling a lot to fix this assertion and I am not able to find the problem. Let’s see if you guys can help me with it. I am leaving below the CustomPropertyDrawer code that I am using:

    [CustomPropertyDrawer(typeof(SubclassSelectorAttribute))]
    public class SubclassPropertyDrawer : PropertyDrawer
    {
        private VisualElement _container;
        private SerializedProperty _property;
        private Dictionary<string, Type> _keyToType;
        private List<PropertyField> _propertyFields;
        private static int _index = 0;

        public override VisualElement CreatePropertyGUI(SerializedProperty property)
        {
            _container = new VisualElement();
            _property = property;
            _keyToType = new Dictionary<string, Type>();
            _propertyFields = new List<PropertyField>();
            Debug.Log($"{_index++}: Property {_property.displayName}");

            var baseType = fieldInfo.FieldType.IsArray ? fieldInfo.FieldType.GetElementType() : fieldInfo.FieldType;

            var assemblies = AppDomain.CurrentDomain.GetAssemblies();

            var derivedClasses = new List<string>();

            int typeIdx = 0;
            int typeCounter = 0;
            foreach (var assembly in assemblies)
            {
                if (assembly.GetName().Name == "Assembly-CSharp")
                {
                    var types = assembly.GetTypes();
                    foreach (var type in types)
                    {
                        if (type.IsSubclassOf(baseType))
                        {
                            derivedClasses.Add(type.Name);
                            _keyToType[type.Name] = type;

                            if (property.managedReferenceValue?.GetType() == type)
                            {
                                typeIdx = typeCounter;
                            }

                            typeCounter++;
                        }
                    }
                }
            }

            var dropdown = new DropdownField("Skill Type", derivedClasses, typeIdx);
            dropdown.RegisterValueChangedCallback(v => OnDropdownValueChanged(v.newValue));
            _container.Add(dropdown);

           Show(derivedClasses[typeIdx]);

            return _container;
        }

        private void OnDropdownValueChanged(string value)
        {
            ResetPropertyFields();
            Show(value);
            _container.Bind(_property.serializedObject);
        }

        private void Show(string value)
        {
            if (_property.managedReferenceValue?.GetType() != _keyToType[value] || _property.managedReferenceValue == null)
            {
                var subclassInstance = Activator.CreateInstance(_keyToType[value]);
                _property.managedReferenceValue = subclassInstance;
            }

            var propertiesIterator = _property.Copy().GetEnumerator();

            while (propertiesIterator.MoveNext())
            {
                if (propertiesIterator.Current is SerializedProperty childSerializedProperty)
                {
                    if (childSerializedProperty.depth <= _property.depth + 1)
                    {
                        var propertyField = new PropertyField(childSerializedProperty);
                        _container.Add(propertyField);
                        _propertyFields.Add(propertyField);
                    }
                }
            }

            _property.serializedObject.ApplyModifiedProperties();
        }

        private void ResetPropertyFields()
        {
            if (_propertyFields.Count > 0)
            {
                foreach (var propertyField in _propertyFields)
                {
                    _container.Remove(propertyField);
                }
                _propertyFields.Clear();
            }
        }
    }

I am basically trying to allow derived classes to be selectable on inspector. Everything goes fine during initialization and the assertion does not pop up, as you can see in the pictures below:
9865944--1421694--upload_2024-5-31_16-53-22.png

But as soon as I add a new element to the array, the assertion pops up:
9865944--1421700--upload_2024-5-31_16-54-16.png

Could you please point me out what I am doing wrong? I guess I may be misunderstanding something about how the GUI is drawn or the serialized objects are updated… but I am not able to catch the bug.

Thank you very much in advance!

The error message does not have a callstack?

If not, just comment out parts of the code until the error goes away, then you know where it’s coming from.

My hunch is that the issue is either the _property field because the property passed into CreatePropertyGUI may be a transient object, meaning you ought to use it within that method and not hold on to it.

Or it’s the Show method. It’s pretty unclear what this does based on the names of things all being very generic. I’m suspicious of the Copy enumerator (see below).

You can replace this entire block of code:

with TypeCache.GetTypesDerivedFrom. It’s a lot faster and one line of code.

And unless there’s something I’m not seeing here:

I’m pretty sure that’s the same as:

foreach(var current in _property.GetEnumerator())

If SerializedProperty actually implements IEnumerator you could even omit the GetEnumerator().

Hello!

Firstly, thank you very much for your reply.

Unfortunately, the error does not show too much info :frowning:
9866439--1421811--upload_2024-5-31_20-59-18.png

This makes a lot of sense because I tried to initialize the array with some elements and it is working perfectly fine, so using the property outside the scope of CreatePropertyGUI method may be the problem… I will try to investigate this further.

This works perfectly fine! Thank you for the tip :slight_smile:

But this one does not work.

I will try to do a workaround in order to avoid using the property outside the main method since this seems to be the main root cause. Thank very much for your advices once again!

I am back again.

I have just realized about what I was saying in my previous message and it does not make any sense… in fact, I am calling the Show method inside CreatePropertyGUI… and as I said, it is working correctly on initialization.

What is certain is that there is something I am doing in the initialization that is not happening the same way after the CustomPropertyDrawer is created.

And one more thing I just noticed now, when adding a new element to the array, the number of times the assertion is logged is proportional to the number of properties per element (so for example if each element has 2 properties, and I already have 3 elements, when adding the fourth element, the assertion log appears 6 times). Somehow the problem is occurring with the existing elements.

Any other idea? :frowning:

Adding new info:

I just found a strange behaviour that may be giving some clue to my current issue. When I have the following array configuration in the Editor:
9867114--1422075--upload_2024-6-1_9-0-4.png
If I modify the Element0 dropdown, the Element2 fields are modified:
9867114--1422078--upload_2024-6-1_9-1-6.png

So it looks like something is wrong in the code I posted in my first comment, but I am not sure how this is happening because _property should be unique per SubclassPropertyDrawer instance, right?

I hope this helps someone else in order to find a solution because this is driving me crazy…

Hello,

It looks like when using lists or arrays, the property drawer class fields are shared across all the elements as well, and I am getting errors when deleting visual elements from Element 0 property drawer since the list where I store the elements is containing an Element 1 visual element. Why is this happening? Could it be related to my previous problem? Is it a bug or is this the expected behavior?

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

namespace Editor
{
    [CustomPropertyDrawer(typeof(SubclassSelectorAttribute))]
    public class SubclassPropertyDrawer : PropertyDrawer
    {
        private Dictionary<string, Type> _typeNameToType;
        //private List<VisualElement> changeableElements;

        private List<string> _elements = new List<string>();

        public override VisualElement CreatePropertyGUI(SerializedProperty property)
        {
            _typeNameToType = new Dictionary<string, Type>();

            var baseType = fieldInfo.FieldType.IsArray ? fieldInfo.FieldType.GetElementType() : fieldInfo.FieldType;

            var derivedTypesNames = new List<string>();
            var derivedTypes = TypeCache.GetTypesDerivedFrom(baseType);
            foreach (var derivedType in derivedTypes)
            {
                derivedTypesNames.Add(derivedType.Name);
                _typeNameToType[derivedType.Name] = derivedType;
            }

            var rootElement = new VisualElement();
            var changeableElements = new List<VisualElement>();
            BuildUI(rootElement, property, derivedTypesNames, changeableElements);

            return rootElement;
        }

        private void BuildUI(VisualElement rootElement, SerializedProperty property, List<string> derivedTypes, List<VisualElement> changeableElements)
        {
            int dropDownInitValue = GetMananagedReferenceValueNameIdx(property, derivedTypes);

            var dropdown = new DropdownField("Derived Classes", derivedTypes, dropDownInitValue);
            dropdown.RegisterValueChangedCallback((evt) => OnDropDownValueChanges(rootElement, property, evt, changeableElements));
            rootElement.Add(dropdown);

            DisplayDerivedClassProperties(rootElement, property, derivedTypes[dropDownInitValue], changeableElements);
        }

        private int GetMananagedReferenceValueNameIdx(SerializedProperty property, List<string> derivedTypes)
        {
            if (!string.IsNullOrEmpty(property.managedReferenceFullTypename))
            {
                var name = property.managedReferenceFullTypename.Split('.').Last();
                return derivedTypes.IndexOf(name);
            }

            return 0;
        }

        private void DisplayDerivedClassProperties(VisualElement rootElement, SerializedProperty property, string value, List<VisualElement> changeableElements)
        {
            if (property.managedReferenceValue == null || property.managedReferenceValue.GetType() != _typeNameToType[value])
            {
                var derivedClassInstance = Activator.CreateInstance(_typeNameToType[value]);
                property.managedReferenceValue = derivedClassInstance;
                property.serializedObject.ApplyModifiedProperties();
            }

            var foldout = new Foldout();
            foldout.text = property.displayName;
            _elements.Add(property.displayName);

            if (property.hasChildren)
            {
                var enumerator = property.Copy().GetEnumerator();

                while (enumerator.MoveNext())
                {
                    var childProperty = enumerator.Current as SerializedProperty;
                    var propertyField = new PropertyField(childProperty, childProperty.displayName);
                    propertyField.BindProperty(childProperty);
                    foldout.Add(propertyField);
                }
            }

            rootElement.Add(foldout);
            changeableElements.Add(foldout);
        }

        private void OnDropDownValueChanges(VisualElement rootElement, SerializedProperty property, ChangeEvent<string> evt, List<VisualElement> changeableElements)
        {
            ResetDynamicVisualElements(rootElement, changeableElements);
            DisplayDerivedClassProperties(rootElement, property, evt.newValue, changeableElements);
            string text = string.Empty;
            foreach (var element in _elements)
            {
                text += $"{element}, ";
            }
            Debug.Log(text);
        }

        private void ResetDynamicVisualElements(VisualElement rootElement, List<VisualElement> changeableElements)
        {
            foreach (var element in changeableElements)
            {
                rootElement.Remove(element);
            }
            changeableElements.Clear();
        }
    }
}

I am adding the modified code in order to make it more understandable.

New things I have noticed but I do not really understand:

  1. Commenting this line since all the array elements were sharing it
    9882273--1425690--upload_2024-6-10_13-40-6.png
    Why is this happening? As I said in my previous comment, is this supposed to be the expected behavior? Due to this variable, I was facing problems in the ResetDynamicVisualElements method since it was trying to remove visual elements from a different array element property… finally I had to share this list across all the methods as an argument to avoid this problem.

  2. Modifying a string property and then adding an element to the array changes the array element label:
    9882273--1425696--upload_2024-6-10_13-42-37.png
    9882273--1425699--upload_2024-6-10_13-42-52.png
    Another mystery I cannot understand. I am binding the PropertyField to the Name SerializedProperty… how is this changing the element label?

3.Still facing the initial problem of the IsInSyncWithParentSerializedObject assertion.
9882273--1425705--upload_2024-6-10_13-46-3.png


I have just realized that the number of times this assertion is logged is proportional to the number of serialized properties of all the already existent array elements. In this example I initially had 2 array elements (4 properties), so when adding a new array element, the assertion logs 4 times.

I hope anyway can help me with this because I spent a lot of time into this and I cannot even realize where the root cause is.

Thank you very much in advance!