Introducing the Runtime Bindings API in Unity 2023.2

Starting with the 2023.2.a18 alpha release of Unity, we have added the runtime bindings feature to UI Toolkit. We have offered a solution to bind against serialized data for a long while now, however it was only available inside the editor and you could only bind a restricted set of data to the value property of a bindable control.

While porting the bindings system to be available at runtime as well, we decided to generalize it and to give you more control over how the bindings are registered and updated. This new system was designed with customizability and extensibility in mind in order to enable a myriad of use cases.

One use case that comes out of the box is the ability to create a link between your data and different parts of the UI. For example, provided you have the following data type:

public class MyDataSource
{
    [CreateProperty]
    public string Name { get; set; }
    [CreateProperty]
    public int Level { get; set; }
}

You can now create a binding between an instance of MyDataSource and your UI by doing:

var element = new VisualElement();

// Sets a data source that will be available to binding instances on this element and its children.
element.dataSource = new MyDataSource
{
    Name = "Peter",
    Level = 9001
};

var nameLabel = new Label();
element.Add(nameLabel);

// Create a one-way binding from the source to the "text" property of the label.
nameLabel.SetBinding(nameof(Label.text), new DataBinding
{
    dataSourcePath = new PropertyPath(nameof(MyDataSource.Name)),
    bindingMode = BindingMode.ToTarget
});

var levelField = new IntegerField();
element.Add(levelField);

// Create a two-way binding from the source to the "value" property of the integer field. Changes in the UI will be propagated back to the source.
levelField.SetBinding(nameof(IntegerField.value), new DataBinding
{
    dataSourcePath = new PropertyPath(nameof(MyDataSource.Level)),
    bindingMode = BindingMode.TwoWay
});

rootVisualElement.Add(element);

This example, while very simple, should help you get started quickly on using the new runtime binding system.

In order to keep this announcement post as short as possible, we have written a series of articles that will provide an in-depth guide into the new runtime bindings system:

These articles are meant to be live documents and will be updated in the weeks to come to provide more details and more insights.

We hope this feature will enable new use cases for you!

As always, we are open to feedback and questions. :slight_smile:

30 Likes

This looks cool!

Questions!

1: In part 3, you’re writing that

Isn’t using a value type as a data source kinda meaningless? It’s a value type, so we’de be passing the ui a copy of the value, so it won’t be possible to reflect any changes. So it’s essentially equivalent to an extremely expensive way of just setting the display value of the visual element. I don’t think the boxing is the biggest problem here!

2: Also in part 3:

I don’t quite understand that! Are you saying that the binding will work on subtypes of the type the property is on, or that the properties will accept subtypes for the bindings, or?

3: I don’t understand why we would ever want to be “Implementing both IDataSourceViewHashProvider and INotifyBindablePropertyChanged”. The example doesn’t make any sense to me - why would we want the system to calculate a hash when we know exactly when the data have changed and the UI needs to update?
Is the idea that when you combine the interfaces, the UI is only updated when you both invoke the PropertyChangedEventHandler and the hash code has changed?

2 Likes

Hi @Baste

Yes, you would pass in a copy. If your value type only contain value types or primitives, then you are correct, it won’t reflect any changes. However, if it contains reference types as field/properties, then it is still possible to have the changes be reflected (of course, as long as those references hold).

I’ve encountered a lot of handle-type structs that hide references to non-public classes. Usually, those structs don’t hold any data and acts only as a wrapper to the data. Those would still work.

We certainly don’t recommend to use value types as data sources, I’ll take a note to review this part. Thank you!

For this, I am saying that the binding will work on subtypes of the type of the property. So, in the property bag system, pretty much every is more akin to [SerializeReference] than to [SerializeField], if that makes sense. We’ll review that part as well.

So the use case here is admittedly a bit niche, but I’ve certainly used it in the past. If you have a lot of very volatile data, but you don’t want the UI to be constantly updated for performance reasons, you could delay it using the IDataSourceViewHashProvider with a custom made “hash” instead of reflecting the changes directly. That way, you get to decide when the UI is updated… and when it is updated, only the parts that have changed will be updated.

