Help me with databinding

My current solution doesn’t work as expected. Let me describe the situation… For each custom element, I have implemented a separate DataSource class, which includes references to other data containing the values to be displayed.

For example, I have a Data class that contains an int Value. A reference to Data is stored in HeaderDataSource, which is connected in the UIBuilder to a custom Header element. At the same time, the Value from Data is also connected to another DataSource (e.g., ContentDataSource for a custom Content element).

In this case, my implementation using BindableProperty<T> with BindingUpdateTrigger.WhenDirty does not work because the data bindings are not the same, even though the value is identical and needs to be displayed simultaneously in two different places.

My implementation using BindableProperty works well only at runtime when the DataSource contains the direct holder of the required value. However, in the current case, the nesting is deeper. This is because I need the interface to be built both in the Editor and at runtime.

What should I do? Should I use OnSourceChanged for displaying values in the Editor (though updates still occur every frame rather than on change), and for runtime, pass a reference to a single class containing the required value into each custom element’s DataSource?

Here are the scripts illustrating the current implementation:

[Serializable]
public class BindableProperty<T>
{
    [SerializeField] public T value;
    [SerializeField] private BindingUpdateTrigger updateTrigger;
    [SerializeField] private BindingMode bindingMode;

    public string PropertyName { get; }

    [CreateProperty] public T Value
    {
        get => value;
        set
        {
            this.value = value;
            binding?.MarkDirty();
        }
    }

    private DataBinding binding;
    public DataBinding Binding => binding ??= new DataBinding
    {
        dataSourcePath = new PropertyPath($"{PropertyName}.{nameof(Value)}"),
        updateTrigger = updateTrigger,
        bindingMode = bindingMode,
    };

    public BindableProperty(T initialValue, string propertyName, BindingUpdateTrigger updateTrigger = BindingUpdateTrigger.WhenDirty, BindingMode bindingMode = BindingMode.ToTarget)
    {
        value = initialValue;
        PropertyName = propertyName;
        this.updateTrigger = updateTrigger;
        this.bindingMode = bindingMode;
    }
}
public class Data : ScriptableObjectInstaller<Data>
{
    public BindableProperty<int> Speed = new BindableProperty<int>(0, nameof(Speed));
    
    public override void InstallBindings()
    {
        Container.BindInstance(this).AsSingle();
    }
}
public class HeaderDataSource : ScriptableObject
{
    [Header("General")]
    public Data.Data Data;
    public Config.Config Config;
}

In Editor mode, I bind the values like this:

SetBinding(nameof(Speed), BindingHelper.CreateBindingFromValue(
    typeof(HeaderDataSource),
    $"{nameof(Data.Data)}.{nameof(Data.Data.Speed)}.{nameof(Data.Data.Speed.Value)}"
));
public static DataBinding CreateBindingFromValue(
    System.Type sourceType, 
    string nameOfValue, 
    BindingUpdateTrigger updateTrigger = BindingUpdateTrigger.OnSourceChanged,
    BindingMode bindingMode = BindingMode.ToTarget)
{
    return new DataBinding()
    {
        dataSourceType = sourceType,
        dataSourcePath = new PropertyPath(nameOfValue),
        updateTrigger = updateTrigger,
        bindingMode = bindingMode
    };
}
private int speed;
[CreateProperty] public int Speed
{
    get => speed;
    set
    {
        speed = value;
        label.text = speed.ToString();
        Debug.Log(value);
    }
}

However, in this case, the value changes every frame.

In runtime, I inject Data directly, replacing HeaderDataSource, and then use MarkDirty and the binding from BindableProperty:

// Header.cs
public void Initialize()
{
    var headerDataSource = dataSource as HeaderDataSource;
    wifiLabel.Initialize(headerDataSource?.Data);
}
// HeaderWiFiLabel.cs
public void Initialize(Data.Data data)
{
    dataSource = data;
    SetBinding(nameof(Speed), data.Speed.Binding);
}

But in this case, the bindings accumulate, and I need to cancel the Editor binding.

Could you please guide me on how to do this correctly? Help me find the right path.

Most of the time, when I’m faced with a situation like this, I try to do the simplest thing that I can and go step by step. Once a step is working as expected, I add complexity.

In your case, I would probably start with something like this:

  • Define my data sources to be completely independant of UI:
public class Data : ScriptableObject
{
    [SerializeField]
    private int m_Speed;

    public int Speed
    {
        get => m_Speed;
        set => m_Speed = value;
    }
}
  • Create a UXML file to display the Speed:
<UXML>
	<UnityEngine.UIElements.IntegerField label="Speed"/>
</UXML>
  • Instrument your data source to create a binding between the data source and the UI:
public class Data : ScriptableObject
{
    [SerializeField, DontCreateProperty] // We want the binding to go through the property and not the field.
    private int m_Speed;

    [CreateProperty]
    public int Speed
    {
        get => m_Speed;
        set => m_Speed = value;
    }
}

And for the UXML, you have multiple options:

  • You can define the data-source and data-source-path on the binding directly:
