Kousen
Kousen

Reputation: 23

Assertion failed on expression: 'IsInSyncWithParentSerializedObject() using CustomPropertyDrawer

I am facing a problem when adding elements to a SerializeReference custom class array. For some reason everything works fine but when adding a new element to the array from Editor the following assertion pops out: enter image description here enter image description here

I leave the code below:

namespace Editor
{
    [CustomPropertyDrawer(typeof(BaseClass))]
    public class SubclassPropertyDrawer : PropertyDrawer
    {
        public override VisualElement CreatePropertyGUI(SerializedProperty property)
        {
            Debug.Log($"{property.displayName} managedReferenceValue is {property.managedReferenceValue}");

            var root = new VisualElement();

            if (property.managedReferenceValue == null)
            {
                property.managedReferenceValue = Activator.CreateInstance(typeof(DerivedClass1));
                property.serializedObject.ApplyModifiedProperties();
            }

            var foldout = new Foldout();
            foldout.text = property.displayName;
            var enumerator = property.Copy().GetEnumerator();
            enumerator.MoveNext();
            var childProperty = enumerator.Current as SerializedProperty;
            var propertyField = new PropertyField(childProperty, childProperty.displayName);
            foldout.Add(propertyField);
            root.Add(foldout);

            return root;
        }
    }
}

And the ScriptableObject here:

namespace Test
{
    [CreateAssetMenu(fileName = "Test", menuName = "ScriptableObjects/Test", order = 1)]
    public class TestSO : ScriptableObject
    {
        [SerializeReference]
        public BaseClass[] customClass;
    }
}

Any idea on what I am doing wrong?

Thank you in advance!

I am trying to basically display a Dropdown element that allows me to select between the different derived types of a base class and show its fields.

i would expect this to work properly without any error.

Upvotes: 1

Views: 728

Answers (1)

derHugo
derHugo

Reputation: 90813

Tbh I couldn't reproduce your exact issue regarding the exception.

There are however a couple of other pitfalls I came across while trying ;)

Assuming your BaseClass is not derived from UnityEngine.Object - in which case you shouldn't be using SerializeReference at all.

Main issue with CreatePropertyGUI vs OnGUI is

  • OnGUI is called every frame. This sometimes adds quite some overhead regarding getting properties / initializing states etc. On the other hand it sometimes makes things easier with refreshing and changes
  • CreatePropertyGUI is only called once when the Inspector is loaded + on certain change events (e.g. reordering elements in a list). This caused an issue with forcing the foldout to re-layout after changing the type (see code below).

Then also you probably wanted to include the type selection popup itself. You can use the TypeCache to list them (This could then even be used in a more generic way via a property attribute.)

enter image description here

This is the inspector I came up with

[CustomPropertyDrawer(typeof(BaseClass))]
public class SubclassPropertyDrawer : PropertyDrawer
{
    public override VisualElement CreatePropertyGUI(SerializedProperty property)
    {
        // Get all types derived from BaseClass
        var types = TypeCache.GetTypesDerivedFrom<BaseClass>().OrderBy(t => t.FullName).ToList();
        // Get the current type of the property
        var currentType = property.managedReferenceValue?.GetType();

        var root = new VisualElement();

        var foldout = new Foldout
        {
            text = property.displayName
        };

        // Dropdown to select the type
        var typeSelectionPopup = new PopupField<Type>("Type", types, types.IndexOf(currentType), TypeDisplay, TypeDisplay);
        typeSelectionPopup.RegisterCallback<ChangeEvent<Type>>(ev => OnNewTypeSelected(ev.newValue));

        // If the property is null, set it to the first available type
        if (property.managedReferenceValue == null)
        {
            OnNewTypeSelected(types[0]);
        }

        root.Add(foldout);

        RefreshProperties();

        return root;
        
        string TypeDisplay(Type type)
        {
            // optional: Group types by namespace into submenus
            return type?.FullName == null ? string.Empty : type.FullName.Replace('.', '/');
        }
        
        void OnNewTypeSelected(Type newType)
        {
            var newObject = newType == null ? null : Activator.CreateInstance(newType);
            property.managedReferenceValue = newObject;
            property.serializedObject.ApplyModifiedProperties();
            
            // also need to refresh the property fields - otherwise they will still show the old fields
            RefreshProperties();
        }

        void RefreshProperties()
        {
            foldout.Clear();
            // per default add the type selection popup
            foldout.Add(typeSelectionPopup);

            var iterator = property.Copy().GetEnumerator();
            var childDepth = property.depth + 1;

            while (iterator.MoveNext())
            {
                // only iterate first level children
                // otherwise e.g. for a Vector 2 you would first get the vector itself and then the nested "x" and "y" fields
                // also for arrays/lists this would iterate each item individually
                // we will want to assume these should rather use their very own drawers
                if (iterator.Current is not SerializedProperty childProperty || childProperty.depth != childDepth)
                    continue;
                
                var propertyField = new PropertyField(childProperty);
                // This is required for dynamically created UI items so they are correctly linked to the serializedObject
                // For UI items created in CreatePropertyGUI Unity does this internally
                propertyField.Bind(property.serializedObject);
                foldout.Add(propertyField);
            }

            (iterator as IDisposable)?.Dispose();
        }
    }
}

And here some example types to select from

namespace Tests2
{
    [Serializable]
    public class DerivedClass4 : BaseClass
    {
        public Vector3 someVector3;
    }
    
    [Serializable]
    public class DerivedClass5 : BaseClass
    {
        public Texture2D someTexture;
        public List<int> someList;
    }
}

namespace Tests
{
    [Serializable]
    public class BaseClass
    {
    }

    [Serializable]
    public class DerivedClass1 : BaseClass
    {
        public float someFloat;
    }

    [Serializable]
    public class DerivedClass2 : BaseClass
    {
        public int someInt;
        public string someString;
    }

    public class DerivedClass3 : BaseClass
    {
        [Range(-2, 6)]
        public float someFloatRange;
        public Vector2 someVector2;
    }
}

In general: Have in mind that with this - going by serialized type names - you will run into trouble whenever you rename the types, namespace, or assembly!

Upvotes: 2

Related Questions