You are correct that the example doesn’t provide any value, we’ll update it.

Hope this helps,
Thank you.

4 Likes

UnityAction to Button binding still not available or am i stupid blind?

And yet another question about IDataSourceViewHashProvider, why not use IEquatable<>?

You can do this by creating a CustomBinding and un/setting the callbacks from the
OnDataSourceChanged method.

So the IDataSourceViewHashProvider interface is not about checking for equality. The idea is to tell the binding system when to update the bindings that are related to a specific data source. You can have some data that changes every frame without wanting the UI to be updated every frame.

We also needed something that we could store the result from for comparisons, IEquatable<> doesn’t really fit the bill.

The example provided where we implement both interfaces treats it as a “normal hash code”, which doesn’t really indicates what it can do. We’ll update it soon.

2 Likes

Uhh, wow - that’s a huge amount of boilerplate required over and over again to bind each individual value.

I’m scratching my head and perhaps asking a quite sad “How about no?”

The SetBinding calls could and should be automatic, taking all schema information from the structure, field visibility, and annotations in the code.

I guess the ship has sailed on suggesting a pure codegen solution, like Qt provides, where UI assets are transpiled into C# code, that is then compiled. (so UI changes lead to compile time errors, which beats finding them in your bug reports inbox the next week)

There’s merit to data driven UI. But then Unity’s solution has to be less verbose than these transpiled workflows, not more verbose.

Basic Idea
Something that could work in your Architecture right now, with just a little bit of codegen:

nameLabel.SetBinding(nameof(Label.text), ref myDataSource.name); //default parameter mode:BindingMode.TwoWay

(you might do the PropertyPath stuff inside the code of SetBinding, for example via a Type Parameter to SetBinding, especially with Roslyn by your side to annotate/populate MyDataSource appropriately, but it could be easier to do it in the getters and setters of MyDataSource.name)

Possibly it could some day boil down to something like (sorry I’m from the C++ UI framework world):

myDataSource.name = ref nameLabel.text;

(and automatic traversing mappers of this for larger trees of VisualElements and more complex data sources - however, this could force UI structure to mirror Code structure, which seldom works out)

There’s much more to be gained from a slightly different approach; copied this here from my thread on Unity.Properties:

Blue Sky Counterexample
Game UI often consists of small packs of data that go together, like start & end dates, bullets & clip capacity, wood & gold & stone, x and y values, first names and last names, emails and passwords, HP and mana, “all difficulty settings”, but the layouts these are shown in are often moved around a lot in the UI and its layout during editing, and oftentimes at runtime, too.

That means not the roots of paths, but instead the ends (suffixes) of property paths often stay relatively similar, e.g. if I move a DatePicker to a different parent element, it’s still a DatePicker there, with the same Year, Day, Month, properties, just a different path leading there from the root.

There are also plenty of infixes that can just be skipped over - e.g., the boxes around the gold price and the silver price in a price tag.

The UX designer doesn’t want approval from the developer want to name each of these roots, paths and infixes.

The developer doesn’t want to ask the name of a new path from the UX designer whenever an item moves.

They both just need an agreement that there’s a DatePicker somewhere, named “birthday”, and what the small-scale, local relative property names are. Unnamed elements intuitively count as empty infixes.

I believe a ideal world example could look kind of like this:
(XContainer and XYContainer here are what MyDataSource is in the original post)

partial struct XContainer //NB: absence of even [Serializable], use only if desired
{
   [CreateProperty(/*optionalpath*/)] public int X {get; set;}
}

//Just get and set X as you always would.
XContainer a = new {X=42};

//Generated code resolves the path, which you may override via optionalpath
//In the default case the path is derived from its name by codegen
//X declares its path, not an outside constant in another class.

In a data binding scenario, the user-facing boilerplate should be limited to the type declarations, property attributes, and single UXML elements.

