The RegisterValueChanged problem

There’s been threads about this before, so I’ll give the short version;

If you call RegisterValueChangeCallback on a PropertyField inside an Editor’s CreateInspectorGUI, it’ll get invoked immediately. This is because PropertyFields raises a SerializedPropertyChangedEvent when bound, which happens after CreateInspectorGUI.

This is incredibly annoying. I want a callback when the inspector has been interacted with by the user! The current behaviour leads two very major problems:

  • Slowdown from selecting objects if there’s many callbacks, or they’re slow, as every single one gets invoked at the same time
  • Potential dirtying of the scene if I update other objects as a side-effect of values changing

So this is a very bad! Having things happen as a side-effect of editing an inspector is a very standard, bread-and-butter kind of Unity editor scripting thing, and having this big of a drawback in the middle of the system really sours my day whenever I run into it.

All the potential workarounds - like waiting a bit before we bind the property - is broken as the time it takes to populate an inspector varies by how large it is, and we can interact with it before it’s done populating.

The correct decision here is to just fix your code, and not have the same callback for “the user changed the value in a property bound to this field” and “which property is bound to this field changed”. Any chance that will happen anytime soon?

1 Like

This topic has caused a bit of back-and-forth internally.
Here’s the main issue:

The ChangeEvent is supposed to show that a field has been changed, without caring whether it’s from a UI interaction, script, binding, or anything else. This idea works until you try using it and expect change events when a serialized value is updated. At that point, you have no way of knowing if the change is from a UI interaction or the SerializedProperty syncing to the field. Knowing the context is key here so we can filter out the changes properly.

Initially, bindings only set the value on fields, and ChangedEvents were dispatched as a result. However, this led to a scenario where no event was sent if a SerializedProperty happened to have the same value as the existing field. This behavior was too unreliable, so the binding system was updated to send change events on bind, even when the values remained the same.

We are still evaluating the best approach to solve this problem. I have shared several suggestions internally earlier this year, and they are still being debated.

Making changes of this nature is complex, as we risk altering behavior that one user might find inconvenient while another depends on it. We manage this by striving to provide solutions that satisfy both user preferences.

You may already be aware of this but TrackPropertyValue is a good way to avoid these change event issues as it will only send an event when the SerializedProperty has been changed. I myself used this workaround earlier in the year when updating the UI Builder inspector. I found it to be more reliable than using change events.

That said, we recognize that dealing with the binding change event has been challenging for some users, as well as for us. Rest assured, we are actively working on finding a more reliable solution.

3 Likes

Hi again. Just wanted to let you know that TrackPropertyValue helped a lot, it covers a lot of ground.

I just found a comment I had left by a workaround for this problem, which sheds some light on when RegisterValueChangeCallback is a better fit. Here’s a thing we do a lot in OnInspectorGUI:

public override void OnInspectorGUI()
{
    EditorGUI.BeginChangeCheck();
    base.OnInspectorGUI();
    if (EditorGUI.EndChangeCheck())
        RebuildTargetObjects();
}

Since SerializedPropertyChangeEvent bubbles up, this should be achievable in UITK with this:

public override VisualElement CreateInspectorGUI()
{
    var root = new VisualElement();
    InspectorElement.FillDefaultInspector(root, serializedObject, this);

    root.RegisterCallback<SerializedPropertyChangeEvent>(RebuildTargetObjects);

    return root;
}

But due to that happening on Bind, I end up doing TrackPropertyValue instead, like you suggested. It looks like this:

public override VisualElement CreateInspectorGUI()
{
    var root = new VisualElement();
    InspectorElement.FillDefaultInspector(root, serializedObject, this);

    var iterator = serializedObject.GetIterator();
    for (var enterChildren = true; iterator.NextVisible(enterChildren); enterChildren = false)
        root.TrackPropertyValue(iterator, RebuildTargetObjects);

    return root;
}

So that’s not the end of the world, tbh! I’d love it if I could just do the RegisterCallback and be done with it, either with SerializedPropertyChangeEvent, or some other event, but I can live with this.

Any chance you update the SerializedPropertyChangeEvent docs to say when exactly it’s fired? Right now you at the very least have a documentation bug.

1 Like

Yeah sure. Ill ask the docs team to take a look.