Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

PropertyField ChangeEvent callback behaves differently from Bindings to IntField

Discussion in 'UI Toolkit' started by ayellowpaper, Nov 10, 2019.

  1. ayellowpaper

    ayellowpaper

    Joined:
    Dec 8, 2013
    Posts:
    52
    Admittedly the title is a mouthful.

    What I want is simple: I want to work with serialized objects/properties and do some work on the target object after values have been changed. More precisely, whenever the user changes values, I want to generate a mesh.

    When using a PropertyField, the registered ChangeEvent will be triggered after the serialized property is set and changes applied to the SerializedObject. This makes sense and is at it should be. When using another field, like an IntField and setting the binding, the order is switched. The callback will be executed first, and then the property will be applied. Why? Examples below.

    Assume we change the value from 20 to 2, the Debug will print 2 because the SerializedProperty has been applied beforehand, as it should be.
    Code (CSharp):
    1. var countElement = new PropertyField(serializedObject.FindProperty("Count"));
    2. countElement.RegisterCallback<ChangeEvent<int>>(x => { Debug.Log(m_Component.Count); m_Component.GenerateMesh(); });
    Assume again, that we change the value from 20 to 2, the Debug will print 20 because the SerializedProperty will be applied after the callback was called.
    Code (CSharp):
    1. var countElement = new IntegerField("Count");
    2. countElement.BindProperty(serializedObject.FindProperty("Count"));
    3. countElement.RegisterCallback<ChangeEvent<int>>(x => { Debug.Log(m_Component.Count); m_Component.GenerateMesh(); });
    This seems counter intuitive.
     
  2. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    The
    RegisterCallback
    callbacks are executed in the order in which they were registered when looking at a single field. When looking at a hierarchy, children will execute before parents (by default).

    For the
    PropertyField
    , when it binds, it creates a new
    IntegerField
    as a child element of itself. Since you registered for the change on the
    PropertyField
    itself, you will get the event after the
    IntegerField
    (child element) has finished with all its
    ChangeEvent
    handling (which includes updating the
    SerializedField
    ).

    For the naked
    IntegerField
    , you are registering your callback directly on the field itself. You are also registering your callback before the binding has occurred and registered its own callback (yes, even though you called
    BindProperty()
    ). This is why your callback is called first.

    Looking at this, the source of confusion is probably
    BindProperty()
    . The line:
    countElement.BindProperty()
    does not actually bind anything. All that line does is assign the property path of
    "Count"
    to the
    bindingPath
    of
    countElement
    . When binding does actually happen, later on, this
    bindingPath
    will be used. Binding happens automatically after you returned from
    Editor.CreateInspectorGUI()
    if doing a custom inspector, or
    CreatePropertyGUI()
    for custom properties. If you're just creating your own custom
    EditorWindow
    , you'll need to explicitly call
    someParentElement.Bind(yourSerializedObject)
    . This will recursively bind all elements with a
    bindingPath
    inside
    someParentElement
    .

    But you should try to avoid calling this
    Bind()
    inside a custom inspector or property drawer. If you want to have your callback called after the
    SerializedObject
    has been modified, do what the
    PropertyField
    does and create a parent element for your field and register for the
    ChangeEvent
    on this parent element.
     
    Aldeminor and ayellowpaper like this.
  3. ayellowpaper

    ayellowpaper

    Joined:
    Dec 8, 2013
    Posts:
    52
    Thanks uDamian. It makes sense now that you explained it. I still think the API isn't very clear specifically when it comes to BindProperty(). I can imagine this might lead to more confusion among other developers as well. Possibly this deserves some highlighting in the docs?

    Thanks again. I'd remove the "Bug" from the title if I knew how. :p
     
  4. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    Removed it!

    But yes, refreshed docs are in the works and should hopefully clarify this a bit.
     
    ayellowpaper likes this.
  5. RaulDAI

    RaulDAI

    Joined:
    Feb 27, 2018
    Posts:
    7
    I'm really confused

    With BindProperty () it works perfectly. But it only works if I reload the script in the inspector.

    How can I make the entered values take effect immediately? Could someone give me an example?

    Note: I am using UI Builder

    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine.UIElements;
    3. using UnityEditor.UIElements;
    4.  
    5. [CustomEditor(typeof(LS))]
    6. public class LS_UIBuilder : Editor
    7. {
    8.     LS _LS;
    9.     VisualElement _RootElement;
    10.     VisualTreeAsset _VisualTree;
    11.  
    12.     private void OnEnable()
    13.     {
    14.         _LS = (LS)target;
    15.  
    16.         _RootElement = new VisualElement();
    17.  
    18.         _VisualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>
    19.             ("Assets/Editor/LS/LS_UIBuilder.uxml");
    20.     }
    21.  
    22.     public override VisualElement CreateInspectorGUI()
    23.     {      
    24.         _RootElement.Clear();
    25.         _VisualTree.CloneTree(_RootElement);              
    26.  
    27.  
    28.         var _UISize_Float = _RootElement.Q<FloatField>("_UISize");
    29.  
    30.         _UISize_Float.BindProperty(serializedObject.FindProperty("_Size"));
    31.  
    32.  
    33.         return _RootElement;
    34.     }
    35. }
     
    Last edited: Jul 12, 2020
  6. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    I appreciate you finding this old thread that seems similar but in the future, please start a new thread. Threads should be kept on topic to the specific issue in the first post.

    But the code you posted should be working correctly. I'm mostly not sure what you mean by "only works if I reload the script in the inspector". Do you mean unselect+reselect?

    I would try first to simplify things by just setting the "bindingPath" of your FloatField element to "_Size" (can use UI Builder for this) and just removing the line:
    Code (CSharp):
    1. _UISize_Float.BindProperty(serializedObject.FindProperty("_Size"));
    completely. It's not needed.

    Also, you need to save the UXML in the UI Builder and refresh the Inspector every time you make changes to it.

    Also worth re-reading my post above:
    https://forum.unity.com/threads/pro...rom-bindings-to-intfield.775583/#post-5163410

    Specifically:
     
  7. RaulDAI

    RaulDAI

    Joined:
    Feb 27, 2018
    Posts:
    7
    Entiendo, me arrepiento de no haber hecho otro hilo. soy nuevo en los foros

    Veo que no he sido claro al explicar mi problema. Así que haré un ejemplo más detallado con un video.

    No tengo muy claro si esto es normal o no. O necesito hacer algo para que cambie inmediatamente al ingresar el valor

    ¿Así que paso todo, desde OnEnable () hasta CreateInspectorGUI ()?
     
    Last edited: Jan 31, 2022
  8. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    I can't say what's going wrong without seeing how you use the Move value to translate the object. UI Toolkit will just update the property on the object you bound to. It's still up to you to detect this and propagate it.

    For example, if you use this value in the Update() override of your MonoBehaviour (just a suspicion, given the behavior in the video), you'll need to make sure you have [ExecuteInEditMode] attribute on your class to have it run while not in playmode:
    https://docs.unity3d.com/ScriptReference/ExecuteInEditMode.html
     
  9. RaulDAI

    RaulDAI

    Joined:
    Feb 27, 2018
    Posts:
    7
    I apologize if I am making stupid mistakes.

    I just want it to move or scale, immediately after entering the value in the inspector.

    These are the two scripts, I don't know what I'm doing wrong. I would like to keep Script Editor separate from MonoBehavior

    If I do it wrong, how is the correct way?

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3.  
    4. public class LS : MonoBehaviour
    5. {
    6.     public float _Move;
    7.     public float _Size = 1f;
    8.  
    9.     public void CHANGE()
    10.     {
    11.         this.transform.position = new Vector3(_Move, 0f, 0f);
    12.         this.transform.localScale = new Vector3(_Size, _Size, _Size);
    13.     }
    14. }
    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine.UIElements;
    3. using UnityEditor.UIElements;
    4.  
    5.  
    6. [CustomEditor(typeof(LS))]
    7. public class LS_UIBuilder : Editor
    8. {
    9.     LS _LS;
    10.     VisualElement _RootElement;
    11.     VisualTreeAsset _VisualTree;    
    12.  
    13.     public override VisualElement CreateInspectorGUI()
    14.     {
    15.         _LS = (LS)target;
    16.  
    17.         _RootElement = new VisualElement();
    18.  
    19.         _VisualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>
    20.             ("Assets/Editor/LS/LS_UIBuilder.uxml");
    21.  
    22.         _RootElement.Clear();
    23.         _VisualTree.CloneTree(_RootElement);
    24.  
    25.         var _UISize_Float = _RootElement.Q<FloatField>("_UISize");
    26.         var _UIMove_Float = _RootElement.Q<FloatField>("_UIMove");
    27.  
    28.         _LS.CHANGE();
    29.  
    30.         return _RootElement;
    31.     }  
    32. }
     
  10. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    CreateInspectorGUI() is only called when you select your GameObject. UI Toolkit is retained-mode UI. You don't draw the elements every frame like you did with OnGUI()/IMGUI. So your _LS.CHANGE() is only called once per selection.

    That is, the _Move and _Size properties are indeed updating in real time as you change them in the UI, but since CHANGE() is never called, you don't see the changes.

    As I mentioned above, if you want to have a poll model where the UI knows nothing about your "LS" object, you can use the Update() loop in LS to constantly transform the object based on those values, but you'll need [ExecuteInEditMode] because Update() only works in playmode by default.

    If you are ok with the UI knowing about LS and the CHANGE() function, as it seems you are, then you can use:
    Code (CSharp):
    1. _UISize_Float.RegisterCallback<ChangeEvent<float>>(evt -> _LS.CHANGE());
    Since UI Toolkit is retained-mode, you need to use events to know when a value has changed. In this case, the binding system takes care of updating the actual values of _Move and _Size, so you just need to register for the change event to know when it has changed so you can call CHANGE(). You should not need to also update _Move/_Size in this callback.
     
  11. RaulDAI

    RaulDAI

    Joined:
    Feb 27, 2018
    Posts:
    7
    Thank you very much nDamian for your patience and quick response.
    I had to do a little modification to make it work
    Code (CSharp):
    1. _UISize_Float.RegisterCallback<ChangeEvent<float>>((evt) => _LS.CHANGE());
    But now I have another error. I do not know if it's normal.

    It works perfectly when using the name controller, but entering a direct value doesn't change it right away, and only until you reenter another value does the change

    Code (CSharp):
    1. [CustomEditor(typeof(LS))]
    2. public class LS_UIBuilder : Editor
    3. {
    4.     LS _LS;
    5.     VisualElement _RootElement;
    6.     VisualTreeAsset _VisualTree;
    7.  
    8.     public override VisualElement CreateInspectorGUI()
    9.     {
    10.         _LS = (LS)target;
    11.  
    12.         _RootElement = new VisualElement();
    13.  
    14.         _VisualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>
    15.             ("Assets/Editor/LS/LS_UIBuilder.uxml");
    16.  
    17.         _RootElement.Clear();
    18.         _VisualTree.CloneTree(_RootElement);
    19.          
    20.  
    21.         var _UISize_Float = _RootElement.Q<FloatField>("_UISize");
    22.         var _UIMove_Float = _RootElement.Q<FloatField>("_UIMove");
    23.  
    24.         _UISize_Float.RegisterCallback<ChangeEvent<float>>((evt) => _LS.INSTANCIARLED());
    25.         _UIMove_Float.RegisterCallback<ChangeEvent<float>>((evt) => _LS.INSTANCIARLED());
    26.  
    27.         return _RootElement;
    28.     }
    29. }
     
    Last edited: Jan 31, 2022
  12. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
  13. RaulDAI

    RaulDAI

    Joined:
    Feb 27, 2018
    Posts:
    7
    It is extremely rare, how this works
    Debugging indicates that it executes _INSTANCIARLED () with the previous value. And although it represents the value in the inspector, it is not used until the next change.

    There is something that is even stranger. When I enter, for example, the number 123, while I enter the values it executes, first 1, then 12 and finally 123. It does not allow me to enter the full value. RegisterCallback <ChangeEvent registers each number I enter and changes it as I enter more numbers

    doesn't seem to work properly

    How could I update the _Move variable first and then run _INTANCIARLED ()?

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class LS : MonoBehaviour
    4. {
    5.     public float _Move;
    6.     public float _Size = 1f;
    7.  
    8.     public void INSTANCIARLED()
    9.     {
    10.         GameObject _LEDViejo = GameObject.Find("LS LED(Clone)");
    11.         DestroyImmediate(_LEDViejo);
    12.  
    13.         GameObject _NuevoLED = Resources.Load("LS LED") as GameObject;
    14.         _NuevoLED.transform.position = new Vector3(_Move, 0f, 0f);
    15.  
    16.         Instantiate(_NuevoLED, _NuevoLED.transform.position, _NuevoLED.transform.rotation, this.transform);
    17.     }
    18. }
    19.  
    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine.UIElements;
    3. using UnityEditor.UIElements;
    4.  
    5. [CustomEditor(typeof(LS))]
    6. public class LS_UIBuilder : Editor
    7. {
    8.     LS _LS;
    9.     VisualElement _RootElement;
    10.     VisualTreeAsset _VisualTree;
    11.  
    12.     public override VisualElement CreateInspectorGUI()
    13.     {
    14.         _LS = (LS)target;
    15.  
    16.         _RootElement = new VisualElement();
    17.  
    18.         _VisualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>
    19.             ("Assets/Editor/LS/LS_UIBuilder.uxml");
    20.  
    21.         _RootElement.Clear();
    22.         _VisualTree.CloneTree(_RootElement);
    23.  
    24.  
    25.         var _UISize_Float = _RootElement.Q<FloatField>("_UISize");
    26.         var _UIMove_Float = _RootElement.Q<FloatField>("_UIMove");
    27.  
    28.         _UIMove_Float.RegisterCallback<ChangeEvent<float>>((evt) => _LS.INSTANCIARLED());
    29.         _UISize_Float.RegisterCallback<ChangeEvent<float>>((evt) => _LS.INSTANCIARLED());
    30.  
    31.  
    32.         return _RootElement;
    33.     }
    34. }
    35.  
     
    Last edited: Jan 31, 2022
  14. uMathieu

    uMathieu

    Unity Technologies

    Joined:
    Jun 6, 2017
    Posts:
    396
    You can set the isDelayed property on the FloatField to receive the ChangeEvent only when the user presses enter or the field loses keyboard focus.
     
  15. RaulDAI

    RaulDAI

    Joined:
    Feb 27, 2018
    Posts:
    7
    An excellent solution to the problem, to constant change before finishing writing the value.
    But the same continues, continues to run with the value prior to that entered the inspector. It is as if the variable has not been updated.
    It is always with the previous value ... sorry if I don't understand this correctly. Examples are appreciated
    Code (CSharp):
    1.     public override VisualElement CreateInspectorGUI()
    2.     {
    3.         _LS = (LS)target;
    4.  
    5.         _RootElement = new VisualElement();
    6.  
    7.         _VisualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>
    8.             ("Assets/Editor/LS/LS_UIBuilder.uxml");
    9.  
    10.         _RootElement.Clear();
    11.         _VisualTree.CloneTree(_RootElement);
    12.  
    13.  
    14.         var _UIMove_Float = _RootElement.Q<FloatField>("_UIMove");
    15.         var _UISize_Float = _RootElement.Q<FloatField>("_UISize");
    16.  
    17.         _UIMove_Float.isDelayed = true;
    18.         _UIMove_Float.RegisterCallback<ChangeEvent<float>>((evt) => _LS.INSTANCIARLED());
    19.  
    20.         _UISize_Float.isDelayed = true;
    21.         _UISize_Float.RegisterCallback<ChangeEvent<float>>((evt) => _LS.INSTANCIARLED());
    22.  
    23.         return _RootElement;
    24.     }
     
  16. uMathieu

    uMathieu

    Unity Technologies

    Joined:
    Jun 6, 2017
    Posts:
    396
    This is a quirk of the way the bindings system works. When a field is bound, it registers a callback on the field, exactly like you did. In this callback, it then applies the value on the serializedObject.

    Problem is, since you register the callback before the binding system, you get notified before the binding applied the modified value to the SerializedObject. This is not the best workflow and we'll improve on it. For the time being, you can schedule the registration to run on the next frame to work around the issue:
    Code (CSharp):
    1.  
    2. _RootElement.schedule.Execute( () =>
    3. {
    4.     _UIMove_Float.RegisterCallback<ChangeEvent<float>>((evt) => _LS.INSTANCIARLED());
    5.     _UISize_Float.RegisterCallback<ChangeEvent<float>>((evt) => _LS.INSTANCIARLED());
    6. }
     
    RaulDAI likes this.
  17. RaulDAI

    RaulDAI

    Joined:
    Feb 27, 2018
    Posts:
    7
    Perfect! It was exactly what I was looking for. Thank you very much uMathieu.
    And thanks also for the quick response, without your help I have never been able to figure out how to do it.
    I hope this thread is useful for many more people.
     
  18. Timboc

    Timboc

    Joined:
    Jun 22, 2015
    Posts:
    238
    I just wanted to say this issue gives me somewhat continuous headaches and the above gets *really* messy in complex situations (like having to cache the scheduled item and cancel it the next frame etc). Is there anything in specific you're able to expand on about how this might be improved in the future @uMathieu? Many thanks in advance.
     
    a436t4ataf likes this.
  19. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    You could instead bind directly to the transform on your target object, in which case you would not need to keep up with the updated value using the above workarounds.

    Another approach would be to look at the ChangeEvent<float>.newValue (which will always be up to date) instead of reading the value from the object being modified (which will be behind by one frame).

    In terms of serialization and bindings, our efforts are shifting to creating a bindings system for runtime (as there isn't one yet). This means we're unlikely to change the workflow for Editor bindings in the near term.
     
    Timboc likes this.
  20. AdamBebko

    AdamBebko

    Joined:
    Apr 8, 2016
    Posts:
    164
    How does one bind to a transform? Pardon my ignorance. I'm getting similar issues as people in this thread. I'm writing a custom inspector to convert spherical coordinate fields to edit the transform. I can get the binding working one way, I can update the inspector based on the changes made in scene view, or vice versa. But when I try to allow both to work at once, they have some strange interaction from RegisterValueChangedCallback, where it gets called both from the scene view edits, and also from the inspector edits, making it break. Is there perhaps a callback that only gets fired from changes made in the GUI? Or this binding directly to a transform might solve this problem too.