UI-Toolkit data binding bug related to the DataSourcePath

I was trying out the new runtime databinding in UIToolkit. My data source implements IDataSourceViewHashProvider, INotifyBindablePropertyChanged and seems to work fine most of the time e.g. app title is ok. If the control uses the DataSourcePath however it doesn’t bind properly anymore.

I have a basic sample to validate a poker hand. The green label updates correctly, the red label only updates once at the start, both are bound to the same value.

The left one uses Every Update as the trigger and works but I don’t want to always update.

The right one uses On Source Changed as the trigger. This only updates once.

The main problem seem to be when the DataSourcePath is set.

For example, if I clear the path and set the datasource directly it works (right side text triggers On Source Changed).

var view = visualElement.Query("Player1Cards").First();
view.dataSource = vm.Player1Cards;
view.dataSourcePath = new Unity.Properties.PropertyPath("");

I’m currently adding this element as a child to fix it but it feels a bit hacky. Is this a know bug or am I missing something?

[UxmlElement]
    partial class ClearDataSourcePathElement : VisualElement
    { 
        INotifyBindablePropertyChanged GetLastDataSource()
        {
            var context = GetHierarchicalDataSourceContext();
            var ds = context.dataSource;
            for (var i = 0; i < context.dataSourcePath.Length; i++)
            {
                var propName = context.dataSourcePath[i].ToString();
                var prop = ds.GetType().GetProperty(propName);
                ds = prop.GetValue(ds);
            }
            return ds as INotifyBindablePropertyChanged;
        }

        public ClearDataSourcePathElement() 
        {
            RegisterCallback<AttachToPanelEvent>(x =>
            {
                parent.dataSource = GetLastDataSource();
                parent.dataSourcePath = new Unity.Properties.PropertyPath("");
            });
        }
    }

Hi @CodeKiwi,

If you set the dataSourcePath on the element directly, it will act as a filter of the dataSource for the binding on the same element and its children, unless they also set a dataSource themselves.

The dataSource is inherited until it is overridden and the resolved dataSourcePath is concatenated from the current binding until the closest resolved dataSource.

Can you show us your data type and your Uxml/code where you setup the bindings?

Thanks for the update. I think I might try and make a smaller standalone example to duplicate the issue. Currently I have a base class like this for my datasource.

public class ViewModelBase : IDataSourceViewHashProvider, INotifyBindablePropertyChanged
    {
        private long m_ViewVersion;
        public event EventHandler<BindablePropertyChangedEventArgs> propertyChanged;

        public void Publish()
        {
            ++m_ViewVersion;
        }

        public long GetViewHashCode()
        {
            return m_ViewVersion;
        }

        protected void Notify([CallerMemberName] string property = "")
        {
            propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(property));
            Publish(); // TODO: CHECK
        }
    }

The data source with the title issue looks like this.

public class CardSetVM : ViewModelBase
{
        private string title;
        [CreateProperty]
        public string Title
        {
            get => title;
            set
            {
                if (title == value)
                {
                    return;
                }
                title = value;
                Notify();
            }
        } 
}

I have a uxml file called CardSetView.uxml that is using the above as the datasource. This binding works fine when attached to a UIDocument e.g. setting the title in code will update the UI.

<engine:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:engine="UnityEngine.UIElements" xmlns:editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
    <engine:Template name="CardView" src="project://database/Assets/Poker/Samples/HandSample/UI/CardView.uxml?fileID=9197481963319205126&amp;guid=2f1254b93f1eb7e40b1056f03b57afa5&amp;type=3#CardView" />
    <engine:VisualElement data-source-type="KiwiGameDev.Poker.Sample.HandSample.CardSetVM, KiwiGameDev.Poker.Sample.HandSample" style="flex-grow: 1; flex-direction: column; background-color: rgb(36, 36, 36);">
        <engine:VisualElement name="Horizontal" style="flex-grow: 1; flex-direction: row; height: auto; max-height: 32px;">
            <engine:Label text="Title" name="TitleLabel" style="color: rgb(255, 255, 255); text-overflow: clip; white-space: pre; -unity-text-align: middle-left;">
                <Bindings>
                    <engine:DataBinding property="text" data-source-path="Title" binding-mode="ToTarget" update-trigger="EveryUpdate" />
                </Bindings>
            </engine:Label>
            <engine:Button text="Clear" name="ClearButton" style="align-self: center; visibility: visible; display: flex; width: 63px;">
                <KiwiGameDev.MVVM.Core.ClickEventElement data-source-path="ClearCommand" />
                <Bindings>
                    <engine:DataBinding property="style.display" update-trigger="EveryUpdate" data-source-path="ClearButtonDisplayStyle" binding-mode="ToTarget" />
                </Bindings>
            </engine:Button>
            <engine:VisualElement name="WinnerIcon" enabled="true" style="flex-grow: 1; width: 24px; height: 24px; background-image: url(&quot;project://database/Assets/Poker/Samples/HandSample/Sprites/trophy.png?fileID=21300000&amp;guid=8ef6f679daff94a458fcbd43d9aefd80&amp;type=3#trophy&quot;); max-width: 24px; justify-content: space-around; align-items: stretch; align-self: center; display: flex; visibility: visible;">
                <Bindings>
                    <engine:DataBinding property="style.visibility" data-source-path="Winner" binding-mode="ToTarget" update-trigger="EveryUpdate" />
                </Bindings>
            </engine:VisualElement>
            <engine:Label text="Title" name="TitleLabel" style="color: rgb(255, 255, 255); text-overflow: clip; white-space: pre; -unity-text-align: middle-left;">
                <Bindings>
                    <engine:DataBinding property="text" data-source-path="Title" binding-mode="ToTarget" update-trigger="OnSourceChanged" />
                </Bindings>
            </engine:Label>
        </engine:VisualElement>
        <KiwiGameDev.MVVM.Core.FrameElement data-source-path="Cards" style="flex-direction: row; align-self: stretch; justify-content: flex-start; align-items: center;">
            <engine:Instance template="CardView" />
            <engine:Instance template="CardView" />
            <engine:Instance template="CardView" />
            <engine:Instance template="CardView" />
            <engine:Instance template="CardView" />
        </KiwiGameDev.MVVM.Core.FrameElement>
    </engine:VisualElement>
    <KiwiGameDev.MVVM.Core.ClearDataSourcePathElement />