<UXML>
	<UnityEngine.UIElements.IntegerField label="Speed">
		<Bindings>
			<UnityEngine.UIElements.DataBinding
				property="value"
				data-source="/* Insert path here */"
				data-source-path="Speed"
				binding-mode="ToTarget"
				update-trigger="OnSourceChanged"
		</Bindings>
	</UnityEngine.UIElements.IntegerField>
</UXML>
  • You can define the data-source on the element and the data-source-path on the binding:
<UXML>
	<UnityEngine.UIElements.IntegerField label="Speed"
		data-source="/* Insert path here */"
	>
		<Bindings>
			<UnityEngine.UIElements.DataBinding
				property="value"
				data-source-path="Speed"
				binding-mode="ToTarget"
		</Bindings>
	</UnityEngine.UIElements.IntegerField>
</UXML>
  • You can define the data-source and data-source-path on the element:
<UXML>
	<UnityEngine.UIElements.IntegerField label="Speed"
		data-source="/* Insert path here */"
		data-source-path="Speed"
	>
		<Bindings>
			<UnityEngine.UIElements.DataBinding
				property="value"
				binding-mode="ToTarget"
				update-trigger="OnSourceChanged"
		</Bindings>
	</UnityEngine.UIElements.IntegerField>
</UXML>

The “resolved” data-source-path is a concatenation between all the data-source-pathfrom the binding to the closest data-source. So if in one case you are binding directly to a Data instance, the resolved path needs to be Speed, but if you are using another data source that contains a Data instance at path “Managers[0].Database.Data”, then the “resolved” path needs to be “Managers[0].Database.Data.Speed”, which could look like something like this:

<UXML>
	<UnityEngine.UIElements.VisualElement
		data-source="/* Insert path here to object containing a nested Data inside */"
	>
		<UnityEngine.UIElements.IntegerField label="Speed"
			data-source-path="Managers[0].Database.Data"
		>
			<Bindings>
				<UnityEngine.UIElements.DataBinding
					property="value"
					data-source-path="Speed"
					binding-mode="ToTarget"
					update-trigger="OnSourceChanged"
			</Bindings>
		</UnityEngine.UIElements.IntegerField>
	</UnityEngine.UIElements.VisualElement>
</UXML>
  • Avoid having updating the binding on every tick.
public class Data : ScriptableObject, INotifyBindablePropertyChanged
{
    public event EventHandler<BindablePropertyChangedEventArgs> propertyChanged;

    [SerializeField]
    private int m_Speed;

    public int Speed
    {
        get => m_Speed;
        set {
            if (UnityEngine.Mathf.Approximately(m_Speed, value))
                return;
            m_Speed = value;
            NotifyChanged();
        }
    }

    private void NotifyChanged([CallerMemberName] string property = "")
    {
        propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(property));
    }
}

Note here that if you want to do the same thing on the other object, that object will also need to implement the interface and report the changes on Data.

  • At this point, if you can make it work across different UI Hierarchies and have all UI update correctly, I would look into the BindableProperty<T> thingy, but personally, I would not go there at all.

hope this helps!

1 Like

I understand your example, but could you provide an implementation example with a custom element? In my case, the custom element Header contains a custom element HeaderWiFiLabel with the property int Speed, to which I want to bind the value of int Speed from Data. The reference to Data is located in HeaderDataSource, which is connected in the UIBuilder to Header, and, consequently, is also available to HeaderWiFiLabel.

@martinpa_unity

I managed to make this solution work:

In the Header element, I provided a reference to Data. In the HeaderWiFiLabel, I set up the following binding:

SetBinding(nameof(Speed), new DataBinding
{
    dataSourceType = typeof(Data.Data),
    dataSourcePath = new PropertyPath("Speed"),
    updateTrigger = BindingUpdateTrigger.OnSourceChanged,
    bindingMode = BindingMode.ToTarget
});

Here is the Speed property implementation:

private int speed;
[CreateProperty]
public int Speed
{
    get => speed;
    set
    {
        speed = value;
        label.text = speed.ToString();
        Debug.Log(value);
    }
}

However, for some reason, Debug.Log(value) and the value assignment are triggered twice instead of just once:

