Suggestion: UxmlElementFactoryAttribute for override CreateInstance method in UxmlSerializedData

Hello, dears.
My suggestion very simple. Add method attribute and make source code generation dependent on it. User can override CreateInstance and make custom factory for custom visual elements.

// User code
[UxmlElement]
public partial class MyElement : VisualElement
{
    [UxmlElementFactory] // <- new attribute
    public static MyElement CreateInstance() => new MyElement();
}

// Generated by source generator
public partial class MyElement 
{
    [global::System.Runtime.CompilerServices.CompilerGenerated]
    [global::System.Serializable]
    public new class UxmlSerializedData : UnityEngine.UIElements.VisualElement.UxmlSerializedData
    {
        public override object CreateInstance() => MyElement.CreateInstance();
    }
}

Are you suggesting an addition to the UxmlElement feature?

Your example code is what we already generate with UxmlElement.

You want to control the Create instance method? What would be the use case for returning anything other than the same type?

Yes! Feature looks like “Control CreateInstance method”

Yes, i want to control CreateInstance method. Basicly for resolve dependencies and pass parameters into constructor without re-creating any of elements.

Can you give me an example of how this would work?
The system passes parameters from the uxml attributes, when would you need to pass different parameters into the constructor?

No problem. One of many examples is an ad button with states depending on the ad state, internet reachability state, and premium state.

[UxmlElement]
public partial class AdsButton : VisualElement
{
    [UxmlAttribute]
    public string Placement { get; set; }

    [UxmlElementFactory]
    public static AdsButton CreateInstance() => DependencyInjection.Resolve<AdsButton>();

    public AdsButton(IAdsManager adsManager, IInternetReachabilityManager internetReachabilityManager, IPremiumManager premiumManager) {
        _adsManager = adsManager;
        _internetReachabilityManager = internetReachabilityManager;
        _premiumManager = premiumManager;

        _adsManager.RewardedReady += RewardedReadyHandler;
        _internetReachabilityManager.ReachabilityStateChanged += ReachabilityStateChangedHandler;
        _premiumManager.PremiumPurchased += PremiumPurchasedHandler;
    }

    private void RewardedReadyHandler() => UpdateView();
    private void ReachabilityStateChangedHandler(bool isReachable) => UpdateView();
    private void PremiumPurchasedHandler() => UpdateView();

    private void UpdateView() {
        // Show/hide "no internet icon" if internet not reachable
        // Show spinner if rewarded not ready
        // Show "Free" is premium purchased
    }
}

This is one of thousands of examples that use dependency resolving and parameter passing to constructors. Developers can’t make singletons on each path of game. Custom factories open up endless possibilities for implementing any ideas.

Thank you @karl_jones for your quick responses.

2 Likes

We need the value returned from CreateInstance to be consistent, it should always be the same type and have the same values otherwise we cannot detect the default values for our attribute overrides system.
Its worth pointing out that CreateInstance is only used when creating the element from UXML, the purpose of this method is to do new T(), there should not be any additional logic going on in here. If you want to have different versions of the class then this should be done through the UXML by setting the element attributes.

For example

<UXML>
    <Department name="Dunder Mifflin" />
</UXML>

This would find the UxmlElement called Department. It would use CreateInstance to create a new default instance and then apply the overridden name attribute to the new instance(using Deserialize). This way we always get the same result from the UXML.
If you want to create different behaviours then you would either inherit from Department, use its attributes or add UxmlObjects support.

You can still have a constructor that takes parameters in your element, the default constructor is required for the UXML serialization but there’s no reason you cant have more for when you want to create them via script, such as for creating a singleton.

There is a draft of a blog post that goes into more details on the new UXML serialization system here.

I think you may be particularly interested in the UxmlObject feature, it sounds like it fits closer to your concept of factories and changing behaviours in elements. In your example I would make _adsManager, _internetReachabilityManager and _internetReachabilityManager UxmlObjects. You can then customize the object reference.

Then you could change the behaviour through UXML, it could look like this:

<UXML>
    <AdsButton>
       <addsManager>
           <MyCustomAddsManager/>
       </addsManager>
   </AdsButton>
</UXML>

or

<UXML>
    <AdsButton>
       <addsManager>
           <MyDifferentCustomAddsManager some-value="something"/>
       </addsManager>
   </AdsButton>
</UXML>

These UxmlObjects can have different behaviours. They could have actions/events that could be subscribed to in the property set method, similar to your example.
You can also modify the element after creation via UXML, such as doing root.Q<MyElement>() and then applying changes via script.

Ok @karl_jones, I tried the two approaches you suggested. It took some time. Both are amazing, but don’t help resolve dependencies. Dependency injection should use constructors or inject dependencies without additional code from the developer.

