Search Unity

Quickly clearing all callbacks on UI elements (TextField, Button)?

Discussion in 'UI Toolkit' started by herra_lehtiniemi, Jan 23, 2021.

  1. herra_lehtiniemi

    herra_lehtiniemi

    Joined:
    Feb 12, 2017
    Posts:
    133
    Is there a way to clear all clickable.clicked callbacks besides clearing a specific handler like Button.clickable.clicked -= handler?

    I bumped to this same issue with TextField.RegisterValueChangedCallback where I have to unregister each callback individually and to do this, I have to save references to them. Would be much easier to call something like TextField.ClearValueChangedCallbacks() and do Button.clickable.clicked = null

    Since UI Toolkit by nature can be used for building reusable GUI's that get bound to different elements with same structure, it would be really handy to have quick ways of resettings handlers and callbacks on objects. This would allow developer to easily repurpose objects for new data with new handlers.
     
  2. SimonDufour

    SimonDufour

    Unity Technologies

    Joined:
    Jun 30, 2020
    Posts:
    574
    There is no way to clear all registered callbacks at the moment. It could potentially break by removing internal callback that were created during the object's construction that you would have no idea they exist from the outside.

    Your request is not a bad idea in itself, it just needs to find a way to guarantee there will be no unfortunate side effect. The developers have been made aware of your feature request.

    For the case of the button, you should be able to assign a new Clickable since it is part of the public API.

    Let us know if you need more help with this.
     
  3. SimonDufour

    SimonDufour

    Unity Technologies

    Joined:
    Jun 30, 2020
    Posts:
    574
    button.clickable = null 
    should also work
     
    Tony_Max and sewy like this.
  4. herra_lehtiniemi

    herra_lehtiniemi

    Joined:
    Feb 12, 2017
    Posts:
    133
    Thanks for the tip on the button. Is the recommended way so far then instead re-instantiate the reusable UXML-components each time I bind to new variables (so just get rid of the previous components)? This way I wouldn't unregister the handlers at all, but just clear the elements that had them. Will there be trouble?

    As a related question, if I proceed with this method (just regenerating the VisualElements each time I rebind, do I have to Unbind the previous before forgetting them or can I just always rebind the SerializedProperties into new elements?
     
  5. SimonDufour

    SimonDufour

    Unity Technologies

    Joined:
    Jun 30, 2020
    Posts:
    574
    The best and proper way is to do the proper maintenance and keep track of the delegate to minimize the garbage collection.

    Yes you generally need to unbind to be safe.

    Rules can be broken, but I really suggest you follow them. :)
     
    TomTrottel likes this.
  6. herra_lehtiniemi

    herra_lehtiniemi

    Joined:
    Feb 12, 2017
    Posts:
    133
    Good to know! Thank you.
     
  7. tonytopper

    tonytopper

    Joined:
    Jun 25, 2018
    Posts:
    226
    I feel that UI Toolkit needs to address this soon. I've worked with vanilla web, SwiftUI, UIKit, React, Angular, Android's UI, and others, and now Unity. And I keep getting surprised by what Unity frameworks do not have. I've gotten the impression that Unity devs seem too eager to get something labeled "1.0". Labeling something 1.0 sends an impression to users of their platform that can create misaligned expectations.

    Sorry if that sounds too strong a complaint, but I've staked my livelihood on this platform. Keep'n it real.

    As a user of UI Toolkit, or any UI system where you work with callbacks, events, triggers, and elements, I expect a clear and straightforward way to inspect what relationships are already present between these things.

    This is two-fold. One, UI Toolkit Debugger needs a way to see event relationships. Two, the APIs need a way, or a clearer way, cause I can't figure it out, to find existing callbacks that are registered to an element.

    If I were a dev on UI Toolkit, I'd be embarrassed to release preview 15 without at least an API to do this.
     
    VenetianFox, Redec5, filod and 5 others like this.
  8. MostHated

    MostHated

    Joined:
    Nov 29, 2015
    Posts:
    1,235
    Another one that seems pretty simple and would save so much time typing that I can't believe is missing:

    Why can't I copy the text/lines of any of these?

     
  9. herra_lehtiniemi

    herra_lehtiniemi

    Joined:
    Feb 12, 2017
    Posts:
    133
    @SimonDufour After having worked with UI toolkit for a while, I feel that UI Toolkit would benefit greatly of some kind of high level layer on top of it. Something that would make it really simple to take care of all bindings and callbacks without having to manually build structures to take care of them. Things would be more automatic and the main work would go into designing the layout.

    My application has changing data underneath so that the UI elements are the same most of the time (or for lists the elements are instantiated of course). I find out that I have to manually serialize stuff, keep the callback references to buttons and input fields, remember the clean them up etc. I would like to see some kind of ease from ie. Angular workflow and way of binding (from user's point of view).

    In Angular I can have a list of data and in the "view" I just say that "loop this data and display it - each data has this callback to process it on click". I don't have to worry about unbinding when the data underneath the controllers changes. Or I can have a field that is similarly bound to a variable inside a class structure and the main class can change. I can just bind it and if the data changes, the field updates.

    I wish UI toolkit would introduce a high level layer like this - to take care of all bindings and callbacks with a simple way. I really like the USS and the ability to design the UI in a new way, but all this technical stuff gets in the way really easily and I find myself using all my time figuring out the bindings or why my interface slows down because some callback gets called twice or trying to find a common base class to button-element and textfield-element to save callback references for both in one universal function call. It gets really complicated really easily and a lot of my code in my current project is related to binding and serializing fields one by one, instead of new functionality and the actual product.

    SUGGESTION:
    What would help would be that one could give some service where I could just make a single call that would contain: 1) ANY kind of UI element, 2) target data path to bind to (data may change, service should rebind automatically) - it should be possible to give a nested class property path as well 3) onElementChange-callback 4) onDataChangeCallback. When the UI element would be destroyed, service would clean up. But when data would change, service would just rebind without problems.

    The idea would be that I could just do this once and I wouldn't have to trigger unbindings manually but this would be done when the UI element disappears from view.
     
    Last edited: May 7, 2021
  10. Timboc

    Timboc

    Joined:
    Jun 22, 2015
    Posts:
    238
    This doesn't directly addresses your concerns but FYI if it helps you track things down, the Unity debug mode (i.e. going to about and type `internal`) exposes a UITK Event analyser. It's pretty fiddly to get anything useful out of it (which is probably part of the reason it's not exposed by default) but I've found it useful on occasion.
     
  11. tonytopper

    tonytopper

    Joined:
    Jun 25, 2018
    Posts:
    226
    Thank you, I'll check that out. That seems to imply though that my gripes are already on the radar. I like the direction that UITK takes us but it really feels something like a 0.8, not a 1.0.
     
  12. magnetic_scho

    magnetic_scho

    Joined:
    Feb 2, 2020
    Posts:
    95
    I ran into this issue as well. Once we introduced UniRx in our project, things got significantly easier in terms of binding/clearing callbacks.

    This is how we bind an action to a button:

    Code (CSharp):
    1.  
    2. # Binding extensions
    3. public static IDisposable Bind(this Button button, Action action)
    4. {
    5.     return button.AsObservable().Subscribe(_ => action());
    6. }
    7.  
    8. private static IObservable<Unit> AsObservable(this Button button)
    9. {
    10.     return Observable.FromEvent(h => button.clicked += h, h => button.clicked -= h);
    11. }
    12.  
    13. # Usage
    14. private void InitializeBinding(){
    15.    _button.Bind(() => {
    16.       # Some action
    17.    }).AddTo(gameObject);
    18. }
    19.  
    The AddTo method diposes the IDiposable returned by the bind method when the gameObject is destroyed and thus, removes the clicked handler from the button.
     
    Last edited: May 12, 2021
  13. MostHated

    MostHated

    Joined:
    Nov 29, 2015
    Posts:
    1,235
    I thought I heard something before that if a VisualElement is removed from the hierarchy, that its events were removed as well, but I might be remembering incorrectly.
     
  14. tankorsmash

    tankorsmash

    Joined:
    Jul 20, 2019
    Posts:
    7
    In trying to clear a Button's callbacks and then setting a new one, setting a
    button.clickable = null
    then trying to do
    button.clickable.clicked += () => {}
    throws a null ref exception. Is this intended?

    Setting the clickable to null does clear the callback list, but you can't add new ones after.
    button.clickable = new UnityEngine.UIElements.PointerClickable();
    doesn't work either because PointerClickable is private, so there's no way to 'undo' the '= null'.

    Is the answer to basically just bind your own wrapper to clickable.clicked once and then from inside the wrapper, manage all this yourself?

    edit:
    button.clickable = new Clickable(()=>{ });
    does allow you to reset the callbacks, it looks like.
     
    Last edited: Jun 4, 2021
    Antibody_antibody, blisz and t2g4 like this.
  15. SimonDufour

    SimonDufour

    Unity Technologies

    Joined:
    Jun 30, 2020
    Posts:
    574
    Glad you were able to find a solution!

    But I would like to insist (for all the peoples that will land on this thread in the future) that clickable.clicked is meant to act as an event and that it is a bad practice to clear all subscriber to an event. The good practice is to keep a reference to what you subscribed to and to remove the callback once it is not needed anymore.

    By default, the C# event do not allow clearing the subscriber for this reason. (there is no = operator, just += and -= so that you cannot clear the full list)

    https://www.tutorialsteacher.com/articles/difference-between-delegate-and-event-csharp
     
    tankorsmash likes this.
  16. tankorsmash

    tankorsmash

    Joined:
    Jul 20, 2019
    Posts:
    7
    Thanks Simon.

    I'm trying to get the hang of Unity, and I'm not sure of the new best practices, so I appreciate your reiteration. Since I'm adding lambdas, I'm not sure how C# handles appending vs removing them. I think adding and removing them makes more sense when you've got a bunch of methods you can reference by name or whatever, and removing them is more clear.

    I'll need to experiment with lambdas and how they work with these clickables. And read more about Delegates vs Events (since they're a new concept to me, coming from C++ and Python)

    In my case as well, I'm totally clearing the button's state to blank, so it feels okay to wipe all its handlers out at once, but I can imagine that once the project gets more advanced, you'll have more complex handles out there and you wouldn't want to lose track of what's added where and when.
     
  17. dthurn

    dthurn

    Joined:
    Feb 17, 2015
    Posts:
    77
    Implementing any type of React-like framework or element recycling system in UIToolkit is pretty painful without this functionality. I understand why you'd believe that people building specific game UI screens should never need this behavior, but enabling framework-level code sometimes means trusting your users and giving access to low-level hooks.
     
    ontrigger, Bunderant and tonytopper like this.
  18. pblpbl

    pblpbl

    Joined:
    Sep 1, 2019
    Posts:
    15
    I tried this and it works well, but now pressing the space bar gives me the following warning:

    FocusController has unprocessed focus events. Clearing.
    UnityEngine.UIElements.UIElementsRuntimeUtilityNative:UpdateRuntimePanels () (at /Users/bokken/buildslave/unity/build/Modules/UIElementsNative/UIElementsUtility.bindings.cs:26)

    Is there a way to properly unregister everything before loading a new scene? For reference, my code is here:

    Code (CSharp):
    1.     void LoadMenu(MenuState menu) {
    2.  
    3.         switch(menu) {
    4.             case MenuState.Singleplayer:
    5.             Button level1 = doc.rootVisualElement.Q<Button>("level1-button");
    6.             level1.clicked += () => {
    7.                 SceneManager.LoadScene("Level1");
    8.                 doc.visualTreeAsset = null;
    9.             };
    10.             break;
    11.         }
    12.     }
     
  19. poprev-3d

    poprev-3d

    Joined:
    Apr 12, 2019
    Posts:
    72
    @SimonDufour This feature would be extremely useful when re-using VisualElement elements in a context such as ListViews (for obvious performance reasons). It's quite painful to store all callbacks to remove those each time when updating the ListView.
     
  20. LarsLundh

    LarsLundh

    Joined:
    Sep 6, 2022
    Posts:
    22
    Hello, is this a stupid thing to do?
    Will the fact that the button is about to be destroyed mean it is unnecessary to unregister the action?

    upload_2022-11-17_11-6-29.png
     
  21. sergmolot

    sergmolot

    Joined:
    Nov 21, 2016
    Posts:
    9
    Sorry, but this is kind of crazy.
    Simple example:
    upload_2023-2-23_1-53-18.png

    I need to send a group to a selected object when a button is clicked. In this case, each time the object can change. And in order to reset the previous event, I need to save the previous object into a variable and practically duplicate the function. But it can be very big. Is it convenient for you, or is it right?

    upload_2023-2-23_1-53-29.png
     

    Attached Files:

  22. tylerw-savatronix

    tylerw-savatronix

    Joined:
    Nov 10, 2013
    Posts:
    90
    My first question would be why do you need to unsub that at all? mog is presumably a variable whose value should be able to be changed as needed. I'm more versed in pure c# events, but is Unity doing something weird here?

    To more directly answer your question though, don't use anonymous delegates directly when you want to unsub. Make it a full fledged delegate and pass that to +=/-=

    Anonymous delegates can't really be unsubbed like that because you're actually trying to unsub to a new anonymous delegate, not the original one.

    When he mentioned "reference" he didn't mean the object you're sending to SendResearchGroup, he meant the actual delegate - the lambda expression you're using () => ...
     
  23. sergmolot

    sergmolot

    Joined:
    Nov 21, 2016
    Posts:
    9
    You need to unsubscribe because not a variable is passed to the delegate, but its value at the moment. And to unsubscribe, you need to store a dictionary with all the functions that have been assigned to each button in the list.

    upload_2023-2-23_13-57-37.png

    This option, of course, is shorter and prettier than the previous one, but still this one looks simpler.
    upload_2023-2-23_13-58-45.png
     
  24. SimonDufour

    SimonDufour

    Unity Technologies

    Joined:
    Jun 30, 2020
    Posts:
    574
    If the object is being destroyed, it means that all the object on which it was subscribed are already destroyed, so yes it is not really useful to unregister at that moment. Ideally, you would unsubscribe your callbacks as soon as you don't think they are relevant to facilitate the GC of the objects. The detach from panel event is a better place to unsubscribe, as it make no sense to update a UI that is not being displayed, but if it could be sooner, the better.


    It seems like your code would greatly benefit from having an interface between the UI and the logic... The button would have a reference to the "manager" class and would trigger an action on it, but the manager class will be responsible for remembering the selected group/knowing the currently active group. I personally like the M-V-VM pattern where you have a game state class, a ui, and the glue logic in between. Like tylerw-savatronix is saying, you need to keep a reference to the lambda, and not recreate the lambda with the same parameters to try to unsubscribe it as it will be a different object...
     
    LarsLundh likes this.