Search Unity

Custom int slider with value field - correct data binding

Discussion in 'UI Toolkit' started by Tom-Atom, Oct 2, 2019.

  1. Tom-Atom

    Tom-Atom

    Joined:
    Jun 29, 2014
    Posts:
    153
    Hi! As slider in UIElements is missing value field, I wanted to create my own component with it. After long struggles I made it work. This is how it looks and code is in bottom of the post:

    SliderWithValue.png

    What I did is, that I derived this control from SliderInt class and added IntegerFiled. But I have these questions:

    1] to synchronize value between original slider control and integer field, I had to register value changed callback to integer field. When value of field is changed, I call setter of value property (which I overriden). In it I am setting value of slider (which is binded to object I am editing). When value is changed with slider knob, value setter sets also value of integer field.
    Is this way of synchronization correct? First I tried to copy bindingPath of slider into bindingPath of integer field in constructor, but it did not work (probably binding is set after construction and it would not also reflect changes in binding later). Is there any way how control can get notified when binding is set or changed?

    2] styles. How can I style integer field inside this control from C# code or from UXML (just inline, not USS)? I have access only to slider control, which is container for label, visual input (which conatins slider track and slider knob) and integer field. I found, I can style my integer field from USS if creating style like bellow, but I may want to adjust only one slider directly with style attribute in UXML (like different background color of field, etc.):

    SliderWithValue IntegerField {
    margin: 0px 5px;
    }


    Code for Int slider with value:
    Code (CSharp):
    1. using UnityEditor.UIElements;
    2. using UnityEngine.UIElements;
    3.  
    4. namespace MyElements {
    5.  
    6.     public class SliderWithValue : SliderInt {
    7.  
    8.         public new class UxmlFactory : UxmlFactory<SliderWithValue, UxmlTraits> { }
    9.  
    10.         private readonly IntegerField _integerElement;
    11.  
    12.         public override int value {
    13.             set {
    14.                 base.value = value;
    15.  
    16.                 if (_integerElement != null) {
    17.                     _integerElement.SetValueWithoutNotify(base.value);
    18.                 }
    19.             }
    20.         }
    21.  
    22.         // ---------------------------------------------------------
    23.         public SliderWithValue() : this(null, 0, 10) {
    24.  
    25.         }
    26.  
    27.         // ---------------------------------------------------------
    28.         public SliderWithValue(int start, int end, SliderDirection direction = SliderDirection.Horizontal, float pageSize = 0)
    29.             : this(null, start, end, direction, pageSize) {
    30.         }
    31.  
    32.         // ---------------------------------------------------------
    33.         public SliderWithValue(string label, int start = 0, int end = 10, SliderDirection direction = SliderDirection.Horizontal, float pageSize = 0)
    34.             : base(label, start, end, direction, pageSize) {
    35.  
    36.             _integerElement = new IntegerField();
    37.             _integerElement.style.width = 30;
    38.             _integerElement.style.flexGrow = 0;
    39.             _integerElement.RegisterValueChangedCallback(evt => {
    40.                 value = evt.newValue;
    41.             });
    42.  
    43.             Add(_integerElement);
    44.  
    45.             _integerElement.SetValueWithoutNotify(value);
    46.         }
    47.     }
    48. }
    49.  
     
  2. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    This sounds correct to me. Something that will help is the use of
    SetValueWithoutNotify()
    , which all controls have. This sets the value without sending ChangeEvents to everyone. In your case, use it when you update the IntegerField from a change to the SliderInt so you don't also get notified of a change to the IntegerField. Otherwise, it's correct to rely on ChangeEvents to sync the two fields. Don't use bindingPath to do this within this particular control.


    I'm not sure I fully understand your question here. I see you're already styling the IntegerField in C# in your code above. Just note that any style you set in C# (or inline in UXML) cannot be overriden by an external user of your control via USS (is this what you're asking?). I recommend moving your styles to a USS StyleSheet and assign your IntegerField a style class. If you don't have a global StyleSheet your use for all your custom controls, you can create a USS just for your SliderWithValue and assign it to yourself in the constructor.
     
  3. Tom-Atom

    Tom-Atom

    Joined:
    Jun 29, 2014
    Posts:
    153
    @uDamian first, thanks for answer!

    Yes, this is some basic style like flex-grow=0 to not resize integer field. I can style all IntegerFields in all instances like this:

    SliderWithValue IntegerField {
    margin: 0px 5px;
    }

    But, let's say I wan one field red and second green like this:
    ColoredSliders.png

    During day I found, I can give each slider specific name in UXML and style it in USS like this:

    #level-width IntegerField {
    background-color: rgb(50, 250, 70);
    }

    #level-height IntegerField {
    background-color: rgb(250, 50, 70);
    }


    I also found way how to get down to IntegerFiled inside component with code - I have simply to use
    mycomponentInstance.Q<IntegerField>();


    But I still do not know how to do it directly in UXML. It would be useful for some simple small adjustments, so I do not have to polute USS with it. Like this:
    <my:SliderWithValue label="Width" name="level-width" low-value="0" high-value="10" binding-path="_width" style="background-color: rgb(50, 250, 70);"/>

    Problem is it is styling whole control and I do not know how to get with style attribute one level down into IntegerField. This is result:
    WholeColoredSlider.png
    I would need something like:
    <my:SliderWithValue label="Width" name="level-width" low-value="0" high-value="10" binding-path="_width" IntegerField.style="background-color: rgb(50, 250, 70);"/>
     
  4. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    USS is not something you should avoid "polluting" with small adjustments. Small adjustments are one of the main reasons to use USS, specifically because it allows you to change styles of elements within elements you don't control.

    There is no way in UXML to set styles of child elements. All styles set in UXML on an element are inlined on that element and will actually prevent anyone from changing them in USS. For example, if you set your SliderWithValue background color in UXML, anyone that instantiates your UXML template will not be able to change that background color in their USS.

    Lastly, it's best to use style classes instead of element names (or ids). It's far more flexible and you can add as many of these classes (think of them as tags) to an element as you want. Here's how you could implement your green slider field above with classes:
    Code (CSharp):
    1. <my:SliderWithValue class="my-green-slider" />
    Code (CSharp):
    1. .my-green-slider IntegerField {
    2.     background-color: rgb(50, 250, 70);
    3. }
     
  5. Tom-Atom

    Tom-Atom

    Joined:
    Jun 29, 2014
    Posts:
    153
    Great! Thanks a lot!