In my example, all the “managers” are just classes that cannot be serializable and are not VisualElements. Now we have only one option - find all custom elements and resolve dependencies used method or property injection. This approach is quite slow and labor-intensive and I wanted to avoid it.

Maybe if I understand your problem more I can suggest something else.
What would be happening inside of DependencyInjection.Resolve<AdsButton>();
Will you be changing the references passed into the constructor?
Is this some form of initialization? The AttachToPanel event is very good for initializing.

This is pseudocode. DependencyInjection.Resolve<T>() return instance of T with resolved constructor parameters.

Yes. Dependent on context (like scope, like scene, like state and anything else).

How can I get IAdsManager inside a custom VisualElement? I have 15+ AdsButton placements inside different uxml templates. Some templates are created later, some in dynamic lists, some static. How to pass IAdsManager into each AdsButton? No singleton and no search root.Q<AdsButton>().SetAdsManager(). IAdsManager implementation - non serialized class.

In general question sounds like: How can i use dependency injection container with UI Toolkit and inject dependencies in custom VisualElement?

↓↓↓ I want to write code like this ↓↓↓

[UxmlElement]
public partial class AdsButton : VisualElement
{
    [UxmlAttribute]
    public string Placement { get; set;   }
    // How?
    private IAdsManager _adsManager;
    
    public AdsButton()
    {
         RegisterCallback<ClickEvent>(ClickEventHandler);
    }

    private void ClickEventHandler(ClickEvent evt)
    {
        _adsManager.ShowRewardedVideo(Placement);
    }
}

Thank you @karl_jones for your involvement =)

Is there any reason that the Resolve method could not be called somewhere else? Does it have to be in the constructor? If you have some logic that can find the adsManager can it not also be called inside of the default constructor or the AttachToPanel event?
Alternatively could the dependency injection be done to a class instance field?

Something like this:

[UxmlElement]
public partial class AdsButton : VisualElement
{
    [UxmlAttribute]
    public string Placement { get; set;   }

    MyClass myDependencyInstance;
    
    public AdsButton()
    {
         myDependencyInstance = dependencyInjection.Resolve<MyClass>();
         RegisterCallback<ClickEvent>(ClickEventHandler);
    }

    private void ClickEventHandler(ClickEvent evt)
    {
        myDependencyInstance._adsManager.ShowRewardedVideo(Placement);
    }
}

In your example how can i get dependencyInjection in each AdsButton?

Constructor and before AttachToPanel - the good place for resolve dependencies.

How would you have gotten it in your original example? I would expect it to work in the same way you planned to do it previously.

My suggestion is to take whatever DependencyInjection.Resolve<AdsButton>(); does and move it into the constructor so it creates a different instance which can be used by the element.

Will you show an example?

That’s difficult because I don’t know what DependencyInjection.Resolve() does, I’m just copying your example code :wink:
How would your version have worked?

I can make real demo on github for you. Give me some time

1 Like

@karl_jones i make simple demo for you! You can find it here!
↓↓↓ Simple structure for better understanding of the topic. Only one service and only one button ↓↓↓


AdsButton_1 - this is a button for you. You can pass IAdsService to this button, as you imagine implementing dependency injection.
AdsButton_2 - this is a button as I see a possible implementation.

Thank you =)

1 Like

Thanks.

I see your example is using a singleton, I thought it could not?
How would you solve this problem when using a MonoBehavior or ScriptableObject?

One way to solve this would be to introduce a Wrapper or Controller element, this is persistent and driven by UXML. It can then create non-persistent elements, we have many of our own elements that do the same thing.

[UxmlElement]
partial class MyButtonWrapper : VisualElement
{
    Button button;

    public MyWrapper()
    {
        if (ComponentFactory.Instance == null)
        {
            button = new AdsButton_2();
        }
        else
        {
            button = ComponentFactory.Instance.Create<AdsButton_2>();
        }

        Add(button);
    }
}

Alternatively wrap the data into its own instance class like I suggested previously:

[UxmlElement]
public partial class AdsButton : VisualElement
{
    [UxmlAttribute]
    public string Placement { get; set;   }

    class MyClass
    {
        public IAdsManager _adsManager;

        public MyClass(AdsManager adsManager)
        {
            _adsManager = adsManager;
        }
    }

    MyClass myDependencyInstance;
    
    public AdsButton()
    {
         myDependencyInstance = dependencyInjection.Resolve<MyClass>();
         RegisterCallback<ClickEvent>(ClickEventHandler);
    }

    private void ClickEventHandler(ClickEvent evt)
    {
        myDependencyInstance._adsManager.ShowRewardedVideo(Placement);
    }
}

