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

UxmlTraits and Custom Attributes resetting in inspector

Discussion in 'UI Toolkit' started by Shinyclef, Sep 8, 2020.

  1. Shinyclef

    Shinyclef

    Joined:
    Nov 20, 2013
    Posts:
    502
    I'm attempting to create a custom control that extends the built in TextField, to add some additional functionality like placeholder text via an added label, and validation types. Both of these features use custom attributes.

    I have two issues and thus questions:
    1. UxmlTraits.Init is being called many times. Each time it gets called, I am adding the placeholder label. It seems that Init is getting called whenever I change the values of my custom attributes, causing multiple labels to be added, although it resolves itself back to one when I save.
    What is the correct way to add the child placeholder text label? Is Init() the wrong place?

    2. Everytime I hit save in the UI Builder after inputting values into my custom controls, the controls reset to default. Why?
    What is the correct way to create custom attributes?

    Here is my code below. Appreciate any help.

    Code (CSharp):
    1. using DigitalGame.Common.UI.Validators;
    2. using DigitalGame.Common.Util;
    3. using UnityEngine.UIElements;
    4.  
    5. namespace DigitalGame.Common.UI
    6. {
    7.     public class PlaceholderInputField : TextField
    8.     {
    9.         private Label placeholderLabel;
    10.         private string placeholderText;
    11.         private string invalidStyleClassName;
    12.         private IInputValidator validator;
    13.         private bool isValid;
    14.  
    15.         protected override void ExecuteDefaultActionAtTarget(EventBase evt)
    16.         {
    17.             base.ExecuteDefaultActionAtTarget(evt);
    18.             if (evt.eventTypeId == InputEvent.TypeId())
    19.             {
    20.                 string text = ((InputEvent)evt).newData;
    21.                 SetPlaceholderTextVisible(string.IsNullOrEmpty(text));
    22.                 ValidateInput(text);
    23.             }
    24.         }
    25.  
    26.         private void SetPlaceholderTextVisible(bool visible)
    27.         {
    28.             placeholderLabel.visible = visible;
    29.         }
    30.  
    31.         private void ValidateInput(string text)
    32.         {
    33.             if (validator == null)
    34.             {
    35.                 return;
    36.             }
    37.  
    38.             bool newValIsValid = validator.ValueIsValid(text);
    39.             if (isValid != newValIsValid)
    40.             {
    41.                 isValid = newValIsValid;
    42.                 if (isValid)
    43.                 {
    44.                     placeholderLabel.RemoveFromClassList(invalidStyleClassName);
    45.                 }
    46.                 else
    47.                 {
    48.                     placeholderLabel.AddToClassList(invalidStyleClassName);
    49.                 }
    50.             }
    51.         }
    52.  
    53.         public new class UxmlFactory : UxmlFactory<PlaceholderInputField, UxmlTraits> { }
    54.      
    55.         public new class UxmlTraits : TextField.UxmlTraits
    56.         {
    57.             private UxmlStringAttributeDescription placeholderText  = new UxmlStringAttributeDescription { name = "placeholder-text" };
    58.             private UxmlStringAttributeDescription invalidStyleClassName = new UxmlStringAttributeDescription { name = "inavalid-style-class-name" };
    59.             private UxmlEnumAttributeDescription<ValidationType> validationType = new UxmlEnumAttributeDescription<ValidationType> { name = "validation-type" };
    60.  
    61.             public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
    62.             {
    63.                 Debug.Log("Init!!");
    64.                 base.Init(ve, bag, cc);
    65.              
    66.                 var inputField = (PlaceholderInputField)ve;
    67.                 var placeholderLabel = new Label { pickingMode = PickingMode.Ignore, name = "Placeholder Text" };
    68.                 placeholderLabel.AddToClassList("InputFieldPlaceholder");
    69.                 inputField.Add(placeholderLabel);
    70.                 inputField.placeholderText = placeholderText.GetValueFromBag(bag, cc);
    71.                 placeholderLabel.text = inputField.placeholderText;
    72.                 inputField.placeholderLabel = placeholderLabel;
    73.  
    74.                 inputField.validator = InputValidatorFactory.CreateValidator(validationType.GetValueFromBag(bag, cc));
    75.                 inputField.isValid = true;
    76.  
    77.                 inputField.invalidStyleClassName = invalidStyleClassName.GetValueFromBag(bag, cc);
    78.             }
    79.         }
    80.     }
    81. }
     
  2. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    The original UXML custom attribute definition API (via UxmlTraits) was not designed to be invoked after initial element creation. This limitation is still present in the current UI Toolkit API. As such, the UI Builder has to be a little creative when it comes to letting you change attributes on an element in the UI Builder's Inspector.

    What the UI Builder does is it finds (via reflection) the Init() call on the current element and just calls it each time an attribute value has changed. This is why your Init() is called. It's by design. The fix is fairly simple: just check for evidence of a previous Init() call (your placeholder label already exists) and account for that (by not creating a new one).

    UI Builder needs some way to read the current value of your custom UXML attribute. You could be doing anything inside your Init() with the value coming from UXML so there's no way for the Builder to know what it has been already set to. So, for the same root cause as above, the UI Builder relies on the existence of C# attributes (ie. { get; }) to be defined on your custom element that have the same name as the corresponding attribute (same name as in, uxml uses dashes "my-value" and corresponding C# attribute uses no dashes "myValue", case insensitive).

    Here's a lot more details on this subject as well as an example custom element:
    https://forum.unity.com/threads/ui-builder-and-custom-elements.785129/#post-5225297
     
    ricvail and Shinyclef like this.
  3. Shinyclef

    Shinyclef

    Joined:
    Nov 20, 2013
    Posts:
    502
    Thank you for the reply. I will give these changes a try tonight.
     
  4. SonicBloomEric

    SonicBloomEric

    Joined:
    Sep 11, 2014
    Posts:
    1,085
    @uDamian Is this documented anywhere in the manual? I did a quick search and couldn't find any mention of them.

    Without documentation, how are users supposed to know how to properly hook into these systems? Also, were C# Attributes considered for supporting these? Something like [UxmlValue("my-value")] to make the association explicit and clear?
     
    RoyBarina, a436t4ataf and Etonix like this.
  5. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    This page covers this:
    https://docs.unity3d.com/2022.2/Documentation/Manual/UIE-create-custom-controls.html

    Many things have been considered and experimented with. And we continue to do that. We know it's a big miss but it's just that we've had other higher priorities to tend to.
     
    Last edited: Jul 29, 2022
  6. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
  7. SonicBloomEric

    SonicBloomEric

    Joined:
    Sep 11, 2014
    Posts:
    1,085
    Well, I see that the property naming convention is mentioned, sure. This "quirk", however, is not explained (from what I can tell):

    This is the entirety of what is stated about Init():
    There might be a hint to this with the following note from the document:
    But even that doesn't talk about getting called a bunch of times and guarding against it. It is focused on simply not having other side effects rather than ensuring that Init() is safely reentrant.
     
    ErnestSurys likes this.
  8. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    Yep, the current docs can definitely be improved. It would help if you filed a bug via the Editor for this (bugs can be docs issues). But I'll re-raise this internally.
     
  9. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    Actually, the best would be to add your suggestion in the provided feedback box on that doc page itself (at the bottom)
    upload_2022-8-4_10-0-28.png
     
  10. SonicBloomEric

    SonicBloomEric

    Joined:
    Sep 11, 2014
    Posts:
    1,085
    I guess... That feedback mechanism always feels like a black hole as there's no way to get confirmation that things have changed. At least, I don't recall there having been such in the past... Will try regardless.
     
    sebesdm likes this.