//UXML declaration for the associate UI element
<IntegerField name="X" label="Answer" value="0" />

// All "X" of type int in all bound containers are bound to this IntegerField
// matching by longest common suffix. If you had a ParentContainer with
//    [CreateProperty]XContainer child{get;set;}
//    [CreateProperty]XContainer friend{get;set;}
// then "X" would match both child.X and friend.X
// while "child.X" and "friend.X" would only match the respective fields.

…and in C#-Land…

//2nd container is a surprise tool that will help us later
partial struct XYZContainer
{
   [CreateProperty] public int X {get; set;} = 69;
   [CreateProperty] public int Y {get; private set;} //one-way, can't be set by visitor (UI)
   [CreateProperty] public int Z {private get; set;} //one-way, write-only for visitor (UI)
}

//Binding some datas now! Finally. Ohhh baby!
XYZContainer ab = default;

//The Visitor would also be automatically generated, and used by the Binder
rootVisualElement.Bind(ref a, ref ab, ...); //NB: for most cases, you can bind directly to root!

//UI now shows 69, as bindings apply in param order
//Should you prefer to take the initial value from UXML or the element's
//state instead, imagine the appropriate overloads.

//User types "123" in UI
Debug.Log($"{a.x}=={ab.x}"}); // >"123==123"
//all bound containers receive the forward-propagated data from UI

//Code path executes this somewhere: ab.x = 9000+1;
Debug.Log($"{a.x}=={ab.x}"}); // >"9001==9001"
// use style/property on IntegerField in UXML to limit back-propagation if desired
// accessors and propagation always synchronous, rendering always asynchronous

Hi @Thygrrr !

Are you talking about calls such as this one?

nameLabel.SetBinding(nameof(Label.text), new DataBinding
{
    dataSourcePath = new PropertyPath(nameof(MyDataSource.Name)),
    bindingMode = BindingMode.ToTarget
});

If so, then there are reasons for each individual piece here:

  • The first parameter of the SetBinding method is the binding Id. In the case of a DataBinding, it is the targeted property. I used the nameof version in the example instead of a naked string, but a string would work or a wpf-like static property definition (i.e. Label.textProperty, which we have internally, but it missed the cutoff). In the case other binding types, you get to define your own ids.
  • The second parameter of the SetBinding method is the binding object itself. Now, the reason it is an object is because we support multiple types of bindings and not just data bindings. You could define a binding object that will add/remove uss classes on your element; you could define a binding object that will localize your content (which we will also provide in the localization package); you could define a binding object that will bind to a manipulator/logic/events, etc. If you only care about data bindings in your own code, you can easily create an extension method that will remove the boilerplate.
  • The parameters we construct the binding instances with are completely optional and depend on your setup and the type of binding you are using.

If you are using uxml files, you can also create most of your data bindings directly inside the UI Builder, which will allow you to add bindings without writing any code.

First, I want to address the code generation bit. Code generation can add to the compilation time and the domain reload time as well. This might be a cost that you are willing to pay, but we can’t generalize this sentiment. We’ve already added code generation (through source generators) to remove the boilerplate associated with an element’s factory/traits classes and we’ve added code generation for the property bags we used for data bindings. This has an impact on both the compilation and domain reload times. As such, they are both opt-in through the use of attributes.

We could add an on-demand code generation of a UI schema of a Uxml file from the UI Builder (and this is something that’s been experimented by some folks), but this would need to come with an integrated workflow, which is not in our priorities at the moment.

Second, we have decided to not to make SetBinding calls automatic based on the schema information because we found that rather limiting and, in our testing, resulted in writing more boilerplate. We instead decided to add ways to configure how the data bindings work so that you could re-use the same source in different contexts. We also decided that we should support different kinds of data sources. Some examples:

  • Want to use the exact same source for in-editor authoring/tooling (editable) and in-game display (not editable)? You can set the binding mode to be TwoWay on the tooling side and ToTarget for the game.
  • Want to process the changes from the UI in a command pattern? You can set the binding mode to ToTarget and register callbacks on the element to create commands (or define a custom binding to do it).
  • Want to have a dynamic data source where bindable properties are generated on the fly from one or many other pieces of data? You can create a property bag manually, gather the data and generate properties on the fly. One example of this is the EntityContainer in ECS where the Entity type is a handle and the component data is stored somewhere else. A manually crafted property bag was created to gather the different kinds of components an entity can have.

