Is it possible to use commands in UI Toolkit like WPF?

I tried runtime data binding and it was great.

Naturally, I wondered if there was a way to bind a Button to a Command in UXML, even an asynchronous Command, just like WPF.

It would be even better if I could combine it with MVVM Toolkit to automatically generate boilerplate code

Am I missing any documentation? Is this possible now?

I found this repository that can achieve this function.
I wonder if Unity officials plan to implement it in the future?

https://github.com/LibraStack/UnityMvvmToolkit?tab=readme-ov-file#command–commandt

Code similar to this

<ui:UXML xmlns:uitk="UnityMvvmToolkit.UITK.BindableUIElements" ...>
    <uitk:BindableLabel binding-text-path="Count" />
    <uitk:BindableButton command="IncrementCommand" />
</ui:UXML>

Hi @KuanMi,

It’s not something we currently support directly, but it can be achieved using a CustomBinding by registering callbacks on the element from OnActivated/OnDeactivated. There is some boilerplate to be handled to achieve this, but until we support it directly, this is your best bet.

1 Like

We also have an example of using UxmlObjects to assign different behaviours to a button.
See ButtonBehaviour Unity - Scripting API: UxmlObjectReferenceAttribute

I was also wondering if App UI itself would support command binding in UXML too.
From the documentation, Commanding patterns are here, but just by code for now.

https://docs.unity3d.com/Packages/com.unity.dt.app-ui@2.0/manual/mvvm-command.html

Tried to make my own

[UxmlObject]
public partial class CommandBinding : CustomBinding, IDataSourceProvider
{
    public object dataSource => throw new NotImplementedException();

    [CreateProperty]
    public PropertyPath dataSourcePath { get; private set; }

    [UxmlAttribute("command")]
    public string DataSourcePathString
    {
        get => dataSourcePath.ToString();
        set => dataSourcePath = new PropertyPath(value);
    }

    protected override void OnActivated(in BindingActivationContext context)
    {
        var source = context.targetElement.dataSource;
        var path = context.targetElement.dataSourcePath;

        if (source == null || !PropertyContainer.TryGetValue(ref source, in path, out RelayCommand command))
        {
            return;
        }

        context.targetElement.RegisterCallback<ClickEvent>(evt => command.Execute());
    }
}
<engine:Button text="Button">
    <Bindings>
        <CommandBinding command="DeleteProfileCommand" />
    </Bindings>
</engine:Button>

But when adding to UXML I get this error:

Assets/UI/Views/LoadGameWindow.uxml (19,18): Semantic - Uxml object has an invalid child element: CommandBinding
UnityEditor.AssetPostprocessingInternal:PostprocessAllAssets (string[],string[],string[],string[],string[],bool)

Any ideas what I did wrong?

Maybe you forgot to fill in the namespace?
I forgot just now. It is more convenient to use “Add Binding” in UI Builder

You’re right! Still not working tho, seems like I can’t just register callback on any element with binding, because right now it gives error that I haven’t provided any property to bind to.

You’re right! Still not working tho, seems like I can’t just register callback on any element with binding, because right now it gives error that I haven’t provided any property to bind to.

Yeah, you need to set property with a unique id. It’s unfortunately named, but every bindings needs to have an id.

public object dataSource => throw new NotImplementedException();

I’m guessing you have already updated this, otherwise this would also give a lot of issue.

Right now I’m thinking about something like this:

<engine:VisualElement> // Could be any element
    <EventTrigger event-name="ClickEvent"> // Could be any keyboard or mouse events
        <Bindings>
            <CommandBinding property="event" command="SomeCommand" />
        </Bindings>
    </EventTrigger>
</engine:VisualElement>

With this design it would be possible to add any mouse or keyboard event to any element and trigger command without writing any code. Now I just need to learn how custom bindings are working to make this haha

1 Like

First rough working proof of concept!!!

[UxmlObject]
public partial class CommandBinding : CustomBinding, IDataSourceProvider
{
    public CommandBinding()
    {
        updateTrigger = BindingUpdateTrigger.OnSourceChanged;
    }

    public object dataSource { get; set; }

    [CreateProperty]
    public PropertyPath dataSourcePath { get; private set; }

    [UxmlAttribute("command")]
    public string DataSourcePathString
    {
        get => dataSourcePath.ToString();
        set => dataSourcePath = new PropertyPath(value);
    }

    protected override void OnDataSourceChanged(in DataSourceContextChanged context)
    {
        EventTrigger trigger = context.targetElement as EventTrigger;

        var source = context.newContext.dataSource;
        var path = context.newContext.dataSourcePath;

        if (source == null || !PropertyContainer.TryGetValue(ref source, in path, out RelayCommand command))
        {
            return;
        }

        trigger.Command = command;
    }
}
[UxmlElement]
public partial class EventTrigger : VisualElement
{
    public static readonly BindingId CommandProperty = "command";

    private RelayCommand _command;

    public EventTrigger()
    {
        RegisterCallback<AttachToPanelEvent>(OnAttachToPanel);
        RegisterCallback<DetachFromPanelEvent>(OnDetachFromPanel);
    }

    [CreateProperty]
    public RelayCommand Command
    {
        get => _command;
        set
        {
            if (_command == value)
            {
                return;
            }

            _command = value;
            NotifyPropertyChanged(in CommandProperty);
        }
    }

