Using TrackPropertyValue and Untracking

Hi there!

I’ll try to explain my issue as simple as possible. I’m working on an Editor UI tool that creates pretty complex controls for managed references. For example, the custom MyControl:

public class MyControl : VisualElement
{
    public MyControl(SerializedProperty property)
    {
        // bunch of initialization
        this.TrackPropertyValue(property, this.OnUpdateTrackedProperty);
    }

    private void OnUpdateTrackedProperty(SerializedProperty serializedProperty)
    {
        // Refresh UI because something has changed, like an Undo
    }
}

However because I have lots of custom controls, every time the Inspector needs to be refreshed, it takes a while to create all the new visual elements.

The documentation also recommends pooling for reuse visual elements, using the AttachToPanelEvent and DetachFromPanelEvent.

However if I do this, visual elements that have been recycled will call the OnUpdateTrackedProperty on both when the previous serialized property changes, and the new one (and this is exacerbated the more you reuse pooled elements).

So my question is: Should I avoid using TrackPropertyValue? I’m not sure why there isn’t an UntrackPropertyValue. This would solve the problem, but maybe I’m not understanding how it works?

Speaking about inner workings, is it safe to use TrackPropertyValue, or is it something that hooks into an internal update and constantly checks if the serialized property has changed?

If someone sheds some light on this, I’ll buy the first round of beers!

Hi
It’s safe to use, we do poll during the update but it’s done on a per-object basis, not per TrackPropertyValue.
We check the SerializedObject, it has an internal counter that increments when a change occurs. if it has had a change then we go through the associated tracked properties and compare the cached hash value to the current one to see if its changed, we then send the change events at the end. So the number of tracked properties wont have any impact on performance other than at the point when a change occurs. So if you have perf issues when the change occurs then it may be worth considering reducing the number.

1 Like

Thanks for the quick reply! Perfect, that’s great.

I was taking a look at the code (can be found here). So without having tested it, this is how I should do it for pooled visual elements?

public class MyControl : VisualElement
{
    public MyControl(SerializedProperty property)
    {
        // bunch of initialization
        this.RegisterCallback<AttachToPanelEvent>(this.OnAttachToPanel);
        this.RegisterCallback<DetachFromPanelEvent>(this.OnDetachFromPanel);
    }

    private void OnAttachToPanel(AttachToPanelEvent attachPanel)
    {
        // This means the visual element is new or recycled from the pool
        this.TrackPropertyValue(property, this.OnUpdateTrackedProperty);
    }

    private void OnDetachFromPanel(DetachFromPanelEvent detachEvent)
    {
        this.Unbind(); // stops tracking property changes on OnUpdateTrackedProperty(...)
    }

    private void OnUpdateTrackedProperty(SerializedProperty serializedProperty)
    {
        // Refresh UI because something has changed, like an Undo
    }
}

I’ll test this and circle back with the results in case anyone’s interested.

1 Like

Yep! I confirm that’s the way. In fact, if one doesn’t add the this.Unbind() method, Unity is smart enough to throw an error that the same element can’t keep track of two serialized properties at the same time.

NotSupportedException: An element can track properties on only one serializedObject at a time

Thanks Karl!

1 Like

A quick update on this. The previous code works well, but when using it in a ListView and binding in the bindItem callback, because it’s pooled it throws the following exception:

NotSupportedException: An element can track properties on only one serializedObject at a time

Because there’s no UntrackPropertyValue(...) the visual element will forever be bound to the serialized property assigned until death do us part, and can never track another serialized property.

Note that I’m not sure if this is the expected behavior, I even tried using the Unbind() method and still keeps tracking serialized property changes.

So what I’ve come up is a little tool that creates a new visual element in charge of tracking serialized property changes. Whenever you try to track another serialized property, it automatically destroys the element and creates a new one that does the tracking.

If someone wants to use it, feel free to do so.

using System;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;

public class PropertyChangeTracker : VisualElement
{
    [NonSerialized] private VisualElement m_Tracker;
 
    [NonSerialized] private SerializedProperty m_Property;
    [NonSerialized] private Action<SerializedProperty> m_OnChange;
 
    public PropertyChangeTracker()
    {
        this.RegisterCallback<AttachToPanelEvent>(this.OnAttachToPanel);
        this.RegisterCallback<DetachFromPanelEvent>(this.OnDetachFromPanel);
    }
 
    public PropertyChangeTracker(
        SerializedProperty property,
        Action<SerializedProperty> onChange)
        : this()
    {
        this.TrackProperty(property, onChange);
    }

    public void TrackProperty(SerializedProperty property, Action<SerializedProperty> onChange)
    {
        this.UntrackProperty();

        this.m_Property = property;
        this.m_OnChange = onChange;
    
        this.m_Tracker = new VisualElement();
        this.Add(this.m_Tracker);
    
        this.m_Tracker.TrackPropertyValue(property, onChange);
    }

    public void UntrackProperty()
    {
        if (this.m_Tracker == null) return;
    
        this.m_Tracker.Unbind();
    
        this.Remove(this.m_Tracker);
        this.m_Tracker = null;
    }

    private void OnAttachToPanel(AttachToPanelEvent attachEvent)
    {
        if (this.m_Property == null) return;
        if (this.m_OnChange == null) return;
    
        this.TrackProperty(this.m_Property, this.m_OnChange);
    }

    private void OnDetachFromPanel(DetachFromPanelEvent detachEvent)
    {
        this.UntrackProperty();
    }
}

To use it, you simply create a new instance of PropertyChangeTracker and add it as a child of any visual element you want (doesn’t matter, won’t be shown).

During the binding callback you do:

propertyChangeTracker.TrackProperty(mySerializedProperty, this.OnChangeCallback);

Optionally during the unbinding callback you can also stop tracking changes:

propertyChangeTracker.UntrackProperty();

Note that you don’t need to untrack and track again a new serialized property. Tracking a new one will automatically untrack the previous one.

1 Like

Hi. I think that’s a nice idea. I found a bug that makes using TrackingPropertyValue troublesome. Basically, it keeps tracking after the element is removed. It can easily mean problems for your tool. I posted about this issue in the forum here .

1 Like