The idea behind these decisions is that we want to offer a solution that can used across a variety of use-cases and not force one in particular. That usually leads to more options, more ways to configure and/or more boilerplate. I personally don’t see that as a problem. We can build something simpler on top of the system if need be. In my experience, making something automatic by default has usually led to more problems down the road.

I’m not sure I’m following you here.

Sure, you can set any arbitrary C# object as your data source. You can set a data source on any given visual element inside your hierarchy and alternatively, you can set a data source on the binding object directly. There are plenty of options to ensure that designers would only need to care about leaf properties and ensure that moving elements around won’t break the bindings.

This is the case with the current system. If a visual element does not override the data source or the data source path, it is skipped.

Again, all doable with the current system.

The only caveat is that you can’t set arbitrary C# objects as data sources inside the UI Builder because we need to save the uxml files as assets and be able to reload them. So at the moment, we only support putting assets as the data source, but in code, there is no such limitation. For authoring purposes, you can set the targeted type of the source and you will have access to some form of auto-completion even though the source is not actually set.

Hope this helps!

2 Likes

Yes. That’s a lot of boilerplate. Am I alone in this feeling?

Let’s focus on the runtime (non UXML) API here, since that was the OP example.

Imagine hooking up 75 UI fields like that, scattered across a project. Maintaining all this.
Imagine passing a method on a Label the name of its own text property.

There are, if we’re generous, 38 characters of information in there (nameLabel,text,MyDataSource.Name,ToTarget), padded with 3x that in boilerplate. Signal-to-noise of 38:167.

Assume most of these defaults are usually the same, (LabelField usually wants to bind to text, and ToTarget is decent default) the SNR drops to 26:167.

These are worse **code-to-**boilerplate ratios than Hello World in Java. But Java’s ratio gets better when you add a second line of output; but in UI Toolkit, the ratio stays constant if you add a second binding.

Like I said, an ideal syntax goal for this iteration could be (coincidentally in ~38 characters of information):

nameLabel.text = (ref)anyObject.Name;

I don’t see how that kind of clear syntax and intent wouldn’t be possible, as the set accessor behind Label.text can surely do the necessary binding work. (even if just replicating, almost line by line, the boilerplate you were showing)

Or, if you prefer to make it an explicit method on the Label type itself, the same applies:

nameLabel.SetBinding(in anyObject.Name); //hey, also ~38 chars
//in, out, ref express binding mode much better than that enum
//a cool pattern from DOTS; but if too sus, this is also good:
nameLabel.SetBinding(ref anyObject.Name, BindingMode.ToTarget);

Why we as users would need to care about PropertyPaths and (a little less so) Binding IDs, I would never know. We also don’t want or need a specific DataSource object that wraps our values, we usually want to bind an extant object’s values, which the C# Language perfectly supports. No need to be declaring accompanying dataSourcePaths or bindingModes for each field when we avoid that forced facade pattern.

No DataSource facade means no sources to re-use to begin with. The data binding instead is already the second (or more) use for anyObject.

3 Likes

text is one of many properties that you can bind to for a Label. For example, you can also bind enableRichTextProperty, selectable, tooltip, one of the style properties and others. They can all be bound using the same syntax.

If you want a dedicated path for the text property of a Label, then you can easily define an extension method that will remove the boilerplate for you.

The binding mode is a property of the binding instance. You may still want to detect changes made from code to the Label.text property and not just from the UI. In the UI Builder, we’re looking into setting a different binding mode when you add a binding based on what you would most likely want.

