Search Unity

Register callback question

Discussion in 'UI Toolkit' started by Araj, Apr 1, 2020.

  1. Araj

    Araj

    Joined:
    Jan 3, 2013
    Posts:
    27
    Hi,

    I'm currently creating some utility Attributes to handle the UI, so far everything is working fine but I think my implementation could be better.

    For example: I'm creating a ShowIf attribute (and CustomPropertyDrawer) that will hide or show a property if certain conditions are met
    Code (CSharp):
    1. public bool visible = false;
    2.  
    3. [ShowIf(nameof(visible))]
    4. public string TestShowIf;
    In the code what I'm doing is get the current value of the variable "visible" and make "TestShowIf" visible or not, depending on the boolean, and then scheduling the same check to be done every 500 milliseconds:

    Code (CSharp):
    1. element.schedule.Execute(theAction).Every(500);
    I was wondering if there is a better way to do this.

    Sadly I can't share the code because it's under an NDA :(
     
  2. Araj

    Araj

    Joined:
    Jan 3, 2013
    Posts:
    27
    Hi again,

    I tried to do something like this, but so far doesn't work:

    Code (CSharp):
    1.     [CanEditMultipleObjects]
    2.     [CustomEditor(typeof(Object), true, isFallback = true)]
    3.     public class DefaultEditor : Editor
    4.     {
    5.         public override VisualElement CreateInspectorGUI()
    6.         {
    7.             var container = new VisualElement();
    8.  
    9.             var iterator = serializedObject.GetIterator();
    10.             if (iterator.NextVisible(true))
    11.             {
    12.                 do
    13.                 {
    14.                     var propertyField = new PropertyField(iterator.Copy())
    15.                     {
    16.                         name = "PropertyField:" + iterator.propertyPath
    17.                     };
    18.  
    19.                     if (iterator.propertyPath == "m_Script" && serializedObject.targetObject != null)
    20.                     {
    21.                         propertyField.SetEnabled(value: false);
    22.                     }
    23.  
    24.                     container.Add(propertyField);
    25.                 } while (iterator.NextVisible(false));
    26.             }
    27.  
    28.             container.RegisterCallback<ChangeEvent<SerializedProperty>>(x =>
    29.             {
    30.                 Debug.Log("Changes to SerializedProperty");
    31.             });
    32.             return container;
    33.         }
    34.     }
    As I understand, the container.RegisterCallback<ChangeEvent<SerializedProperty>> should notify when changes are made to any of the SerializedProperties of the element, right? So far It's not working. However if I change the ChangeEvent to detect changes to a bool, it works as expected.
    container.RegisterCallback<ChangeEvent<bool>>
     
  3. Catsoft-Studios

    Catsoft-Studios

    Joined:
    Jan 15, 2011
    Posts:
    703
    The specific type of the ChangeEvent<T> should be the type of the field, not the Serialized Property. So if you want to detect changes on the visible field, you do:

    Code (CSharp):
    1. PropertyField field = new PropertyField(this.serializedObject.FindProperty("visible"));
    2. field.RegisterCallback((ChangeEvent<bool> e) =>
    3. {
    4.     Debug.Log("Change in visibility");
    5. });
    And this works. However, I've tried going one step further and trying to detect changes on entire classes. For example, the following portion is inside a MonoBehavior class called "MyMonoBehavior.cs":

    Code (CSharp):
    1. [Serializable]
    2. public class MyClass
    3. {
    4.     public string hello = "Hello World";
    5. }
    6.  
    7. public MyClass myClass = new MyClass();
    And the Editor code:

    Code (CSharp):
    1. PropertyField field = new PropertyField(this.serializedObject.FindProperty("myClass"));
    2. field.RegisterCallback((ChangeEvent<MyMonoBehavior.MyClass> e) =>
    3. {
    4.     Debug.Log("Change detected!");
    5. });
    6.  
    7. return field;
    But changing the values in the Inspector doesn't fire the change detection. Not sure if this is a bug or PropertyFields are not meant to detect changes deep in their hierarchy. Hopefully someone from Unity will pick this up.
     
    uDamian likes this.
  4. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    To add, at this time,
    [CanEditMultipleObjects]
    (multi-selection editing) is not properly supported by UI Toolkit (formally UIElements) at this time. Just be aware when using those tags. They will work in the future though.
     
  5. Araj

    Araj

    Joined:
    Jan 3, 2013
    Posts:
    27
    Hey thanks for your answer :)

    What I wanted to achieve is to detect changes to any field in the SerializedObject, more or less the UI Toolkit equivalent to the old EditorGUI.BeginChangeCheck / EditorGUI.EndChangeCheck, but I'm not sure if that is possible at the moment.
    Maybe @uDamian can throw some light into this.

    Thanks!
     
    Catsoft-Studios likes this.
  6. Catsoft-Studios

    Catsoft-Studios

    Joined:
    Jan 15, 2011
    Posts:
    703
    Yeah I'm on the same boat. Maybe not detecting any change in a SerializedObject, but in a managed class instance. Using a PropertyField like so (I've posted this above, but just so it's clearer) should, in theory, work. However it does not.

    Code (CSharp):
    1. [Serializable]
    2. public class MyClass
    3. {
    4.     public string hello = "Hello World";
    5. }
    6. public MyClass myClass = new MyClass();
    Code (CSharp):
    1. PropertyField field = new PropertyField(this.serializedObject.FindProperty("myClass"));
    2. field.RegisterCallback((ChangeEvent<MyClass> e) =>
    3. {
    4.     Debug.Log("Change detected!");
    5. });
    @uDamian Can we expect this to work in the future? Or should we add change event listeners for each child property?
     
  7. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    To clarify, the
    ChangeEvent
    is not fired by the
    PropertyField
    itself. It is instead fired by whatever UI fields the
    PropertyField
    created inside during the
    Bind()
    . So, in your case of creating a
    PropertyField
    of
    MyClass
    , you would see inside a
    TextField
    for the hello field. Therefore, the
    ChangeEvent
    you would get when the use makes a change would be a
    ChangeEvent<string>
    .

    Also, a note that you
    EditorGUI.BeginChangeCheck
    /
    EditorGUI.EndChangeCheck
    are no longer relevant for UI Toolkit as you don't need to manually update the
    SerializedObject
    yourself. If you used
    Bind()
    and/or
    CreateInspectorGUI()
    with
    PropertyField
    , all the updated to the SerializedObject should be automatic. The only reason to register for
    ChangeEvents
    would be to do something else in your UI when the user makes a change (like turn the field red if invalid, for example).
     
  8. Catsoft-Studios

    Catsoft-Studios

    Joined:
    Jan 15, 2011
    Posts:
    703
    Ah, understood, so basically the PropertyField acts as a relay that takes any change from its child elements and bubbles it up. However, shouldn't the change type in PropertyField group the entire state of the object? Not only the changed field.

    Because for example, if you have the following class:

    Code (CSharp):
    1. public class MyClass
    2. {
    3.     public string textA = "hello";
    4.     public string textB = "world";
    5.     public bool isOn = false;
    6. }
    7.  
    8. public MyClass myClass = new MyClass();
    And you want to detect any change made in the Inspector, you have to do something like this (where propertyField is a PropertyField type bound to the SerializedProperty of the myClass member):

    Code (CSharp):
    1. propertyField.RegisterCallback((ChangeEvent<string> e) =>
    2. {
    3.     Debug.Log("Change for either textA or textB");
    4. });
    5.  
    6. propertyField.RegisterCallback((ChangeEvent<bool> e) =>
    7. {
    8.     Debug.Log("Change for isOn");
    9. });
    Instead. This isn't very versatile, because you need a change event listener for every type of your class member. It would be much more useful if we could simply ask for any change in any member of the class instance.

    Code (CSharp):
    1. propertyField.RegisterCallback((ChangeEvent<MyClass> e) =>
    2. {
    3.     Debug.Log("Change for either textA or textB or isOn");
    4. });
    Is this something that could be considered? The use case is, as you mention, validating that the values of a form are correct and painting them in red after validating each one of them. We're doing something similar, but also using polymorphic serialization (with the [SerializeReference] attribute), so we don't know which types the members of the class will have.
     
    Last edited: Apr 2, 2020
  9. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    The
    PropertyField
    itself does not do anything regarding forwarding of events or tracking changes. Each individual UI field is responsible for this. For your example above, you'll have 2
    TextFields
    and 1
    Toggle
    . When the users changes a
    TextField
    , you get a
    ChangeEvent<string>
    . The
    PropertyField
    is not involved. Therefore, you cannot have a
    ChangeEvent<MyClass>
    , unless you had a monolithic custom C# field (inheriting from
    BaseField<T>
    ) that is specialized to have value of type
    MyClass
    . (fyi, there are some limitations right now that prevent you from creating your own
    BaseField<T>
    , so that's not yet an option anyway).

    That said, we know this is a limitation. We might have the
    PropertyField
    send out a generic
    ChangeEvent
    in the future, but at this time, you need to register
    ChangeEvents
    for all supported primitive types. A reminder that you can do this at the root of your inspector to catch ALL fields in all
    PropertyFields
    inside. These events bubble up all the way to the root unless they are caught by an element. So you don't need to do this on every
    PropertyField
    . Using the
    ChangeEvent<T>
    , you can also look at the
    ChangeEvent.target
    and
    ChangeEvent.newValue
    to know what has changed (which property via
    bindingPath
    ) and what the new value is (hence the need to have a specialized type T).
     
    Catsoft-Studios likes this.
  10. Catsoft-Studios

    Catsoft-Studios

    Joined:
    Jan 15, 2011
    Posts:
    703
    Thank you Damian for the detailed response. Makes sense, as I was thinking about detecting changes on the Serialized Property, but instead, changes are detected on each type of widget (the the thing that inherits from BaseField, not sure if it has a name). A generic
    ChangeEvent
    would certainly help. Meanwhile, we'll do as you suggested, and capture all primitive types.

    In case someone's interested, here's what I quickly put up. It hasn't been tested in depth, but should give a head start anyone struggling with the same:

    Code (CSharp):
    1. public void RegisterAnyChangeEvent(this VisualElement field, Action callback)
    2. {
    3.     field.RegisterCallback((ChangeEvent<int> e) => callback());
    4.     field.RegisterCallback((ChangeEvent<bool> e) => callback());
    5.     field.RegisterCallback((ChangeEvent<float> e) => callback());
    6.     field.RegisterCallback((ChangeEvent<string> e) => callback());
    7.     field.RegisterCallback((ChangeEvent<Color> e) => callback());
    8.     field.RegisterCallback((ChangeEvent<UnityEngine.Object> e) => callback());
    9.     field.RegisterCallback((ChangeEvent<Enum> e) => callback());
    10.     field.RegisterCallback((ChangeEvent<Vector2> e) => callback());
    11.     field.RegisterCallback((ChangeEvent<Vector3> e) => callback());
    12.     field.RegisterCallback((ChangeEvent<Vector4> e) => callback());
    13.     field.RegisterCallback((ChangeEvent<Rect> e) => callback());
    14.     field.RegisterCallback((ChangeEvent<AnimationCurve> e) => callback());
    15.     field.RegisterCallback((ChangeEvent<Bounds> e) => callback());
    16.     field.RegisterCallback((ChangeEvent<Gradient> e) => callback());
    17.     field.RegisterCallback((ChangeEvent<Quaternion> e) => callback());
    18.     field.RegisterCallback((ChangeEvent<Vector2Int> e) => callback());
    19.     field.RegisterCallback((ChangeEvent<Vector3Int> e) => callback());
    20.     field.RegisterCallback((ChangeEvent<RectInt> e) => callback());
    21.     field.RegisterCallback((ChangeEvent<BoundsInt> e) => callback());
    22. }
    To use it, simply use the
    RegisterAnyChangeEvent(callback)
    extension method. Like so:

    Code (CSharp):
    1. myPropertyField.RegisterAnyChangeEvent(() =>
    2. {
    3.     Debug.Log("Something changed in a child property!");
    4. });
    Also, this should be fairly easy to modify in the event (pun intended :)) that Unity implements a generic ChangeEvent. Cheers and thanks again!

    EDIT: Made extension method more generic.
     
    Last edited: Apr 3, 2020
  11. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    Seems there's still some fog around this. My bad. To reiterate, these RegisterCallbacks should not be done on each PropertyField. Events bubble up. So if all your PropertyFields are children of a ROOT element, then all these registrations should be done once on this ROOT element - not on each PropertyField.

    I would modify your extension method to work on any VisualElement, not just PropertyFields.
     
    Catsoft-Studios likes this.
  12. Catsoft-Studios

    Catsoft-Studios

    Joined:
    Jan 15, 2011
    Posts:
    703
    Definitely. I'll change the original text to avoid any confusion.

    Not necessarily on all cases. For example, in my case I want to detect any changes made on each element of a list, so that the header on each list item contains a summary of what's inside (taking into account its values). See the image below.

    Screen Shot 2020-04-03 at 5.03.08 PM.png

    So far, I think the question has been resolved. Thank you for taking the time to answer :) Although the new UI system is a bit tricky to get used to, so far I'm very happy with the direction its taking!
     
  13. Araj

    Araj

    Joined:
    Jan 3, 2013
    Posts:
    27
    Wow that list you have there looks absolutely amazing!
    Can you share how you managed to do that? Does the drawer work on any kind of List? Or is a drawer for a specific class?
     
    Catsoft-Studios likes this.
  14. Catsoft-Studios

    Catsoft-Studios

    Joined:
    Jan 15, 2011
    Posts:
    703
    Sure! It's a drawer for any array-type that implements a specific interface, but also requires a custom visual element (so it's not that one-for-all solution). However, the basic structure is based on another thread where Matthew kindly provided a very neat way to deal with arrays: https://forum.unity.com/threads/pro...y-dont-refresh-inspector.747467/#post-5167271.

    Also, to implement the drag and drop, this part of the documentation was very useful: https://docs.unity3d.com/2020.1/Documentation/Manual/UIE-Events-DragAndDrop.html.

    Hope this helps! :)
     
  15. Araj

    Araj

    Joined:
    Jan 3, 2013
    Posts:
    27
    Hey thanks a lot!
    The info that you provided was super useful :)

    Gracias! ;)
     
    Catsoft-Studios likes this.
  16. fherbst

    fherbst

    Joined:
    Jun 24, 2012
    Posts:
    802
    I'm still not sure I understand this with the above info.
    How do I register for changes in an array/list?

    Say I have a
    public List<MyCustomScriptableObject> myList;

    and
    root.Add(new PropertyField(serializedObject.FindProperty("myList")));


    how do I register for changes to this specific list?
    I tried
    root.RegisterCallback((ChangeEvent<MyCustomScriptableObject> x)

    and
    root.RegisterCallback((ChangeEvent<List<MyCustomScriptableObject>> x)

    but neither does anything.
    Neither does registering for int changes (as another thread suggested) to get the change of count.
     
    Last edited: May 19, 2020
  17. Catsoft-Studios

    Catsoft-Studios

    Joined:
    Jan 15, 2011
    Posts:
    703
    Changes bubble up (meaning that the parent objects will receive notifications on any changes made to child properties). For example, if you have the following:

    Code (CSharp):
    1. class A
    2. {
    3.     public B b = new B();
    4. }
    5.  
    6. class B
    7. {
    8.     public C[] cs = { new C() };
    9. }
    10.  
    11. class C
    12. {
    13.     public int value;
    14. }
    A PropertyField that is bound to A will receive event from changes done to A, B and C. A PropertyField bound to B will receive change events made on B and C (but not on A). C will receive change events made on the value field.

    However, I think there should be a generic event that registers any change, because a parent class may not be aware of its child object types (for example, if you use polymorphism). Something like
    RegisterCallback(ChangeEventAny e)
    . Something that could be assessed @uDamian ? I can provide use cases where this would be very useful and there's no workaround (that I am aware of).

    Hope this helps!
     
  18. fherbst

    fherbst

    Joined:
    Jun 24, 2012
    Posts:
    802
    I got the part about the bubbling up, but I don't receive any events for PropertyFields that have lists as content, no matter where I try to register for them.
     
  19. Catsoft-Studios

    Catsoft-Studios

    Joined:
    Jan 15, 2011
    Posts:
    703
    Try making sure they are bound to a serialized object. Other than that, I don't see what else could be wrong (other than a bug in the UI Toolkit, but so far, this has worked out for me)
     
  20. fherbst

    fherbst

    Joined:
    Jun 24, 2012
    Posts:
    802
    But what do you register? <List<MyType>>? Just <MyType>? Your code above only handles non-array types, as far as I can see.
     
  21. Catsoft-Studios

    Catsoft-Studios

    Joined:
    Jan 15, 2011
    Posts:
    703
    I haven't worked on this lately, so I am no 100% sure, but I think just ChangeEvent<MyType>
     
  22. Quasimodem

    Quasimodem

    Joined:
    May 21, 2013
    Posts:
    53
    Sorry to report that nothing suggested in this thread works. I just registered a change listener for every supported type on every VisualElement in my entire hierarchy and not a single one fires when I drag a new Object into a field bound with a PropertyField. Day 2 of disappointment with VisualElements!
     
  23. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    7,536
    I'm struggling here too. This seems basic functionality again being needlessly complicated. If I have a
    PropertyField
    , why can't I know when it has changed?

    Adding callbacks on every relevant type on a
    PropertyField
    gets no actual callbacks. What gives?

    In my particular application I would like to either know when a
    PropertyField
    changes so I can update an
    Image
    sprite value - or I would like to bind an
    Image
    to a
    SerializedProperty
    . Unfortunately
    Image
    has no
    .BindProperty
    method for some reason and
    PropertyField
    won't deliver on callbacks so what the heck is the actual approach here?

    If I use
    RegisterValueChangeCallback
    then I do get a callback, but its when the field is interacted with, and doesn't even fire when the actual value changes. This is confirmed because the field is serialized to the new value, but no event is fired when it does, it's fired when the field is interacted with.

    Finally to confirm this, the code
    propertyField.RegisterCallback<SerializedPropertyChangeEvent>(method);
    does not fire the callback when the serialized property actually does change it's value. This appears to be a bug, or at the very least some internal mixup between the VisualElement changing and the SerializedObject/Property changing. Either way, it does not seem to work as intended.
     
    Last edited: Aug 3, 2022