</engine:UXML>

However, I have another uxml file below that includes CardSetView which doesn’t work.

<engine:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:engine="UnityEngine.UIElements" xmlns:editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
    <engine:Template name="CardSetView" src="project://database/Assets/Poker/Samples/HandSample/UI/CardSetView.uxml?fileID=9197481963319205126&amp;guid=d87691ad24eb67c46bcb3cc7d8ab7ac8&amp;type=3#CardSetView" />
    <engine:VisualElement data-source-type="KiwiGameDev.Poker.Sample.HandSample.HandSampleVM, KiwiGameDev.Poker.Sample.HandSample" style="flex-grow: 1; background-color: rgb(36, 36, 36);">
        <engine:Label text="Label" style="color: rgb(255, 255, 255);">
            <Bindings>
                <engine:DataBinding property="text" data-source-path="Title" binding-mode="ToTarget" />
            </Bindings>
        </engine:Label>
        <engine:Instance template="CardSetView" name="SpadesCards" data-source-path="SpadesCards" />
        <engine:Instance template="CardSetView" name="DiamondsCards" data-source-path="DiamondsCards" />
        <engine:Instance template="CardSetView" name="ClubsCards" data-source-path="ClubsCards" />
        <engine:Instance template="CardSetView" name="HeartsCards" data-source-path="HeartsCards" />
        <engine:VisualElement style="flex-grow: 1; flex-direction: row; justify-content: space-between;">
            <engine:Instance template="CardSetView" name="JockerCards" data-source-path="JockerCards" />
            <engine:Instance template="CardSetView" name="Player1Cards" data-source-path="Player1Cards" />
            <engine:Instance template="CardSetView" name="Player2Cards" data-source-path="Player2Cards" />
            <engine:Instance template="CardSetView" name="Player3Cards" data-source-path="Player3Cards" />
        </engine:VisualElement>
    </engine:VisualElement>
</engine:UXML>

The above does seem to have the correct bindings e.g. I can see the title that is set on load. EveryUpdate also works but OnSourceChanged doesn’t until I directly set the data source and path for some reason. I’m also using Unity 6000.0.3f1.

The OnSourceChanged will update a given binding if:

  • The resolved dataSource or the resolved dataSourcePath has changed.
  • The binding is marked dirty.
  • The binding is registered during that frame.
  • If we detect a change on the resolved dataSource. When no interfaces are used, this means every frame. If the INotifyBindablePropertyChanged interface is implemented, a given binding will be updated if a change notification is sent for a property along the resolved dataSourcePath of the binding. It is important to note that a change is always detected from the root dataSource (the closest dataSource that is set) and not the dataSource at the given path. So if in your example the HandSampleVM contains a CardSetVM and the HandSampleVM is used as the dataSource, it must report also the changes of the CardSetVM in order to get the related bindings to update.

I’m also using Unity 6000.0.3f1.

It would probably be best to update to the latest version in that stream.

1 Like

Awesome, thanks for that. Looks like I did need to report the changes to the parent. For anyone else reading this I thought I’d post some notes on it.

I created two new uxml files (Main and sub). This includes bindings using update and On Source changed for multiple cases.
uxml

Without notifying the parent it wouldn’t update some of the fields (adds a dot every time I pressed ‘A’).
withoutNotify

In the case of SubData.SubTitle this wouldn’t update even with Every Update.
pathBinding

After the child tells the parent it changed everything works.
fixed

Example code of MainDataSource.cs

public class MainDataSource : DataSourceBase
{
    private string mainTitle = "Main"; 

    [CreateProperty]
    public string MainTitle
    {
        get => mainTitle;
        set
        {
            if (mainTitle == value)
            {
                return;
            }
            mainTitle = value;
            Notify();
        }
    }

    [CreateProperty]
    public SubDataSource SubData { get; set; }

    public MainDataSource(SubDataSource subData)
    {
        SubData = subData;
        subData.propertyChanged += SubData_propertyChanged;
    }

    private void SubData_propertyChanged(object sender, UnityEngine.UIElements.BindablePropertyChangedEventArgs e)
    {
        Notify(nameof(SubData));
    }
}
1 Like