Search Unity

Bug RegisterValueChangedCallback had a breaking change in a recent update

Discussion in 'UI Toolkit' started by Aka_ToolBuddy, Jan 11, 2021.

  1. Aka_ToolBuddy

    Aka_ToolBuddy

    Joined:
    Feb 25, 2014
    Posts:
    549
    Hi,

    Here is how I setup one of my toggles:
    Code (CSharp):
    1. Toggle freeFormToggle = contentContainer.Q<Toggle>("FreeFormToggle");
    2. freeFormToggle.value = trackSection.IsFreeForm;
    3. freeFormToggle.BindProperty(serializedTrackSection.FindPropertyRelative(nameof(trackSection.IsFreeForm)));
    4. freeFormToggle.RegisterValueChangedCallback(evt => OnTrackSectionChange());"
    I was using package version 1.0.0 preview 9. Now I upgraded to preview 13.

    When clicking on my toggle, and before the upgrade, the callback was called after the update of the value of the bound property. After the upgrade, the event is called before the update of the property. So my questions are:
    1. Is the official default behaviour from now on?
    2. If yes, is there any easy way implemented in UI Toolkit to use the old behaviour?
    Thanks.
     
    a436t4ataf likes this.
  2. tonycoculuzzi

    tonycoculuzzi

    Joined:
    Jun 2, 2011
    Posts:
    301
    I've noticed something similar. I wasn't sure if it was default behaviour, or if I had broken something unknowingly.
     
  3. Aka_ToolBuddy

    Aka_ToolBuddy

    Joined:
    Feb 25, 2014
    Posts:
    549
    Any input @antoine-unity ?
     
  4. uMathieu

    uMathieu

    Unity Technologies

    Joined:
    Jun 6, 2017
    Posts:
    398
    This is a real usability problem that we need to address.

    For performance and in order to support more complex use-cases, binding operations are now done asynchronously when a VisualElement is not attached to a panel.

    The bindings system uses standard ChangeEvent callbacks to listen to the field value changes and to apply them to the serialized object. This means that if you register before the bindings, you'll get the change notification before the value has been applied to the serialized object. This can be useful when you want to perform some validation on the input value before letting the binding apply it.

    Now that the bindings are performed out-of-scope, this clearly doesn't help your simple use-case.

    So to answer your question:

    1. No, this is the situation now but we'll find a way to address it.
    2. There are ways to workaround the behavior, depending on your context.

    The trick is to ensure the Bind operation gets completed before we register to value changes.

    * If you're in an EditorWindow, we can do it by making sure the elements are added to the panel's hierarchy before binding. Add your elements inside the CreateGUI() method instead of OnEnable(). This makes sure the editorWindow.rootVisualElement is added to a panel when you add your own elements to it. In order: Add the fields to the hierarchy, bind, then perform your RegisterValueChangeCallbacks
    * In an InspectorWindow, the workaround will feel way more hackish. Since the CreateInspectorGUI() has no context, and it will be bound by the InspectorWindow later on, you'll need to wait for a frame before registering your callbacks. This changes your code something like:

    Code (CSharp):
    1. freeFormToggle.schedule.Execute( () => freeFormToggle.RegisterValueChangedCallback(evt => OnTrackSectionChange()));
    Thank you for your patience on this :)
     
  5. Aka_ToolBuddy

    Aka_ToolBuddy

    Joined:
    Feb 25, 2014
    Posts:
    549
    Thank you for your detailed answer.
     
  6. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,338
    Is this tracked as a bug?

    It's not so bad when the change event is eg. a ChangeEvent<string> or whatever, since we'll just be able to read off evt.newValue. But if we're getting a SerializedPropertyChangeEvent, there's just no way to know what the value changed to, which makes the event useless.

    The "check that the new value is valid" case is a lot less common than "use the new value" case. And this is yet another "here's a quirk that you have to remember" that I'm not going to remember. We shouldn't have to be UI Toolkit internals experts in order to use it.


    And I'm just deadly afraid that you'll refuse to fix this because the arbitrary Unity "we must preserve backwards compatibility!" timestamp gets placed after this change instead of before it.
     
    Keepabee likes this.
  7. herra_lehtiniemi

    herra_lehtiniemi

    Joined:
    Feb 12, 2017
    Posts:
    133
    @uMathieu I have this problem as well. I was wondering about if there are some new workarounds?

    My setup is like this:
    -I have an EditorWindow, where I present a list of data in a custom way. When user selects one data, a separate GUI is instantiated for them within the same EditorWindow, like this:
    * instantiate new GUI
    * bind each property of myCustomClass.propertyName to the GUI fields, where:
    -- newGUIUIElement.BindProperty(serializedMyCustomClassProperty)
    -- and then add onValueChangedCallback to the newGUIUIElement

    This is done each time a new data is selected from my custom view.

    But like you explained above, the callbacks get called before the real binding is done.

    You suggested creating the GUI in OnEnable, but what to do when new GUI is created after OnEnable, like described above? Basically when new data is selected from the GUI, I call "BindData(selectedDataID)".

    I tried your suggestion from another thread (https://forum.unity.com/threads/pro...differently-from-bindings-to-intfield.775583/) - as I understood, you suggested that after BindProperty(thisElement), I would immediately call BindProperty(parentOfThisElement)? This resulted a lot of callbacks triggering and didn't get rid of them. It also made the GUI generation incredibly slow, because I have tens of properties that bind and each of them called Bind before adding callbacks, causing serialization after each bind.

    Is there a way to force the binding immediately? Or should I "collect the callbacks" and apply them after all the bindings are done? In this case, how can I force the binding process after calling BindProperty on all fields and be sure it's done and then add the callbacks afterwards?

    Any suggestions to solve this are welcome!

    P.S. Loosely related to this, as a feature suggestion: I think a very high-level Angular-like API would be very welcome on top of the UI Elements. Some kind of "set it and forget it"-thingie, that would remove the boilerplate code of worrying about the callbacks. The bindings and worrying about callbacks seem complicated when one would want to just build the GUI and display data. Something to gap this would greatly help so one could just say "bind this UI field to this data" and when the data would disappear, the UI would disappear or rebind. Well, Angular-like stuff where callbacks wouldn't even often be needed. This would speed up working with UI elements and focus on the GUI-building, especially with data sets (lots of arrays and relational data).
     
    Last edited: Aug 12, 2021
  8. herra_lehtiniemi

    herra_lehtiniemi

    Joined:
    Feb 12, 2017
    Posts:
    133
    Has anyone come up with a solution to this, how to bind callbacks and make sure they are attached after binding when the UI is bound inside an EditorWindow and changes (=binding is done upon request and not only once)? In this scenario I can’t bind everything in OnEnable.

    What’s the correct way to bind callbacks after the elementary has been bound? Or alternatively detect that first callback and ignore it?
     
  9. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    I've 'liked' uMathieu's response - because it worked, and has finally (after many hours of debugging) 'fixed' a bug that has been causing data-corruption problems (Toggle values showing up in UnityEditor, but Unity deleting the values on save).

    But I hate it.

    The real bug here is that Toggle in UIToolkit just basically doesn't work - if it's been implemented in such a way that this arcane piece of knowledge is necessary, and the 1 frame delayed call (!) is necessary just to use the core API's ... then the class isn't fit for purpose, needs serious re-implemetnation/re-design.

    (extensive testing shows: yes, it's all necessary. I tried all combinations I could think of that were less obscure than this, even re-implementing most of the Undo/Binding/etc logic myself, but UIToolkit breaks in other ways or UnityEditor serialiation breaks in other ways, and they all generated one or more other problems. This workaround worked perfectly first time, and in all my different scenarios, so it seems to be the only correct way)
     
  10. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    I recommend against this solution in Unity versions from 2021.3 onwards. Unity now sometimes will do the bindings asynchronously even when the element is attached to panel. This can happen for performance when Unity is overloaded; I've noticed it happening after a domain reload, for example. In general, I like this change, because it's improved some performance bottlenecks in the editor; sometimes by a good margin.

    I'd recommend using TrackPropertyValue and TrackSerializedObjectValue when your UI needs to make sure that the serialized object has already been modified after a user action.

    I feel like I should say that it seems like a bad idea to design UI in a way that has these requirements. One reason is because an important idea behind UITK's paradigm is declarative programming, which is by definition asynchronous; it's like forcing a square peg into a round hole. If one needs to update the UI when a serialized field changes, it feels better to design it in a way that doesn't care about what happens first.

    And, if one needs to do something non-UI related every time a bound field's value changes, it feels wrong to do it from a ValueChangedCallback in a bound element. That callback is also called when the field changes because of things like Undo/Redo or making a tab visible again; so your UI would be causing non-UI effects that are not triggered by direct user interactions.

    In case it helps, here's what I do when I need it because of the previously mentioned case, which is rarely:
    • Sometimes I do the reaction to a field change in a place that guarantees it will be handled even when the change comes from outside the UI, like version control; that is either an ISerializationCallbackReceiver callback or an Object's OnValidate.
    • Sometimes I don't bind the field, so I can be sure the Change Callback is only triggered by user actions, then I use the callback to both update the SerializedProperty and do other side-effects, and I use things like TrackPropertyValue to update the field when it's related value changes from outside the UI.
     
    Last edited: Mar 24, 2023
    a436t4ataf likes this.