126 UnityEngine.Debug:Log (object)
UI.Header.HeaderWiFiLabel:set_Speed (int) (at Assets/Scripts/UI/Header/HeaderWiFiLabel.cs:24)
Unity.Properties.ReflectedMemberProperty`2<UI.Header.HeaderWiFiLabel, int>:SetValue (UI.Header.HeaderWiFiLabel&, int)
...

Despite this, it’s still better than updating the value on every tick, and when the interface is refreshed in the editor, the values are inserted properly. It’s almost exactly what I need.

However, in this approach, I have to pass Data to the dataSource. What I want instead is to pass HeaderDataSource, which contains a reference to Data, while still updating the Speed value from Data. This should be done without directly owning the reference to HeaderDataSource, as there could be multiple instances of it (e.g., in cases where different Headers have the same Speed field but other content differs).

As a result, I cannot simply move INotifyBindablePropertyChanged because I don’t directly set Data in HeaderDataSource. Without it, the binding gets triggered on every tick. How can this be resolved? For now, the only solution I see is to check for changes directly in the custom element itself.


Another problem I encountered is why the dataSource field is null both in the editor and at runtime. I need to retrieve another parameter from it, which should be used alongside speed to configure the visualization of the value (e.g., color). Should I bind everything separately as well?

Here’s the relevant code:

speed = value;
label.text = speed.ToString();
Debug.Log(value);
Debug.Log($"dataSource = {dataSource != null}");

The output indicates that dataSource is null:

dataSource = False
UnityEngine.Debug:Log (object)
UI.Header.HeaderWiFiLabel:set_Speed (int) (at Assets/Scripts/UI/Header/HeaderWiFiLabel.cs:25)
Unity.Properties.ReflectedMemberProperty`2<UI.Header.HeaderWiFiLabel, int>:SetValue (UI.Header.HeaderWiFiLabel&, int)
...

I need dataSource to access a parameter for configuring the display (like the color) without explicitly binding every possible property. Is there a way to ensure dataSource is correctly set, or should I indeed bind all necessary properties individually?

For binding purposes, custom elements will work in the same way as built-in ones, the functionality is shared across all element types. The same logic will apply. For any given binding, we’ll find the closest data source (either on the binding, on the element or one of its ancestors) and figure out the path to use from that data source.

It doesn’t matter which element actually sets the data source or from where, it only matters that it is set. For a DataBinding, the bindingId or property will act as a path from the element.

For setting speed in your UI, you have two main approaches:
1- Cache the property on your element and manually set the UI (similarly to what you’ve done)

private IntegerField field;
private int speed;

[CreateProperty]
public int Speed
{
    get => speed;
    set
    {
    	if (UnityEngine.Mathf.Approximately(speed, value))
    		return;
        speed = value;
        field.value = speed;
        NotifyPropertyChanged();
    }
}

This might require you to track changes on the field’s value to update the cached field, otherwise the values will go out of sync.

2- Use a passthrough property:

private IntegerField field;

[CreateProperty]
public int Speed
{
    get => field.value;
    set => field.value = value;
}

This requires to catch changes to the field’s value to propagate the NotifyPropertyChanged("Speed");.

However, for some reason, Debug.Log(value) and the value assignment are triggered twice instead of just once:

UI.Header.HeaderWiFiLabel:set_Speed (int) (at Assets/Scripts/UI/Header/HeaderWiFiLabel.cs:24)
Unity.Properties.ReflectedMemberProperty`2<UI.Header.HeaderWiFiLabel, int>:SetValue (UI.Header.HeaderWiFiLabel&, int)

If you are referring to these two calls above, then it’s for the same call as one calls the other using reflection.

However, in this approach, I have to pass Data to the dataSource. What I want instead is to pass HeaderDataSource, which contains a reference to Data, while still updating the Speed value from Data.

If you want to pass HeaderDataSource, then make sure to adjust the path accordingly, it shouldn’t be Speed anymore, but Data.Speed.

If you set HeaderDataSource as the data source, then the path needs to go from the HeaderDataSource to your Speed property. If you want to keep Speed as the path, either set HeaderDataSource.Data as the data source or create a Speed property on HeaderDataSource that maps to Data.Speed.

As a result, I cannot simply move INotifyBindablePropertyChanged because I don’t directly set Data in HeaderDataSource. Without it, the binding gets triggered on every tick. How can this be resolved? For now, the only solution I see is to check for changes directly in the custom element itself.

They would both need to implement it and you need to propagate the changes from Data to the HeaderDataSource, as in HeaderDataSource needs to listen to the propertyChanged events of the Data instance it has and propagate the changes (either by notifying that Data has changed, or Data.Speed).

Another problem I encountered is why the dataSource field is null both in the editor and at runtime. I need to retrieve another parameter from it, which should be used alongside speed to configure the visualization of the value (e.g., color). Should I bind everything separately as well?

dataSource will be the value that you have set. If you set the dataSource on a parent element, it won’t update the children’s dataSource property. You either need to query it on the parent or call GetHierarchicalDataSourceContext to retrieve it from the hierarchy (there is also GetDataSourceContext(BindingId) that you can use).

Hope this helps!

1 Like

Thank you very much for your assistance. I hope that using BindingUpdateTrigger.OnSourceChanged and checking for value changes directly on the custom object before updating the interface does not consume too many device and application resources. However, this approach is definitely more convenient than trying to extend DataBinding to every element, even though using WhenDirty results in significantly fewer property access requests.

@martinpa_unity It would be great if you could duplicate the examples provided to me in the Unity documentation so that others can benefit from them more easily.

Yeah, I’ll see if we can bring more complicated use-cases into the documentation.

I hoped to be able to expose a project that showcased advanced use-cases, but priorities have shifted since.

1 Like