    [UxmlAttribute]
    public string EventName { get; set; }

    private void OnAttachToPanel(AttachToPanelEvent evt)
    {
        switch (EventName)
        {
            case "ClickEvent":
                parent.RegisterCallback<ClickEvent>(evt => _command.Execute());
                break;
            default:
                break;
        }
    }

    private void OnDetachFromPanel(DetachFromPanelEvent evt)
    {
        UnregisterCallback<AttachToPanelEvent>(OnAttachToPanel);
        UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanel);
    }
}

Use it like this:

<engine:Button text="Button">
    <Project.MVVM.EventTrigger event-name="ClickEvent">
        <Bindings>
            <Project.MVVM.CommandBinding property="command" command="DeleteProfileCommand" />
        </Bindings>
    </Project.MVVM.EventTrigger>
</engine:Button>

In this example it will call DeleteProfileCommand relay command located inside dataSource’s ViewModel.

1 Like

I’m open for suggestions how this code can be improved :slightly_smiling_face:

1 Like

Is there a way to make sure binding is initialized before element’s OnAttachToPanel called? I’m getting null ref because my EventTrigger’s OnAttachToPanel is running before binding is setting Command on it.

I want to implement it directly on the custom Button class and configure it conveniently in UI Builder.
Like this:

Now I wrote the following code, but how do I know when the DataSource is changed?

[UxmlElement]
public partial class CommandButton : Button
{
    private IRelayCommand _command;

    [UxmlAttribute]
    public Type CommandDataSourceType { get; set; }

    [CommandDrawer, UxmlAttribute]
    public string CommandStr;

    // !!! There is no suitable method for override
    protected override void OnDataSourceChanged(in DataSourceContextChanged context)
    {
        var dataSource = context.newContext.dataSource;
    
        if (dataSource == null)
        {
            return;
        }
    
        var com = CommandDataSourceType.GetProperty(CommandStr)?.GetValue(dataSource) as IRelayCommand;
        if (com == null)
        {
            return;
        }
    
        _command = com;
    }

    public CommandButton()
    {
        clicked += OnClick;
    }

    void OnClick()
    {
        _command?.Execute(null);
    }
}

I even tried using reflection to get the PropertyChangedEvent event, but this only gets notified when this element’s DataSource is changed, not when its parent is assigned a value.

        public CommandButton()
        {
            Type propertyChangedEventType =
                Type.GetType("UnityEngine.UIElements.PropertyChangedEvent, UnityEngine.UIElementsModule");

            if (propertyChangedEventType == null)
            {
                Debug.LogError("PropertyChangedEvent not found.");
                return;
            }

            var allMethods = typeof(Button).GetMethods();
            
            MethodInfo registerCallbackMethod = allMethods.FirstOrDefault(f => f.Name == "RegisterCallback")
                ?.MakeGenericMethod(propertyChangedEventType);

            if (registerCallbackMethod == null)
            {
                Debug.LogError("RegisterCallback not found.");
                return;
            }

            // Define the callback with a lambda that matches the expected delegate
            Delegate callback = Delegate.CreateDelegate(
                typeof(EventCallback<>).MakeGenericType(propertyChangedEventType),
                this,
                typeof(CommandButton).GetMethod(nameof(OnPropertyChanged), BindingFlags.Instance | BindingFlags.NonPublic)
            );

            registerCallbackMethod.Invoke(this, new object[] { callback, TrickleDown.NoTrickleDown });

            clicked += OnClick;
        }
        private void OnPropertyChanged(EventBase evt)
        {
            Debug.Log("OnPropertyChanged");
            
            // var propertyChangedEvent = evt as PropertyChangedEvent;
            
            Type propertyChangedEventType =
                Type.GetType("UnityEngine.UIElements.PropertyChangedEvent, UnityEngine.UIElementsModule");

            if (propertyChangedEventType == null)
            {
                Debug.LogError("PropertyChangedEvent not found.");
                return;
            }
            
            var propertyField = propertyChangedEventType.GetProperty("property");
            var property = propertyField.GetValue(evt);
            
            var bindingId = (BindingId) property;
            
            Debug.Log("OnPropertyChanged " + bindingId);

        }

I used an ugly class to implement :smiling_face_with_tear:

    [UxmlObject]
    public partial class AutoUpdateCommandBinding : CustomBinding
    {
        protected override void OnDataSourceChanged(in DataSourceContextChanged context)
        {
            if (context.targetElement is CommandButton commandButton) 
                commandButton.OnDataSourceChanged(context);
        }
    }

The OnDataSourceChanged callback is called asynchronously on a binding instance when the resolved data source context (data source + data source path) has changed. We are only caching the resolved data source context on active binding instances and not every visual element, which would have a high cost in term of performance.

If you want to use the dataSource/dataSourcePath from the CommandButton directly, you could do it directly from the OnClick method, doing something like this:

void OnClick()
{
    var context = GetHierarchicalDataSourceContext();
    if (context.dataSource == null)
        return;
    if (PropertyContainer.TryGetValue(context.dataSource, context.dataSourcePath, out IRelayCommand command))
    {
        command?.Execute(null);
    }
}

Hope this helps!

2 Likes