We have access to two forms of code generation:

  • IL Post Processors, where you can re-write the IL after its been compiled.
  • Source Generators, where you can add new code to complement or to complete existing code.

In the case of IL Post Processors, as far as I’m aware, the input have to be valid C# code. In the case of Source Generators, we cannot re-write existing code and the resulting code must be valid C# code.

nameLabel.text = (ref)myDataSource.name;

With the example data source that I’ve provided in the original post and the version of C# we have access to at the moment, this snippet gives two compilation errors:

  • error CS8373: The left-hand side of a ref assignment must be a ref variable.

  • error CS1510: A ref or out value must be an assignable variable

I can fix the data source side to return a ref property, but that removes only one compilation error and there are other considerations with using ref properties as well.

Perhaps I’m not understanding you correctly. Can you make a custom VisualElement that would define a text property that would allow this?

Two reasons why we expose PropertyPath:

  • You can bind to sub-properties of a data source (i.e. source.path.to.my.value.
  • You can construct a PropertyPath using different part types. There are three part types: name, index (for lists/arrays) and keys (for dictionaries and hashsets).
3 Likes

I know this might be going off topic but I’d like to know if you are considering implementing my suggestion here . Basically, specifying the target type for binding in UXML will help tooling developers, for IDEs and Unity Assets Store alike.

I’d really like to see compile-time / static checking for binding. All XML based UI frameworks I worked with supports static checking, including but not limited to Android UI, WPF, UWP, and even Angular

Hi @AliAlbarrak !

That is a good feature to have and something we have on our radar as well. At the moment, we’ve added the data-source-type as an attribute that the UI Builder can use to drive the auto-complete when setting the data-source-path attribute.

It doesn’t do static validation, but it’s something we can build on. Not perfect, but it’s a start!

[Edit] For the editor bindings, it’s already using the new system under the hood, but we intend to expose public dedicated binding types to deal with serialization. Once we expose these, they will inherit the workflows as well.

1 Like

Hey guys. appreciate your work a lot, and thank you for the examples on how to use this API.
However, I am sorry to say but I find it still very hard to understand from your examples how to use the new binding on a list with a ListView, something that I imagine should be very simple given how common of a use case it is in UI development (i.e. A simple inventory system, a list of characters etc…)

In the 2022 LTS version there was this page

Which clarified some things, but considering it only worked in the editor and not in runtime, I would really appreciate a new simple tutorial like this with the new system

The only reason I upgraded my simple 2d UI-heavy idle game to 2023.2 was to use this new binding system, as I thought it would save me a lot of time.
So I would really appreciate any help in the matter.

Thanks in advance for your hard work!

I think it would help if you provided built in extensions methods to reduce boilerplate.

Also I don’t completely understand this part.

If we can bind properties, then why is any of these binding objects necessary? If you want to convert some data you can just do that in a property itself. So binding objects seems to be only necessary when you want to “add/remove uss classes”. If I had to guess one of the use cases would be, like, making health text red on low HP?
In this case though I think it would be easier to have some value “post processors”, that get a callback when property value is changed. At least this way you don’t couple data conversion itself and how UI is styled based on it, not to mention you could stack these post processors

Hi @Pot !

I did talk about how to bind to the ListView in this post here . As I mentioned in that post, I consider the ListView only partially supported at the moment as you will need to create a custom binding to refresh or rebuild the list. It’s something we want to improve, but haven’t had time to complete a full generic solution just yet.

What I’m currently doing is a custom binding on the list view that tracks the count and call RefreshItems when it changes, so that the list view can add/remove them. Individual items should be fine if they are using the method in the link.

Hope this helps!

1 Like

Hi @Saniell

There are several reasons why they might be necessary:

  • The data you are binding against is not readily available through instance fields/properties and requires a method call.
  • The binding you are creating doesn’t need to go through data at all.
  • You have a specialized data source and you can create a binding object that will perform better than DataBinding.
  • You only care about when the resolved data source and not its individual properties.

For example, when dealing with localization, you typically won’t have a field or a property to bind against. Instead, you usually have a localization table id and localization key id and you can query the system to get the localized value. In this case, creating a custom binding is the way to go. The localization package will be updated to provide these custom bindings so that you won’t have to. This should be part of the 1.5.0 version of the package.

Another example is when dealing with events, such as adding a callback to a Button where the callback itself is defined by the binding object.

One last example is something I’m toying with, which is to create an element that can generate a UI hierarchy based on some data and automatically bind to it. In that use-case, the element itself doesn’t care about the current state of the data source, it only cares about when the data source itself has changed. This looks something like:

private class GenerateHierarchyBinding : CustomBinding
{
    public GenerateHierarchyBinding()
    {
        // Doesn't need to be updated, so setting `WhenDirty` to avoid most updates.
        updateTrigger = BindingUpdateTrigger.WhenDirty;
    }
   
    // Overriding this method to receive a callback only when the resolved context changed.
    protected override void OnDataSourceChanged(in DataSourceContextChanged context)
    {
        var generatorElement = context.targetElement as /* ... */;
        // Generate hierarchy based on the `context.newContext.dataSource` and `context.newContext.dataSourcePath` properties.
    }
}

It could be any use-case where you need to add/remove a uss class: change the text color of something, hide/display elements, etc. When dealing with style properties, it’s usually better to go through adding/removing uss classes when it is possible than creating binding objects to individual properties. Not always possible, though.

1 Like

Well couldn’t you just do this?

class Data
{
    public LocalizedTextID TextID;

    public string Text
    {
        get => Localization.Lookup(TextID);
    }
}

In any case, can I bind multiple things at once to a property? Say I’m making My Friendly Neighbourhood and my health is displayed as localized text which also changes color based on value. The way I would want to do it is by binding text using code similar to above (if possible) and then have a callback “On value change - color the text”

I guess my point is that just having a callback seems easier than a singular binding. But I am not very experienced in UIToolkit so this is just a first impression

class Data
{
    public LocalizedTextID TextID;
    public string Text
    {
        get => Localization.Lookup(TextID);
    }
}

Yes, that’s what the localization bindings will do under the hood. The difference between using a custom binding and going through a data source and using DataBinding in this case is performance. If you have all the necessary information to resolve the value using the localization table id and localization key, there is no need to also extract the value from Data.Text, as it’s pretty much just wasted computing time. A dedicated binding type here can also ensure that the binding is updated only when the localization database or the current locale is changed.

Yes, you can. The preferred and most performant way is to use a custom element that has a value that can change multiple properties when it’s set. You can also create multiple bindings that use the same input, but targets different UI properties, but it will less performant than using a custom element.

Another way would be to extend an existing binding type and apply additional logic to it.

Oh I see, makes sense right.

Again, I see, makes sense. Really nice.

Well I prefer to not have inheritance deeper than 1 level necessary :slight_smile:
Thanks for answering my questions, looking forward to using this API in the future to get a better feel for it. Btw if you still didn’t, I would appreciate if you added some examples of having multiple bindings, maybe something similar to that colored text thing I mentioned

Sure, I’ll provide three examples here:

  • Using a custom element with a value that will set its own sub properties when changed. This requires the most code, but it is probably the most efficient solution and the most “self contained” solution as well.
  • Using a normal Label and registering multiple bindings from the same input string with converters.
  • Defining a binding that extend DataBinding.

All examples will focus on using the UI Builder. All could be done through code by calling SetBinding as well.

Using a custom element:
9203367--1283838--ComplexLabel.gif

This example requires more code, but it would behave the same way whether the value is set from a binding, from the UI Builder or from code. This is most likely the most efficient solution as well, as a single binding is necessary and no conversion is done.

The custom element is defined as:

using Unity.Properties;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

[UxmlElement]
public partial class ComplexLabel : VisualElement
{
    public static BindingId valueProperty = nameof(value);
   
    private Label m_Label;

    [CreateProperty, UxmlAttribute]
    public string value
    {
        get => m_Label.text;
        set
        {
            if (string.CompareOrdinal(m_Label.text, value) == 0)
                return;

            m_Label.text = value;
            UpdateVisuals();
            NotifyPropertyChanged(valueProperty);
        }
    }
   
    public ComplexLabel()
    {
        m_Label = new Label();
        Add(m_Label);
        UpdateVisuals();
    }

    // In this case, a more performant solution would be to use uss classes to drive these values since it is a known set.
    private void UpdateVisuals()
    {
        if (string.IsNullOrEmpty(value))
        {
            m_Label.style.display = DisplayStyle.None;
            m_Label.style.color = StyleKeyword.Null;
        }
        else
        {
            m_Label.style.display = DisplayStyle.Flex;
            if (value.StartsWith("[Danger]"))
                m_Label.style.color = Color.red;
            else if (value.StartsWith("[Warning]"))
                m_Label.style.color = Color.yellow;
            else
                m_Label.style.color = StyleKeyword.Null;
        }
    }
}

Using multiple bindings:
9203367--1283844--MultipleBindings.gif

This example uses a single string as input and register multiple bindings that use a converter to convert the string to a StyleColor and to a DisplayStyle. This can then use a normal Label instead of a custom element, however, you would need to add multiple bindings on each of the targeted labels.

The code to register the converters for uxml is defined as:

using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

class UxmlConverters
{
    [InitializeOnLoadMethod]
    static void RegisterUxmlConverters()
    {
        var group = new ConverterGroup("complex_label");
        group.AddConverter((ref string str) =>
        {
            if (string.IsNullOrEmpty(str))
                return new StyleColor(StyleKeyword.Null);

            if (str.StartsWith("[Danger]"))
                return new StyleColor(Color.red);
           
            if (str.StartsWith("[Warning]"))
                return new StyleColor(Color.yellow);
           
            return new StyleColor(StyleKeyword.Null);
        });
       
        group.AddConverter((ref string str) =>
        {
            if (string.IsNullOrEmpty(str))
                return new StyleEnum<DisplayStyle>(DisplayStyle.None);

            return new StyleEnum<DisplayStyle>(DisplayStyle.Flex);
        });
        ConverterGroups.RegisterConverterGroup(group);
    }
}

Using a binding extending DataBinding:
9203367--1283847--CustomBinding.gif

This example uses a specialized data binding that will set the text color and the display style whenever the main targeted property is set. This solution would have the same performance characteristics as the first solution and also allow to be used across different elements.

The binding object is defined as:

using UnityEngine;
using UnityEngine.UIElements;

[UxmlObject]
public partial class CustomLabelBinding : DataBinding
{
    protected override BindingResult UpdateUI<TValue>(in BindingContext context, ref TValue value)
    {
        var result =  base.UpdateUI(in context, ref value);
        if (result.status == BindingStatus.Success && value is string str)
        {
            var target = context.targetElement;
            if (string.IsNullOrEmpty(str))
            {
                target.style.display = DisplayStyle.None;
                target.style.color = StyleKeyword.Null;
            }
            else
            {
                target.style.display = DisplayStyle.Flex;
                if (str.StartsWith("[Danger]"))
                    target.style.color = Color.red;
                else if (str.StartsWith("[Warning]"))
                    target.style.color = Color.yellow;
                else
                    target.style.color = StyleKeyword.Null;
            }
        }
        return result;
    }
}

Obviously, a data source could also be defined to expose the properties themselves as bindable properties, but hopefully, this can give some ideas of ways to achieve your use-case using the new system.

5 Likes

After reading the last comments, I was horrified, it’s all so inconvenient for an ordinary developer that it’s just creepy…
And after some attempts to use runtime bindings, I realized that the old root.Q() and etc variants so still more convenient…
I don’t understand why unity always manages to recreate existing technologies and make them less convenient to use.
WPF-like bindings?
Razor pages? (also razor are just more ideally suited to unity, taking into code generation out of the box)

2 Likes