Looking for examples of single data binding across multiple properties and elements

Hello, on Unity - Manual: Define binding mode and update trigger Unity suggests: " Don’t keep binding types per-element state. You can use a binding instance across multiple elements and properties simultaneously. During updates and callbacks, the binding system passes in a context object that contains the target element, binding ID, and relevant data. You can use this context object to store the per-element state"

I have an enum, “GameState”, which needs to control the DisplayStyle for several elements.

Are there any examples of how to use a data binding with multiple properties / elements I can follow? I’m having trouble figuring out what the “right” way to accomplish this is. This is what I have so far, and it works, but feels incorrect:

    [CreateProperty]
    public DisplayStyle FinalizePathButtonDisplayStyle => 
        CurrentState == WipeoutGameState.CREATING_PATH && _gridController?.pathIsComplete == true
            ? DisplayStyle.Flex 
            : DisplayStyle.None;
    
    [CreateProperty]
    public DisplayStyle BuildHeaderDisplayStyle => 
        CurrentState != WipeoutGameState.CREATING_PATH
            ? DisplayStyle.Flex 
            : DisplayStyle.None;
    
    [CreateProperty]
    public DisplayStyle StartWaveDisplayStyle => 
        CurrentState == WipeoutGameState.ADDING_OBSTACLES
            ? DisplayStyle.Flex 
            : DisplayStyle.None;

...

        DataBinding displayFinalizePathBinding = new DataBinding
        {
            bindingMode = BindingMode.ToTarget,
            dataSourcePath = new PropertyPath(nameof(FinalizePathButtonDisplayStyle)),
            updateTrigger = BindingUpdateTrigger.OnSourceChanged,
        };
        _finalizePathButton.SetBinding("style.display", displayFinalizePathBinding);
        
        DataBinding buildHeaderBinding = new DataBinding
        {
            bindingMode = BindingMode.ToTarget,
            dataSourcePath = new PropertyPath(nameof(BuildHeaderDisplayStyle)),
            updateTrigger = BindingUpdateTrigger.OnSourceChanged,
        };
        _buildHeader.SetBinding("style.display", buildHeaderBinding);
        
        var startWaveBinding = new DataBinding
        {
            bindingMode = BindingMode.ToTarget,
            dataSourcePath = new PropertyPath(nameof(StartWaveDisplayStyle)),
            updateTrigger = BindingUpdateTrigger.OnSourceChanged,
        };
        _startWaveButton.SetBinding("style.display", startWaveBinding);

I think to reuse the one binding here, data needs to come from a single source. Or, multiple data sources via the same path. Then these can be applied to different UI binding paths.

So in your example it won’t work, particularly as you binding seems backwards? You’re binding from what appears to be data represented by UI and setting that onto something else.

If you have a ‘GameState’ enum, then that needs to be represented by some data source, then you bind from that data source to your various UI elements, which can be done with the one data binding instance as well.

Rough example, using the INotifyBindablePropertyChanged interface:

public class GameStateManager : MonoBehaviour, INotifyBindablePropertyChanged
{
    [SerializeField]
    private GameState _gameState;
    
    private DataBinding _gameStateBinding;
    
    public event EventHandler<BindablePropertyChangedEventArgs> propertyChanged;
    
    [CreateProperty]
    public GameState GameState
    {
        get => _gameState;
        set
        {
            if (value == _gameState)
            {
                return;
            }
            
            _gameState = value;
            Notify();
        }
    }
    
    private void Notify([CallerMemberName] string property = "")
    {
        propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(property));
    }
    
    public DataBinding GetGameStateDataBinding()
    {
        return _gameStateBinding ??= new DataBinding()
        {
            bindingMode = BindingMode.ToTarget,
            dataSourcePath = new PropertyPath(nameof(GameState)),
            updateTrigger = BindingUpdateTrigger.OnSourceChanged,
        };
    }
}

Then you can get the cached binding and use that across multiple visual elements.

Hi @superweeniehutjr

Don’t keep binding types per-element state.

Yeah, this is not phrased clearly. It should be something like:

Don’t keep per-element state in your binding instance as much as possible.

Note that this is not a recommendation to reuse binding instances as much as possible, but a guidance that if you write your own binding types, you should try to avoid keeping per-element state as much as possible.

In the case of DataBinding, we are only using the provided context and don’t store any data during the various update methods, so it’s safe to use the same instance on multiple elements/properties.

Each time you register a binding on an element for a given property/id, internally, we create a binding context and each context will get resolved and updated separately. For example, a binding instance that does not set the dataSource or dataSourcePath will use the one computed from the hierarchy. If you register said binding on multiple elements, you might get different resolved dataSource/dataSourcePath.

- root.dataSource = MyCoolObject
    - child.dataSourcePath = "someProperty"
        - grandChild.dataSourcePath = "someOtherProperty"

Given the hierarchy above, if you register the bindings as follow:

var binding = new DataBinding();
child.SetBinding("value", binding);
grandChild.SetBinding("text", binding);

During the binding tick, child will try to set the value property with the value at MyCoolObject.someProperty and grandChild will try to set the text property with the value at MyCoolObject.someProperty.someOtherProperty.

If you set the dataSource and/or dataSourcePath on the binding instance, then it will be used to resolved the context everwhere it’s used.

In your example, I would probably not bother with using the same binding instance unless I really needed to shave off a few allocations.

Hope this helps!

2 Likes

Thanks for the reply, but unfortunately UI toolkit does not allow binding from different types, meaning I can not bind my “GameState” directly to “DisplayStyle”. Had to figure out that out the hard way earlier after an hour of it silently failing, it’s buried in the documentation as a random “Note”.

Had to figure out that out the hard way earlier after an hour of it silently failing […]

When something is not working, be sure to toggle on the binding logs, as we overly verbose with the errors. For an editor window, you can do so through the kebab menu on the top right of the window and runtime use-cases, you can do so through the PanelSettings asset.

Hope this helps!

1 Like

I never suggested that you should. You would still bind it to a property of the same type in your visual elements. Then any changes to the underlying data can be received and reflected by the UI.