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

Question Change string format for bound properties?

Discussion in 'UI Toolkit' started by Kazko, Feb 25, 2023.

  1. Kazko

    Kazko

    Joined:
    Apr 2, 2014
    Posts:
    82
    Hi, I created a Label that binds to a float property. Works. The float represents a percentage (for example 0.542).
    Is there any way to somehow insert some process that would format the string to 54.2% ?

    I know how to do the format/conversion, I just can't seem to figure out if it is even possible to do it for the output of the binding. Thanks for any ideas!
     
  2. SimonDufour

    SimonDufour

    Unity Technologies

    Joined:
    Jun 30, 2020
    Posts:
    515
    As you are binding on a label, can you bind to a string property containing the exact string that you want?
     
  3. Kazko

    Kazko

    Joined:
    Apr 2, 2014
    Posts:
    82
    I suppose I could do that, although I was hoping for a solution on the view side. I don't need the string representation for anything else than the inspector. Runtime uses only the float value for calculations.

    Anyway, thanks for the reply.
     
  4. SimonDufour

    SimonDufour

    Unity Technologies

    Joined:
    Jun 30, 2020
    Posts:
    515
    This is a frequent usage, and is being considered in future features, but you will have to wait until unity 2024 for the better solution in the inspector.

    For the time being, you could create your own dummy percentage type (a struct containing a float) and do the corresponding property drawer that would convert it to text as needed.
     
    Kazko likes this.
  5. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    You could bind another hidden element to the float, it could even bee a FloatField with display set to none. Then, you could use RegisterValueChangeCallback on your hidden FloatField. That way you can know when the float value changes and update the Label's text with code.

    EDIT:
    If you're sure that your UI will never need to be unbound and rebound to another field or another Object, you could even use TrackPropertyValue on your label instead of the hidden field to get notified when the float value changes and be able to change the Label's text with code.
     
    Last edited: Feb 28, 2023
    Kazko likes this.
  6. Kazko

    Kazko

    Joined:
    Apr 2, 2014
    Posts:
    82
    Thanks for ideas! I am using register callback usually, but when I discovered BindProperty, I thought there could be more functionality. If I bind a label with some property and the label automatically displays the value, there has to be some string conversions under the hood. So I was hoping I could tap into that. Some simple override or method that allows changes to the output e.g. my percentage case, or enclosing the string with brackets, etc.

    I tried extending Label and found overridable .text property but it didn't work.

    BTW - are bindings more efficient than callbacks and tracking?

    PS: wasn't aware of the tracking, thanks for the tip, I'll definitely use it somewhere :)
     
    Last edited: Feb 28, 2023
  7. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    Bindings are related to callbacks but they are not subsitutes for each other. There's a whole section in Unity's manual about binding; I recommend reading it. Here's the gist of this stuff:

    Binding is a mechanism that creates a relationship between a UITK element and a C# field in an Object. Unity will update the UITK element when the C# field changes, and it will update the C# field when the UITK element changes.

    UITK elements that represent values have the ability to execute some callbacks when their value changes; this includes bindable UITK elements. Unity itself uses these callbacks to update a C# field when a UITK element that's bound to that field changes. You can also take advantage of this mechanism to know when a C# field changes.

    If you add a FloatField and bind it to a C# float field, you could use RegisterValueChangeCallback to get a notification when the C# float field changes. That's because when the C# field changes, Unity will change the value of the UITK FloatField, which will in turn execute the Value Change Callback you registered. Then, you could hide the FloatField by setting its display style to none. The idea is that you could use that callback to update the text of your Label in code; it'd be your responsibility to update the Label because the Label itself wouldn't be bound to the C# field.

    Alternatively, you can use TrackPropertyValue directly on your label. It also allows you to be notified when a C# field changes without having the Label's text being automatically updated by Unity. The difference is that Unity won't update automatically the C# field that is being tracked when the UI is reused and rebound to another Object or another field. This could happen when this UI is part of a ListView row, which reuses rows multiple times when scrolling, or in other advanced use cases.

    None of these options should have a meaningful difference performance-wise. YMMV, so remember to always look at the profiler if you are experiencing problems.

    A third option is to just make your own bindable element that displays your text. It's a bit more advanced, but it's not hard to do, and there's also a lot of info about this in the manual. Look, here's a basic implementation; just modify the last line in SetValueWithoutNotify to show your value however you want:


    Code (CSharp):
    1.     // Our FloatDisplay is a BindableElement so it can be bound like any other UITK field.
    2.     // It also implements INotifyValueChanged, which is needed for binding to work.
    3.     public class FloatDisplay : BindableElement, INotifyValueChanged<float>
    4.     {
    5.         // USS classes for easily styling this element.
    6.         // They're available through public fields in case someone needs them in code.
    7.         public static readonly string ussClassName = "float-display";
    8.         public static readonly string labelUssClassName = "float-display__label";
    9.  
    10.         private readonly Label m_Label;
    11.         private float m_Value;
    12.  
    13.         public FloatDisplay()
    14.         {
    15.             AddToClassList(ussClassName);
    16.  
    17.             m_Label = new Label();
    18.             m_Label.AddToClassList(labelUssClassName);
    19.             Add(m_Label);
    20.  
    21.             // This initializes the display.
    22.             SetValueWithoutNotify(0);
    23.         }
    24.  
    25.         // The next two members implement INotifyValueChanged. They're needed for
    26.         // Unity to be able to update our FloatDisplay when the C# field changes.
    27.         // You shouldn't need to use these members directly anywhere for this case.
    28.  
    29.         // The value property gets and sets the value represented by this element.
    30.         public float value
    31.         {
    32.             get => m_Value;
    33.             set
    34.             {
    35.                 if (m_Value != value)
    36.                 {
    37.                     if (panel != null)
    38.                     {
    39.                         // When the value changes, and our element is bound and is part of an
    40.                         // existing UI, Unity expects a ChangeEvent<float> to be sent.
    41.                         using (ChangeEvent<float> e = ChangeEvent<float>.GetPooled(m_Value, value))
    42.                         {
    43.                             e.target = this;
    44.                             SetValueWithoutNotify(value);
    45.                             SendEvent(e);
    46.                         }
    47.                     }
    48.                     else
    49.                     {
    50.                         SetValueWithoutNotify(value);
    51.                     }
    52.                 }
    53.             }
    54.         }
    55.  
    56.         // This method is like setting the value property, but without a ChangeEvent.
    57.         public void SetValueWithoutNotify(float value)
    58.         {
    59.             m_Value = value;
    60.             // Modify this last line to show the value in the label however you want.
    61.             m_Label.text = "m_Value as a percentage";
    62.         }
    63.  
    64.         // These next two nested classes add support for our element in UXML and UI Builder.
    65.  
    66.         public new class UxmlFactory : UxmlFactory<FloatDisplay, UxmlTraits> { }
    67.  
    68.         public new class UxmlTraits : BindableElement.UxmlTraits
    69.         {
    70.             // This disables adding child elements to our FloatDisplay in UI Builder.
    71.             public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
    72.             {
    73.                 get { yield break; }
    74.             }
    75.         }
    76.     }
     
    Last edited: Feb 28, 2023
    Kazko likes this.
  8. Kazko

    Kazko

    Joined:
    Apr 2, 2014
    Posts:
    82
    Thank you so much for the detailed answer. This text could be in the manual to better explain how the binding works. I guess when they say "synchronize", it really means the binding works both ways, but in fact I only got this thanks to your post. I thought the visual elements are only receivers of the change and was kinda confused by the without-notify method etc. Thanks for the code as well, I tried to do something similar and was quite close, but gave up.

    Anyway, really helpful!
     
  9. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    No problem :), I'm happy to help.

    After taking a quick look back at the documentation, I have to agree: there's a detailed description of how the flow of data from an Object to a UITK element works, but there's not a lot of information that describes how it works the other way around. There's not even information that explicitly says that it works the other way around. I guess didn't see it because I learned about these kinds of bindings before reading the documentation; maybe that kind of bias affected the people that write the docs too.

    I was thinking this code may be useful to you in some cases: ValueTracker. It's like an input field that can be bound to a SerializedProperty, but without any visible UI. It has an event that is called when a value changes for convenience. These days I use it very little because TrackPropertyValue exists, but I still find it useful sometimes for cases that need to be able to support a UI being reused and rebound.
     
    Kazko likes this.