Maybe my solution is a bit over-engineered, but well now I have implemented it and it works. So for each UI document I have one UXML file and one monobehaviour script. The source generator generates two more (partial) classes: an InterfaceView class and an InterfaceController class.
This is an UXML file:
<ui:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
<VindemiatrixCollective.UIElements.Components.Containers.ViewModelPanel name="panel-constraints" view-model-type="VindemiatrixCollective.SineFine.Interface.UIElements.Design.ShipDesignModel" class="flex-grow">
<VindemiatrixCollective.UIElements.Components.Containers.Sidebar name="sidebar-constraints" use-parent-with-class="window" semantic-type="Neutral" class="badge-outline-info flex-grow">
<VindemiatrixCollective.UIElements.Components.Containers.Accordion name="accordion-constraints" expanded="true" semantic-type="Primary" text="Constraints" class="mb-2 text-xl">
<ui:ListView name="list-constraints" allow-add="false" allow-remove="false" item-template="project://database/Assets/Resources/UI/Panels/ShipDesign/ShipConstraintItem.uxml?fileID=9197481963319205126&guid=e4b77017ff49033448c812d71570a0dd&type=3#ShipConstraintItem" binding-source-selection-mode="AutoAssign" fixed-item-height="50" focusable="false" />
</VindemiatrixCollective.UIElements.Components.Containers.Accordion>
<VindemiatrixCollective.UIElements.Components.Selection.Dropdown />
<VindemiatrixCollective.UIElements.Components.Buttons.IconButton size="WideLarge" icon="Download" label="Save" label-text="Save" label-visible="true" name="btn-save" text="Save" command="SaveAction" class="wide3-xs mt-auto" style="self: center;" />
</VindemiatrixCollective.UIElements.Components.Containers.Sidebar>
</VindemiatrixCollective.UIElements.Components.Containers.ViewModelPanel>
</ui:UXML>
This is the generated view class:
// <auto-generated/>
using UnityEngine.Assertions;
using UnityEngine.UIElements;
using VindemiatrixCollective.Common.Interfaces;
using VindemiatrixCollective.UIElements.Components.Buttons;
using VindemiatrixCollective.UIElements.Components.Containers;
namespace VindemiatrixCollective.SineFine.Interface.UIElements.Generated
{
public partial class ShipConstraintsPanelView : IInterfaceView {
public VisualElement Container { get; }
internal ViewModelPanel PanelConstraints { get; private set; }
internal Sidebar SidebarConstraints { get; private set; }
internal Accordion AccordionConstraints { get; private set; }
internal ListView ListConstraints { get; private set; }
internal IconButton BtnSave { get; private set; }
public ShipConstraintsPanelView(VisualElement rootContainer)
{
Assert.IsNotNull(rootContainer, nameof(rootContainer));
Container = rootContainer.Q<VisualElement>("panel-constraints");;
PanelConstraints = (ViewModelPanel)Container;
SidebarConstraints = Container.Q<Sidebar>("sidebar-constraints");
AccordionConstraints = Container.Q<Accordion>("accordion-constraints");
ListConstraints = Container.Q<ListView>("list-constraints");
BtnSave = Container.Q<IconButton>("btn-save");
PostInitialisation();
}
partial void PostInitialisation();
}
}
rootContainer is the instantiated VisualTreeAsset of the UI. As you can see the source generator creates a statement for each named element in the UXML file and automatically finds the corresponding element via a `Q(name) call. The UXML files are passed via a csc.rsp file as “additionalFiles” to the source generator (is there a better way?) and it then parses the uxml files to find elements of note. Named elements are converted from dash-case to PascalCase.
There is also a generated Controller class like this below:
// <auto-generated/>
using UnityEngine.Assertions;
using VindemiatrixCollective.Common.Interfaces;
using VindemiatrixCollective.SineFine.Interface.UIElements.Design;
namespace VindemiatrixCollective.SineFine.Interface.UIElements.Generated
{
public partial class ShipConstraintsPanelController : IController {
public ShipConstraintsPanelView View { get; }
public ShipDesignModel ShipDesignModel { get; }
public ShipConstraintsPanelController(ShipConstraintsPanelView view, ShipDesignModel shipDesignModel)
{
Assert.IsNotNull(view, nameof(View));
Assert.IsNotNull(shipDesignModel, nameof(ShipDesignModel));
View = view;
ShipDesignModel = shipDesignModel;
PostInitialisation();
}
public void RegisterCallbacks()
{
View.BtnSave.clicked += SaveAction;
}
partial void PostInitialisation();
partial void SaveAction();
IInterfaceView IController.View { get; }
}
}
which also finds elements like buttons that have a “command” attribute filled in, which assigns a method having as name the value of the attribute to the clicked event of a button and creates a partial method so that I am reminded to write the function handling it. The benefit of all this is that now there is no possibility of ever referencing a named VisualElement that does not exist.
The “xxxModel” is another part of my workflow. Essentially I am using a “context” object that is shared by multiple UI panels. I have a monobehaviour + View/Controller for every distinct UI panel. You can see here what the code I pasted here refers to in action. This is in case I might later decide to reuse or rearrange UI panels in other parts of the interface.
With that “Model” object shared by multiple panels I allow each to read/write to relevant parts of the model. That’s why I would like to have the possibility of specifying data-xxx attributes in the UXML, because in this specific case I am declaring the type of the Model object as an attribute to a custom VisualElement object which is not ideal. I could get it from the type of the datasource, but in this instance the type of the datasource of the UXML file is not the same as the type of the model.
I also have yet another source-generator for ViewModel classes which implements interfaces like IDataSourceViewHashProvider, INotifyBindablePropertyChanged because I did not want to “litter” my model classes with tons of attributes or unity-specific code. So all of the code that follows is generated via a specific “GenerateViewModel” attribute on the original class that specifies which properties I want to include in the VM class:
// <auto-generated/>
using System;
using System.Runtime.CompilerServices;
using Unity.Properties;
using UnityEngine.UIElements;
using VindemiatrixCollective.SineFine.Core.Ships;
namespace VindemiatrixCollective.SineFine.Core.Ships
{
public class ShipConstraintVM : IDataSourceViewHashProvider, INotifyBindablePropertyChanged {
private long _viewVersion;
private ShipConstraint _source;
private Severity _severity;
public event EventHandler<BindablePropertyChangedEventArgs> propertyChanged;
public ShipConstraint Source
{
get => _source;
}
[CreateProperty]
public Severity Severity
{
get => _severity;
set
{
if (_severity == value)
return;
_severity = value;
Notify();
}
}
[CreateProperty]
public string Description
{
get => _source.Description;
}
public ShipConstraintVM(ShipConstraint source)
{
this.Severity = source.Severity;
_source = source;
}
private void Notify([CallerMemberName] string property = "")
{
propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(property));
}
public void Publish()
{
++_viewVersion;
}
public long GetViewHashCode()
{
return HashCode.Combine(_viewVersion, _source, _severity);
}
}
}
Hope this helps. If you would like and think it could speed up the inclusion of such features in Unity, I can share the source generator project.
edit: reposting as I replied to the wrong person.