Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Feedback VisualElement lacking equivalent to MonoBehaviour Messages

Discussion in 'UI Toolkit' started by rtm223, Mar 26, 2021.

  1. rtm223

    rtm223

    Joined:
    Apr 12, 2017
    Posts:
    28
    I'm sure this has been asked before, but could not see anything in the search.

    Editing here to say that I'm not convinced by some of my original statements in the OP, but the discussion further down feels like it reveals that there is value in some enhancements to the event system to add events that are valuable for runtime UI, but maybe have limited use for Editor UI.

    Original Text:

    As far as I can tell with
    VisualElements
    , there is currently no system equivalent to the Messages that exist for
    MonoBehaviours 
    (
    Start
    ,
    OnEnable
    ,
    OnDestroy 
    etc.), or the overridable methods that exist for
    UIBehaviours 
    (
    OnRectTransformDimensionsChange
    , etc.). This is probably fine for Editor UI, but is extremely limiting for Runtime UI, and greatly reduces the ability to create modular UI widgets

    I would not expect the same events as MonoBehaviours, but absolutely would expect the following kinds of callbacks:
    • Awake / Start / Init - something after the XMLFactory.Init(), so gathering of references to other Visual Elements is possible. This I would consider essential for modularity
    • WillBecomeVisible / Invisible (usage equvalent to OnEnable / OnDisable in monobehaviours), again, pretty valuable for modularity
    • Update - This would be handy, but for retained mode UI, probably not all that sensible, however:
      • PreRepaint - called prior to the document repainting if the element will be redrawn is extremely valuable
    • OnDestroy or OnRemovedFromHierarchy (particularly useful if you use delegate-binding to drive UI as delegates cannot be unbound in destructors)
    Some of these might be handled by the EventSystem (adding / removing from panel), though that seems to be very much about Input Handling and Editor Inspectors at the moment. But maybe some more general-purpose utility events could be added?
     
    Last edited: Mar 30, 2021
  2. SudoCat

    SudoCat

    Joined:
    Feb 19, 2013
    Posts:
    45
    There are various ways to achieve what you're looking for.

    Awake/Start/Init
    Either using constructors, or using events (AttachToPanelEvent).

    WillBecomeVisible/Invisible
    Depends on what you're intending this for; Either custom events when changing style, or using the AttachToPanelEvent and DetachFromPanelEvent

    Update

    As you pointed out, this probably isn't a great idea, but you could just emit a custom event from a MonoBehaviour if so desired. the GeometryChangedEvent may also be what you're looking for in some scenarios.

    OnDestroy/OnRemovedFromHierarcy
    Either use a destructor, or use DetachFromPanelEvent, depending on your specific needs.

    In some instances of using VisualElements managed by Unity Objects, I just dispatch an event from OnEnable/OnDisable or other unity event functions to handle specific scenarios (for instance, cleaning up when an editor window is closed).

    For a (mostly) full list of built-in events, see here:
    https://docs.unity3d.com/2021.1/Documentation/Manual/UIE-Events-Reference.html

    Hope that helps!
     
    seobyeongky and rtm223 like this.
  3. rtm223

    rtm223

    Joined:
    Apr 12, 2017
    Posts:
    28
    Thanks for the response! I figured some of this might be in the event system. Couple of questions, if you know the answers offhand, otherwise I'll try digging in next week.

    Does the inital AttachToPanelEvent happen after the entire hierarchy is there, or is it going to be triggered on each element immediately as it is attached?

    I guess I'm most interested in "Display" style changing - there's so may cases where I would previously do stuff in a UI widget OnEnable or OnDisable. I'm guessing that won't trigger a Attach/Detach event? Could you point me in the direction of how a style change event would work - the API and reference for events is a bit vague beyond the obvious user input events. There's a ChangeEvent<T0> that has no practical usage info, so I'm not sure if that's the one I would want?



    Yeah, I can see that being an approach, but I've seen what happens with a moderately complex ingame UI that tries to manage ALL of the functionality at the window level and things tend to become a mess of code. That's why I'm attempting to push some responsibility into custom VisualElements, for the sake of modularity.

    I'm definitely going to look a little further into this next week, but even if some of this is possible it feels very much like workarounds, so if anyone from Unity is reading, I'd still give the feedback that maybe the Event system is suitable for Editor UI but might want some beefing up for in-game things?
     
  4. SudoCat

    SudoCat

    Joined:
    Feb 19, 2013
    Posts:
    45
    Just checked, it'll be each element as it's attached, and it doesn't bubble/trickle. If you need to know when everything is ready, you would likely need to add handlers to each one, then call something on your parent, or emit an additional custom event the parent could listen to.

    No, it _may_ emit a GeometryChangedEvent though, but I haven't checked. At the very least, the parent would receive a GeometryChangedEvent, changing the display type would trigger a relayout.

    It would depend a lot on how you're handling the show and hide; If you're doing it via code, then as you change the style, you could just emit a custom event.

    Something like:

    Code (CSharp):
    1.  
    2. public class ParentElement : VisualElement
    3. {
    4.   ParentElement()
    5.   {
    6.     var child = new ChildElement();
    7.     Add(child);
    8.     RegisterCallback<VisibilityChangedEvent>((evt) => { Debug.Log("do something"); });
    9.   }
    10. }
    11.  
    12. public class ChildElement : VisualElement
    13. {
    14.   public void ToggleVisibility(bool visible)
    15.   {
    16.     style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
    17.     using (var evt = VisibilityChangedEvent.GetPooled())
    18.     {
    19.       evt.target = this;
    20.       evt.target.SendEvent(evt);
    21.     }
    22.   }
    23. }
    24.  
    25. public class VisibilityChangedEvent : EventBase<VisibilityChangedEvent>
    26. {
    27.    // more useful stuff here...
    28. }
    29.  
    This is a super minimal example, but hopefully points you in the right direction to achieve what you want.

    True, but events can be made to bubble/trickle through elements, so each element can handle it's own cleanup, listening to one event.

    Code (CSharp):
    1.  
    2. public class DisableEvent : EventBase<DisableEvent>
    3. {
    4.     public DisableEvent()
    5.     {
    6.         LocalInit();
    7.     }
    8.  
    9.     protected override void Init()
    10.     {
    11.         base.Init();
    12.         LocalInit();
    13.     }
    14.  
    15.     private void LocalInit()
    16.     {
    17.       bubbles = true;
    18.       tricklesDown = true;
    19.     }
    20. }
    21.  
    22. public class UICanvas : MonoBehaviour
    23. {
    24.   private VisualElement _element;
    25.   OnDisable()
    26.   {
    27.     using (var evt = DisableEvent.GetPooled())
    28.     {
    29.       evt.target = _element;
    30.       evt.target.SendEvent(evt);
    31.     }
    32.   }
    33. }
    34.  
    Then in each element that needs it, you can register a callback for the disabled event, and handle the cleanup independently.
     
    Last edited: Mar 26, 2021
    rtm223 likes this.
  5. rtm223

    rtm223

    Joined:
    Apr 12, 2017
    Posts:
    28
    Alright, thanks It's well past working time on a Friday for me so I won't be looking at this until Monday, but thanks a lot for taking the time to help and dropping the code snippets.

    Cheers,
     
    SudoCat likes this.
  6. Midiphony-panda

    Midiphony-panda

    Joined:
    Feb 10, 2020
    Posts:
    234
    Not fond of having to handle custom events for Displayed/NotDisplayed or Visible/NotVisible switch.

    On my side, I had some success relying on GeometryChangedEvents to monitor changes on style.display property :
    Code (CSharp):
    1.         private void OnGeometryChanged(GeometryChangedEvent evt)
    2.         {
    3.             Debug.Log("[OnGeometryChanged] Resolved style : " + this.resolvedStyle.display);
    4.             Debug.Log("[OnGeometryChanged] Previous display : " + this._previousDisplay);
    5.            
    6.             // Do things if (_previousDisplay != this.resolvedStyle.display)
    7.            
    8.             _previousDisplay = this.resolvedStyle.display;
    9.         }
    10.  
    It works if the display property of this exact element is modified.

    But it doesn't work, for example, if we change the display of a parent element : resolvedStyle.display is always true, even if the GeometryChangedEvents seem to be triggered at the appropriate moments.
     
    SudoCat likes this.
  7. Midiphony-panda

    Midiphony-panda

    Joined:
    Feb 10, 2020
    Posts:
    234
    Just found this :
    https://forum.unity.com/threads/how...-our-uxml-uss-at-runtime.872242/#post-5760451

    According to @antoine-unity , you could write something like :
    Code (CSharp):
    1. private void OnGeometryChanged(GeometryChangedEvent evt)
    2. {
    3.          if (evt.newRect == Rect.zero)
    4.          {
    5.              // "Likely", DisplayStyle was set to None
    6.          }
    7.          else if(evt.oldRect == Rect.zero)
    8.          {
    9.              // "Likely", DisplayStyle was set to None and is back to Flex
    10.          }
    11. }
     
    rtm223 likes this.
  8. rtm223

    rtm223

    Joined:
    Apr 12, 2017
    Posts:
    28
    Agreed.

    Thanks for this tip - this seems like a decent stopgap. But it does feel that the "likely" is doing an awful lot of work there and really just highlighting the issue: if we're having to hack around to infer maybe some indication of what the engine probably is doing (except in undefined edge cases) it might be better if the engine just told us?

    Going back to my original post - I don't think OnDestroy actually has an sane equivalent for VisualElements, but still think the following are essential events for reliable behaviours that should be added at the engine level:
    • Awake/Start (after all hierarchy is in place but before the first draw. Also called for new elements that are spawned via code.)
    • OnDisplayChanged
    • WillRepaint (for elements that are dirty and will be repainted, probably at the very beginning of the GUI rendering phase of the execution order

    Edit: Addiitonal note. UIToolkit has no concept of Editor vs Runtime for the events, which makes this stuff extremely valuable. We can't even use things like EditorApplication.IsPlaying() as that will still return true in UIBuilder, so we have to resort to lots of defensive coding, null pointer checks, etc, on top of the hacky workarounds. Again, I think this comes back to the Runtime portion being very new and just still needing some more features
     
    Last edited: Mar 29, 2021
    Midiphony-panda likes this.
  9. SudoCat

    SudoCat

    Joined:
    Feb 19, 2013
    Posts:
    45
    I wouldn't hold your hopes up for getting MonoBehaviour events into UI Toolkit. From what I can gather, I think they want to keep this new UI system reasonably isolated from the traditional GameObject/Component model, likely to better future integrations with the new DOTS/ECS style approach (not that I'm very familiar with any of that). Furthermore, it's fairly contrary to the ideal of simulating web-style retained UI models.

    Personally, implementing custom specific methods and emitting events feels pretty safe and natural to me, however I do hail from the lands of front end web development, so I'm pretty used to these sorts of constraints. I've seen plenty of web frameworks come and go that have attempted implementing lifecycle methods, which are usually abandoned in favour of either events or handling checks on re-render.

    Listening to Geometry changed events can work well in some cases, but it feels frankly quite flimsy for a lot of situations, and doesn't give you much information as to the cause of the event, unless you do further checks against your current document state to determine what changed, which can also be expensive if there are frequent changes; which would become more likely as animation gets thrown into the mix. I find explicit methods and custom events generate much clearer, safer solutions for working with this web UI imitation. Of course, I don't know precisely what you're aiming for, and I've mostly used UI Toolkit for editor-only purposes thus far.
     
  10. rtm223

    rtm223

    Joined:
    Apr 12, 2017
    Posts:
    28
    I'm not suggesting getting the same MonoBehaviour messages into the UI, I'm talking about the fact that these messages exist for good reason in game systems and get used extensively in uGUI (Which was also retained) too. They shouldn't be the same, but adding events for important changes is just a sensible thing to do.

    As you say, listening for geometry changed is very flimsy but so is the visibility change event example you posted. For your example, everything that listens to visibility change also needs to track it's own visibility internally, in case multiple ancestors get hidden. Then of course you also need to track reparenting changes, and maybe try to infer new values after that happens which you probably can't actually do reliably and then you're just playing whack-a-mole with edge-cases, because it's hacking together a solution to the problem without the right information. That's not "cleaner and safer" than having a reliable event from the engine and suggesting that it does makes me wonder if maybe we are misunderstanding each other?

    Edit - I've also noticed that the above example for a custom visibility event literally doesn't do the desired behaviour. As in, when you set the element not visible, it will tell all the ancestors that one of their children is not visible any more, but not tell the descendents. So it's basically useless for the purpose - it's elements further down the heirarchy that need to get that event, and the event system does not do that.

    As for not providing useful callbacks because they want this system to be more like ECS... That's... just odd. Because this isn't ECS, it's not data oriented. It's completely incompatible at the level of VisualElements, unless one or both of those systems completely change their underlying principles.

    I'm also honestly not seeing the idea that GeometryChanged or Attach/Detach are "good, acceptable events" and "Display/NotDisplay are "bad, unacceptable events" (or "contrary" as you put it) for this style of GUI. And it's also very important to remember that even if the inspiration is from Web UI, this is not, actually, a tool for the Web.

    So I 100% agree that they shouldn't try to mimic MonoBehaviours just for the hell of it. But there is an event system here and expanding to provide key events that are valuable for creating in-game UI is a good thing. And if those events represent engine-level data, then it's a million times better that those are provided by the engine, than by hacky, unreliable, custom user events.
     
    Last edited: Mar 30, 2021
    SudoCat and Midiphony-panda like this.