We have no plans to allow people to change the CreateInstance method, it would create far too many problems for very little gains. What would happen if you returned a completely different type or changed the field values.

Yes. I can’t find a better solution without your help in creating elements using factories. Why did you abandon factories in favor of sources generated code?

Doesn’t this approach create unnecessary elements? This will create difficulties when laying out the document. Applying styles to non-persistent elements will also create difficulties. But at the same time, this is the best solution after manual resolution via script.

Each project has thousands of scripts and thousands of elements. Every custom VisualElement should use a singleton for the dependency injection container - this is bad practice. Thousands of hard links to a singleton is exactly what you want to avoid.

Another approach is to use UxmlAttributeConverter and UxmlAttribute. I can write source code generator which create converters automatically for every registered services. But UxmlAttributeConverter<>.FromString(string value) is not called at runtime and not called for non-serialized classes.

@karl_jones I have a “last chance” solution, but I don’t know how to set custom data to the panel before calling AttachToPanelEvent or constructor on the custom VisualElement.

// This code did not work because the document is not ready yet
 _documentRoot.rootVisualElement.userData = container.Resolve<IComponentResolver>();
_documentRoot.gameObject.SetActive(true);
using UnityEngine;
using UnityEngine.UIElements;

[UxmlElement]
public partial class AdsButton_1 : Button
{
    [UxmlResolve] // <- Custom attribute for custom source code generator
    private IAdsService _adsService;

    public AdsButton_1() : this(true)
    {
        this.clicked += OnClick;
    }

    private void OnClick()
    {
        if (_adsService != null)
        {
            _adsService.ShowRewarded("placement", (success) => { Debug.Log("ShowRewarded callback: " + success); });
        }
    }

    // ↓↓↓ THIS CODE SHOULD BE GENERATED BY CUSTOM SOURCE CODE GENERATOR ↓↓↓ 
    public AdsButton_1(bool resolve)
    {
        if (Application.isPlaying is false || resolve is false) return;

        RegisterCallback<AttachToPanelEvent>(ResolveDependencies);
    }

    private void ResolveDependencies(AttachToPanelEvent evt)
    {
        // How to set userData before `AttachToPanelEvent` invoked?
        if (this.panel.visualTree.userData is IComponentResolver resolver)
        {
            _adsService = resolver.Resolve<IAdsService>();
        }
    }
}

The source code generation is to replace all the boilerplate code that factories required. Factories were never designed to solve your issue either.

No this is fine to do. Like I said we do it as well. You would apply styles to non-persistent elements in the same way you do it to persistent ones, through style sheets with classes. I would not recommend using inline styles for anything than very simple things, they are hard to maintain.
E.G

button = ComponentFactory.Instance.Create<AdsButton_2>();
button.AddToClassList("my-class-element");

I understand that but I never said you should use a singleton, im using the code you provided in the example. If you were using the old UxmlFactory system how would you solve it without a singleton?
I still cant understand how being able to control the construction of the element avoids using a singleton and how it can not be done in another part of code instead of the constructor. How would you solve this with MonoBehaviors or ScriptableObjects?

What do you mean by the document not being ready? Is this a UIDocument?
I believe the document is setup during OnEnable, you could try adjusting your script execution order so UIDocument executes before yours or you may just need to call SetActive first.

Direct question. How can i get userData from custom VisualElement in constructor or AttachToPanelEvent?

First example

public class DemoScope : LifetimeScope
{
    [SerializeField] private UIDocument _documentRoot;
    
    private void Awake()
    {
       // Null referece exception
        _documentRoot.rootVisualElement.userData = "My user data";
    }
}

[UxmlElement]
public partial class AdsButton : Button
{
    public AdsButton() 
    {
        Debug.Log(this.panel.visualTree.userData); // <- null

        RegisterCallback<AttachToPanelEvent>(AttachToPanelEventHandler);
    }

    private void AttachToPanelEventHandler(AttachToPanelEvent evt)
    {
        Debug.Log(this.panel.visualTree.userData); // <- null
    }
}

Second example

[DefaultExecutionOrder(100)]
public class DemoScope : LifetimeScope
{
    [SerializeField] private UIDocument _documentRoot;
    
    private void OnEnable()
    {
        // Document initialized and already has rootVisualElement. All right.
        _documentRoot.rootVisualElement.userData = "My user data";
    }
}

[UxmlElement]
public partial class AdsButton : Button
{
    public AdsButton() 
    {
        Debug.Log(this.panel.visualTree.userData); // <- null

        RegisterCallback<AttachToPanelEvent>(AttachToPanelEventHandler);
    }

    private void AttachToPanelEventHandler(AttachToPanelEvent evt)
    {
        Debug.Log(this.panel.visualTree.userData); // <- null
    }
}