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.