Search Unity

Question Any plans for a Begin/EndProperty equivalent?

Discussion in 'UI Toolkit' started by oscarAbraham, Aug 26, 2022.

  1. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    One of the things I'm missing the most when making Editors with UI Toolkit is BeginProperty and EndProperty. It's needed for correct behavior of Prefab Overrides and Localized fields, as it shows the blue/yellow indicators, and adds the proper context menus.

    I know these cases can be covered by binding fields but that's not enough sometimes. It's specially needed when doing complex stuff without property fields in custom editors, or when property drawers have some nested stuff that's not very simple. Also, sometimes one needs to do their own custom binding for fields (i.e. for custom Undo/Redo behavior), and it'd be nice to still be able to mark those fields as prefab overrides.

    I think this is even more needed now that there's a push to convert stuff in the Editor from IMGUI to UI Toolkit. In 2022.2, there's more reason than ever to convert all my custom property drawers to UITK, but some of them are very hard to make work because of this missing feature. I've found some very tricky ways around it using bound foldouts, but it's not a nice solution.

    ***
    On a related note, it'd be nice if the property menu added by Unity used ContextualMenuPopulateEvent. Right now it uses PointerDown or MouseDownEvent, so we can't add menu items to it.

    EDIT
    My suggestion for implementing this would be an extension method for VisualElements that marks an element as a SerializedProperty container, like this:
    Code (CSharp):
    1. BindingExtensions.MarkAsPropertyContainer(this VisualElement ve, SerializedProperty prop);
    I'll be fine if it has to be done with a special VisualElement subclass, though.
     
    Last edited: Aug 26, 2022
    Xarbrough likes this.
  2. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    Would you mind providing an example of a case where PropertyDrawers do not work? Not saying there are no such cases, but it would help me be more specific in my answer.

    One of the difficulties is that the entire concept of Begin/EndProperty doesn't translate well to a retained-mode UI like UI Toolkit. We need something like an in-place type definition for a custom PropertyDrawer, inside the custom Editor class. It's not impossible, it just can't be copied 1:1 from IMGUI. So knowing more about specific use cases would help us prioritize this feature better.

    If you don't mind, can you report this as a bug?
     
  3. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    : ) Of course. I think a good example for it is stuff similar to UnityEvents. In UnityEvents you can right click on the Event header to manage overrides for the whole event, you also get a blue indicator next to the header when the event contains any overrides. The fields for the listeners still get their own override indicators and menus.

    Here's an example of mine that's very similar to the UnityEvent case. In my custom messaging system, there are Message Transmitters where one can assign a message ScriptableObject, then select a target to receive that message, and assign the appropriate values to be sent with the message. I have a header override indicator that represents the whole Message Transmitter, and then there are separate indicators for each of the relevant parameters:
    Capture.PNG

    The mere need of using PropertyDrawers would be limiting sometimes. It's common to do custom editors where fields are drawn directly without a PropertyField; using property drawers in those cases would add a lot of complexity and classes.

    Maybe it doesn't need to translate all that Begin/EndProperty does. The mixed value functionality maybe doesn't make a lot of sense. Personally, I'd be happy with just a way for adding a property menu and the respective prefab/localization indicators to a VisualElement that's not an INotifyValueChanged. It kind of already works by binding a foldout to the parent property, but the foldout functionallity gets in the way.

    Sure. I'll make some time tomorrow to make a reproducible case.

    Thanks for looking at this : ). I hope you have a nice day.

    EDIT
    I should add, in my Transmitter example, those three nested fields are actually structs that have their own multiple nested fields. By using Begin/EndProperty, I can treat them whole as a single overridable property in IMGUI; the right click menu shows items for the whole struct, instead of just showing menu items for an individual bound field like it would in UI Toolkit.

    EDIT 2
    The example I showed was in IMGUI. I'm realizing now that a UITK drawer might look the same, but the override indicator and the context menu in the header would be just for the object field inside it.

    Maybe it wasn't the best example...
     
    Last edited: Aug 26, 2022
  4. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    Sorry for the double post, here's another example, in case it helps. I have a plain serializable class called DataInput; if you ever watched RyanHipple's ScriptableObject talk, it build ups on his value reference idea. It basically has multiple modes to assign a value either from a ScriptableObject, from a direct field in the inspector, or from other places. In the UI, it looks like a single field with an extra button to choose the mode for assigning the value, but inside it contains multiple different fields. So the same DataInput field could look like either of these two different images depending on its mode:

    fieldExample1.PNG fieldExample2.PNG

    I want to be able to copy all the nested data inside the parent DataInput field from its menu. I also need to show an override marker even if the overridden field is hidden, and I want to be able to revert and apply that override from the context menu. It makes sense because one thinks of this group of fields as a single unit. Plus, otherwise I'd have some bad UX problems; e.g. if a single nested field is hidden and overridden, its component will say it contains prefab overrides, but I won't be able to see where is that overridden field.

    In IMGUI I can do it by using Begin/EndProperty with the parent DataInput property to surround the whole drawer. I can even use Begin/EndProperty with the parent property just on the little > button on the right, that way I can access the menu for the whole field by right-clicking that button, while being able to access the nested field's menu from its label.
     
    Last edited: Aug 27, 2022
  5. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    I've reported it now. It's got the IN-15128 id. Please let me know if you need any additional information. :)


    EDIT
    Corrected the report id
     
    Last edited: Aug 29, 2022
  6. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    The thing is, without the INotifyValueChanged (ie. value { get, set }), what does it mean for this VisualElement to have the blue bar of "prefab override"? Even for the Foldout case, the Foldout itself only has blue bars because of INotifyValueChanged elements inside.
    Apart from bugs, if you put all of this logic inside a custom property drawer (which in turn is instantiated by a PropertyField), wouldn't you be able to do all this inside while gaining the right-click menu and prefab override support for the whole control?

    I might be missing something still (very likely), but your examples seem like exactly what custom PropertyDrawers do best. I understand it's a bit more code than doing something more "inline" in the custom Inspector (like Begin/EndProp), but it's also a good forcing function to split UI code a bit more.

    Just to be clear, I still agree there's a need for something equivalent to Begin/EndProperty in UITK. In the meantime, I'm trying to dig a good workaround for you and ya, make sure there's no hard blocker in UITK for your use cases.

    Thanks! Will do.
     
  7. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    It would mean that the element represents a composite property and that its nested properties are overridden. There are multiple cases in IMGUI where that characteristic is used, even in Unity's own types (i.e. UnityEvent's header).
    Those are not the blue bars that I was talking about. The Foldout gets a blue bar that's independent of any other INotifyValueChanged elements inside. A Foldout can be assigned a bindingPath, and then it's bound to the isExpanded of a SerializedProperty; that makes it the only element that can currently be bound to a custom composite property. Since the blue bars and the property's context menu currently can only be assigned through the binding system, it means it's currently the only way to have blue bars and menu for a composite property.
    At least in 2022.2 beta and 2021.3, PropertyDrawers don't offer that capability. Only INotifyValueChanged fields get prefab and context menu support for their bound property. If you added that capability, it could solve some of these issues, but, in my opinion, it wouldn't be the best way to do it, as it'd be very limiting and it could make it impossible to have feature parity with IMGUI.

    I'm thinking that I should use UnityEvents to make an example. Imagine a prefab asset that has a UnityEvent like this:
    Screenshot 2022-09-11 130246.png

    Then, in a prefab instance we decide to make the name "ham" instead of "ha" when the event is fired. The first image is how it looks in IMGUI, the second image is how it looks in UI Toolkit:
    Screenshot 2022-09-11 130611.png Screenshot 2022-09-11 130653.png

    IMGUI allows us to handle overrides for the whole event in the Event's header. That means if we have multiple overridden fields in the event, even if those that aren't shown currently in the UI, we can handle them together. It also means that we can right click in the header and copy/paste the whole event. Finally, having a dedicated area to handle the property as a whole means that we can still copy/paste and handle the overrides of individual fields inside the listeners.

    In UITK we get none of that functionality in the header. We can only handle the override inside the individual fields (the string field that says "ham" in this case). Actually, we can't even do that because the string field doesn't have a label, and UI Toolkit fields currently require a label to show the property's menu, but that's a separate issue.

    It's specially ugly when that string field gets hidden. Imagine that now we change the GameObject.name popup to GameObject.isStatic. That would hide the string field. Then, imagine we apply the change from name to isStatic to the prefab asset. The string field that is hidden is still overridden, and the prefab instance says that it contains an overridden field, but without the functionality of IMGUI, we don't have an idea of where is the overridden field nor a way to revert it. In UITK the event field would appear to contain no overrides, while in IMGUI the hidden override is handled in the header like this:
    Screenshot 2022-09-11 132631.png

    That functionality in the IMGUI header gets even more useful when more listeners are added. I'm pretty sure it's implemented by having a BeginProperty/EndProperty block surrounding the header. If the whole property drawer had that functionality, we'd lose the individual markers on the individual fields that are overridden, and it'd be very confusing to know which property is supposed to be copied or pasted from the right click menu in any given place.

    Finally, if we wanted to fix Unity's UI Toolkit drawer for UnityEvents, to add that functionality in the header, we could do it by replacing the "On Click ()" Label with an empty Foldout. We'd bind that Foldout to the whole UnityEvent. Then we'd use USS to hide the Foldout's arrow and make it look like a simple label. It's kind of ugly and complicated.

    If we had the ability of a Foldout to bind to any composite property without all the other foldout functionality, things would be a lot easier. Ideally, it would work for any VisualElement. I know that VisualElement's have an internal bag where they can store all kinds of data, it's used for things like TrackPropertyValue, so I imagine it could also be used for this functionality. But even if it was implemented with a separate Bindable element without INotifyValueChanged, it could solve this issue completely.

    EDIT
    I apologize if I'm still not explaining this well enough. Please tell me if there's anything I could make clearer. This issue is important to me. If you think there's another type of content or another form of communication that would help, I'd be happy to try to provide it.
     
    Last edited: Sep 11, 2022
    Pier-Luc_Artisan likes this.
  8. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
  9. Ruchir

    Ruchir

    Joined:
    May 26, 2015
    Posts:
    934
    I have a use case as well,
    upload_2023-5-3_8-9-40.png
    Here the "Grounded State" has a property drawer with other class objects inside them which just use the property field (and they work as expected) but as you can see I have the blue bar for the Factory(it's a class with multiple state data inside it) label but not for the grounded state when I change something inside the Data dropdown.

    So it's really a pain point for me to find which state data I have exactly changed as I can't really see any blue bars or bold labels in my case.
    upload_2023-5-3_8-12-34.png

    This is UILayout for property field:
    upload_2023-5-3_8-13-59.png
    .
    And, I have a lot of elements with similar problems this is just an example.

    Another example:
    upload_2023-5-3_8-15-0.png

    In this case, after I update the value of F I have to click on the float field itself to get the option to apply changes to the prefab.


    Any help is really appreciated.
     
    oscarAbraham likes this.
  10. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    I ended up making my own workaround for this. It's in my UITK Editor Aid package. The element that fixes it is called Property Container (it's code should work without the rest of the package, if you only want that). Please let me know know if you find any problems. :)

    I'd still love an official solution to handle prefabs and property menus properly, though.
     
  11. Ruchir

    Ruchir

    Joined:
    May 26, 2015
    Posts:
    934
    Could you show a small example of how to use the PropertyContainer with foldouts?
    Thanks a lot.
     
  12. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    You may not need it for foldouts. Foldouts are the one element that can be bound to any SerializedProperty in Unity, at least for now. So you can do something like this:
    Code (CSharp):
    1. // myCompositeProperty would be a SerializedProperty of your choosing
    2. myFoldout.bindingPath = myCompositeProperty.propertyPath;
    3. // Or just pass the path to the property if you know it:
    4. myFoldout.bindingPath = "pathToMyCompositeField";
    A nice thing of binding the foldout in this way is that it will remember it's closed/open state throughout the session.

    You can use a PropertyContainer like you would use a normal VisualElement. If you set its bindingPath property in the same way that the foldout's bindingPath is set in the previous example, it will show the prefab override blue bar and the right-click menu for that property.

    EDIT:
    In both cases (Foldouts and PropertyContainer) you can also use the attribute "binding-path" in UXML instead of using code.

    EDIT 2:
    In both cases, like with any other field or element with a bindingPath, the elements need to be actually bound for it to work. This happens automatically in custom inspectors, but not in custom EditorWindows. Hmmm... I don't know how much I should explain about this; let me know if you get stuck. :)
     
    Last edited: May 3, 2023
    Ruchir likes this.
  13. Ruchir

    Ruchir

    Joined:
    May 26, 2015
    Posts:
    934
    Yeah it works for now thanks a lot.

    But I still couldn't figure out how to use your PropertyContainer in my case.
    Can you show an example of it?
    I'll show you my setup:
    upload_2023-5-3_12-14-27.png

    Custom Drawer:


    upload_2023-5-3_12-15-1.png
     
  14. Ruchir

    Ruchir

    Joined:
    May 26, 2015
    Posts:
    934
    I tried doing this:
    upload_2023-5-3_12-16-5.png

    upload_2023-5-3_12-16-16.png

    But it didn't do anything for me
     
  15. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    I think I don't see anything incorrect. It may be hard to see its effects because, since all elements are in a single row, all their blue bars would appear in the same place.

    Can you right-click on the name label to see if you get a context menu for the whole SpringSetting? If you do, then it is working.

    If you don't, and you're sure the field contains prefab overrides, could you remove everything from inside the PropertyContainer and just put something inside it that takes physical space to see if it works? That would help us diagnose whether there's some weird interaction between the PropertyContainer and your elements, and we can try some more stuff to find out what.
     
    Last edited: May 3, 2023
    Ruchir likes this.
  16. billykater

    billykater

    Joined:
    Mar 12, 2011
    Posts:
    329
    I recently had the exact same problem of needing BeginProperty / EndProperty and oscar's PropertyContainer would only work partially on my unity version so I reverse engineered what unity is actually doing and came up with the following solution which unfortunately needs access to the internal BindingStyleHelpers which you either need to call via reflection/expressiontrees or by using one of the internalsvisibleto tricks (which is what I am using).

    Code (CSharp):
    1.  
    2. public static class BindingsHelper
    3. {
    4.     public static void RegisterPropertyFieldEvents(this VisualElement element, SerializedProperty property)
    5.     {
    6.         element.userData = property.Copy();
    7.  
    8.         var eventData = (element, property);
    9.  
    10.         element.RegisterCallback(AttachToPanelCallback, eventData);
    11.         element.RegisterCallback(StopContextClicksCallback);
    12.         element.RegisterCallback(BindingsStyleHelpers.RightClickFieldMenuEvent);
    13.        
    14.         element.TrackPropertyValue(property, UpdateState);
    15.        
    16.         void UpdateState(SerializedProperty obj)
    17.         {
    18.             BindingsStyleHelpers.UpdateElementStyle(element, property);
    19.         }
    20.     }
    21.  
    22.     static void StopContextClicksCallback(ContextClickEvent evt)
    23.     {
    24.         evt.StopImmediatePropagation();
    25.         evt.PreventDefault();
    26.     }
    27.  
    28.     static void AttachToPanelCallback(AttachToPanelEvent _, (VisualElement element, SerializedProperty property) data)
    29.     {
    30.         BindingsStyleHelpers.UpdateElementStyle(data.element, data.property);
    31.     }
    32. }
    33.  
    And the usage is just
    Code (CSharp):
    1. objectField.RegisterPropertyFieldEvents(property);
     
    oscarAbraham likes this.
  17. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    Hi, would you mind telling me what didn't work and in which version in case I can fix it?
     
  18. Ruchir

    Ruchir

    Joined:
    May 26, 2015
    Posts:
    934
    It doesn't work if I just add a simple label inside it either:
    upload_2023-5-3_18-11-31.png

    upload_2023-5-3_18-11-47.png
     
  19. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    Oh! Is this inside the UI Builder? It should be in an actual inspector or Editor Window so it can be bound, so it can work. Do this steps do anything different outside the UI Builder preview? Also, would you mind telling me what version of Unity is this?

    EDIT
    Oh. I see now that the first image isn't from the builder. Sorry. It's weird; it works for me in the latest 2021.3, and it's worked in some older 2022.2 before. I'll test in newer versions and report back. Thanks.
     
    Last edited: May 3, 2023
    Ruchir likes this.
  20. Ruchir

    Ruchir

    Joined:
    May 26, 2015
    Posts:
    934
    it's in the latest beta version 2023.1.0b14
     
  21. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    Thank you for going along with fixing this thing. It didn't work in newer Unity versions, but It's fixed now. :) You can update the package from UPM if you are using it. Or, if you just need the code from this class, here it is.

    If anyone is curious about what fixed it, here's a summary:
    The PropertyContainer uses a foldout internally to show override indicators and display a property's context menu. This is because foldouts are the only element that can do these things for composite properties, like arrays, or custom structs or classes. An alternative to using foldouts would be to use reflection, like billykater suggests; I didn't want to do that because I feel Unity's internal code for this is prone to change, as it's still lacking many features.

    Anyway, the first part of the problem is that, before Unity 2022.2, blue bars appeared along the whole height of a Foldout, but now they only appear in the header. I needed to adjust the internal style to both scenarios so that blue bars became visible again. The second part of the problem is that Unity now uses pointer events to show the context menu, I changed the Mouse Events to Pointer Events and now the menu works too. I hope one day they use a proper ContextualMenuPopulateEvent for this, though.

    And that's the gist of it, really. Well, I found out that Unity's new Properties package also has a PropertyContainer class, so watch out for that in 2023. I'll probably need to change the name of my class in a later major version of this package.

    Please tell me if you find a problem with it again :). I appreciate it.
    Regards.
     
    Ruchir likes this.
  22. Ruchir

    Ruchir

    Joined:
    May 26, 2015
    Posts:
    934
    It does work now.
    I think a good way for finding a child label(for making it bold) could be to see if the first child label has a particular class added to it.
     
    oscarAbraham likes this.
  23. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    Using a USS class is an interesting idea. Maybe I could add a class to the PropertyContainer instead. That way, users can style their child elements however they need when there's a prefab override.

    The main problem I have with implementing that feature is detecting that the override vars are being shown. I can't access the actual SerializedProperty without reflection, to see if it's being overridden. I noticed that visual elements with overrides get the "unity-binding--prefab-override" USS class, so I could use that. That still leaves the problem of when to check for the presence of that class, as the relevant callbacks are internal. One way I can think of doing this without reflection is to check for that class periodically with a scheduler.

    Hmm. Using a scheduler to check for a USS class is a bit ugly, but this PropertyContainer was never about an elegant implementation anyway. I'll try to add this later today and report back.
     
  24. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    Hi, just letting you know I've implemented support for this kind of thing now. Is in the newest package version. I added an example in the docs for making a label bold with USS. I ended up adding a USS class to the PropertyContainer instead of querying the PropertyContainer's children; it gives more control to users. I also added an onPrefabOverrideChanged event that can be used to set custom styles or other things from code.

    : ) Thank you for the feedback.
     
    Ruchir likes this.
  25. Ruchir

    Ruchir

    Joined:
    May 26, 2015
    Posts:
    934
    It does work now but I think you should mention in the example that the class name for override is
    Code (CSharp):
    1. .editor-aid-property-container--prefab-override .my-custom-label-class
    and not
    Code (CSharp):
    1. .editor-aid-property-container .my-custom-label-class
    Thanks for all the help btw. :)
     
  26. Ruchir

    Ruchir

    Joined:
    May 26, 2015
    Posts:
    934
    Also, 1 more thing, isn't there a way to hook up to see if the serialized value is changed instead of scheduling a check every 500ms?

    Also, you could add a simple check to see if we are in play mode, where prefab overrides don't really matter.

    These could boost performance in large cases.
     
  27. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    Thanks! I missed that one. I'll fix it.

    I can't think of a way of doing this without either using reflection, or removing support for UXML and rebinding. I like to avoid both of these things when possible; I think it makes the package usable for longer and in more cases without changing its code.

    Maybe I could use reflection to somehow hook into the callback Unity uses to check whether it needs to update prefab override styles. The thing is, Unity already does this by polling. The whole binding system works with polling really, so it may not be better performance-wise. I'd probably still consider doing this if I didn't need to use reflection, though, just for clarity and convenience.

    The other approach would be to obtain the SerializedProperty that is assigned to the element. I could do that, then I could track the property or the serialized object to know if it changed. I'm not sure if it detects changes to the override status when the value doesn't change, though. Or maybe I could use undoRedoPerformed and postProcessModifications to trigger a callback where I check if the property is overriden, as those events cover all the normal ways users can change values in the inspectors; I'm not sure postProcessModifications covers when the override status changes but the value doesn't, though. Even if some of this worked, I can't think of a good way of getting the SerializedProperty.

    I could get the SerializedProperty by receiving it in the constructor, that's what I do in some other elements. But then it wouldn't work with UXML, and it wouldn't work if the element is recycled and bound to another property. I'd rather avoid losing these capabilities if it's not necessary. Or I could use reflection to get it through Unity's internal binding events, but, again, I'd rather avoid reflection if there's a another way.

    :/ I also have the instinct that polling every 500ms is wrong. I think it's ugly. But I can't think of another way that fits better. I felt 500ms was low enough to not feel too sluggish; override statuses don't tend to change very often anyway. I could make it lower if it feels slow, though.

    It's an interesting idea. The thing is prefab variants can still have overrides during Play Mode, and I don't think I can know for sure whether the object is a variant without obtaining the element's SerializedProperty.


    I think this isn't too slow compared to what UITK already does. UITK's binding system already does multiple ClassListContains checks many times per second, I'm just adding one every 500ms. I think it's fine enough, but I'd be happy to look into optimizing it more if it turns out I'm wrong.

    Thank you for the code review! I appreciate the chance to talk about these ideas with someone :). I hope you have a nice day.
     
    Last edited: May 5, 2023
  28. Wilhelm_LAS

    Wilhelm_LAS

    Joined:
    Aug 18, 2020
    Posts:
    53
    Hello. I encountered the same problem with the Right-click field menu

    Could you help me with this? I just dont understand how this works:
    https://forum.unity.com/threads/property-right-click-field-menu-not-shows-up.1499564/

    I fixed it with your PropertyContainer. I hope they announce a fix for that!
     
    Last edited: Sep 30, 2023
    oscarAbraham likes this.