Search Unity

Question OnValidate not being called when reflection-acquired property is changed

Discussion in 'Editor & General Support' started by UnkelRambo, Dec 7, 2022.

  1. UnkelRambo

    UnkelRambo

    Joined:
    Oct 26, 2012
    Posts:
    80
    Hi! I've got a bit of a complex situation that I'm probably going to provide inadequate code to fully explain, but here goes...

    I have a custom PropertyDrawer for a tree view of Unreal-like GameplayTags that looks a bit like so:
    upload_2022-12-7_9-23-27.png

    This is built with two classes:
    1. GameplayTagContainerTreeView - A specialized tree view control with tons of customization options. This is used a few different ways, each with their own unique requirements: Drawing the Project Settings page, drawing a property for a single GameplayTag, and drawing a property for a GameplayTagContainer (more later.)
    2. GameplayTagContainerPropertyDrawer - A custom property drawer for displaying the configured TreeView (above) for a GameplayTagContainer property.

    The GameplayTagContainer is a SerializedDictionary that copies dictionary contents to and from two arrays of keys and values (very similar to this: https://github.com/github-for-unity...Editor/GitHub.Unity/SerializableDictionary.cs)

    In GameplayTagContainerPropertyDrawer, I'm retrieving the GameplayTagContainer property from the object via reflection like so:

    Code (CSharp):
    1.        
    2. public static T GetTarget<T>(this SerializedProperty prop)
    3.         {
    4.             var propertyNames = prop.propertyPath.Split('.');
    5.             object target = prop.serializedObject.targetObject;
    6.             var isNextPropertyArrayIndex = false;
    7.             for (var i = 0; i < propertyNames.Length && target != null; ++i)
    8.             {
    9.                 var propName = propertyNames[i];
    10.                 if (propName == "Array")
    11.                 {
    12.                     isNextPropertyArrayIndex = true;
    13.                 }
    14.                 else if (isNextPropertyArrayIndex)
    15.                 {
    16.                     isNextPropertyArrayIndex = false;
    17.                     var arrayIndex = ParseArrayIndex(propName);
    18.                     var targetAsArray = (object[]) target;
    19.                     target = targetAsArray[arrayIndex];
    20.                 }
    21.                 else
    22.                 {
    23.                     target = GetField(target, propName);
    24.                 }
    25.             }
    26.  
    27.             return (T) target;
    28.         }
    29.  
    30. /// in OnGui(), I check if initialization is needed and, if it is, retrieve the GameplayTagContainer like so:
    31.  
    32.             container = property.GetTarget<GameplayTagContainer>();
    33.  
    OK, hopefully this all makes sense so far!

    When the GameplayTagContainerPropertyDrawer is initialized, it sets a delegate in the GameplayTagContainerTreeView to handle when a tag gets toggled to set the underlying tag in the container like so:

    Code (CSharp):
    1.             TreeView.OnToggled += (tag, on) =>
    2.             {
    3.                 Undo.RecordObject(property.serializedObject.targetObject, "Modify Tags");
    4.  
    5.                 // Add the tag
    6.                 if (on)
    7.                     container.PushTag(tag);
    8.                 else
    9.                     container.PopTag(tag);
    10.  
    11.                 // Force the container to reserialize, forcing OnValidate(). This is 100% a dirty, dirty hack ^_^
    12.                 //container.OnBeforeSerialize();
    13.                 var obj = property.serializedObject.targetObject;
    14.                 property.serializedObject.ApplyModifiedProperties();
    15.                 EditorUtility.SetDirty(property.serializedObject.targetObject);
    16.             };
    You can see by the comment, something isn't working as expected ;)

    THE ISSUE:

    Everything works as intended EXCEPT that I have a few ScriptableObjects with GameplayTagContainer properties that, when modified, I need to do some work in OnValidate() which never gets called!

    THE QUESTION:

    Given the setup I have here modifying this class via reflection, is there something I'm missing to force OnValidate() to be called for my scriptable object? Or is there a way to force OnValidate() to be called via some other hackery?

    I was using the hack suggested here:
    https://forum.unity.com/threads/force-custom-propertydrawer-to-trigger-onvalidate.344105/

    BUT since my object is a ScriptableObject and NOT a MonoBehaviour, this doesn't work.

    I've been poking at this off and on for a couple of days and it's one of the few things remaining for me to fix before submitting this to the asset store, so any and all help would be greatly appreciated!

    I'd also like to keep any fix free of client-code intervention (as a nicety for asset store customers), so not forcing client code to inherit from an interface, as an example. I'm hoping I can smack this with a hammer in the TreeView or PropertyDrawer and it "just works" ;)
     
  2. UnkelRambo

    UnkelRambo

    Joined:
    Oct 26, 2012
    Posts:
    80
    All it took was a fresh cup of oolong and 20 minutes of hackery to find a solution, but it's DIRTY...

    I added an editor only property to GameplayTagContainer called "_ForceDirty".
    When a tag is changed, set _ForceDirty to TRUE.
    In OnGui(), just before EditorGUI.EndProperty() I do this:

    Code (CSharp):
    1.             // HACKITY HACK: DON'T TALK BACK
    2.             // This is necessary to force OnValidate() to be called when a container is modified via reflection. It works but holy cow...
    3.             var dirtyProperty = property.FindPropertyRelative("_ForceDirty");
    4.             if (dirtyProperty.boolValue)
    5.                 dirtyProperty.boolValue = false;
    May god have mercy on my soul...

    I'd still LOVE to hear if there's a better solution out there ;)
     
    TheLastHylian likes this.