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.
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.
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.
<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
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.
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);
}
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);
}
}