I have some custom UI elements I made using the element first approach in this docs page. I was using Resources.Load() at first but we have since migrated to using Addressables.
I’m having some issues with initialization, because we had to move the loading of the VisualTreeAsset into an async Init() function to make it work (we have a WebGL build target, we can’t use synchronous loading).
This has cause some unintended consequences when calling some functions right after the constructor that fail because the initialization function has not completed yet.
Here is a simple example:
public class MyCustomElement : VisualElement
{
public new class UxmlFactory : UxmlFactory<MyCustomElement> { }
public MyCustomElement() : base()
{
Init();
}
private async void Init()
{
VisualTreeAsset asset = await Addressables.LoadAssetAsync<VisualTreeAsset>("TreeAsset.uxml").Task;
asset.CloneTree(this);
}
public void TroublesomeFunc()
{
this.Q<Label>().text = "Accessing VisualTree!";
}
}
In this class before I could do CloneTree() in the constructor after using Resources.Load(), and TroublesomeFunc() would run fine every time.
As it is right now if TroublesomeFunc() is called right after the constructor it might fail because it can happen before Init() is complete.
I can think of some solutions for this, like checking in TroublesomeFunc if Init has completed before executing its code, but I don’t think any of my solutions is particularly elegant. What is the best way to solve this issue?
You could WaitForCompletion() on the AsyncOperationHandle<TObject> that ``LoadAssetAsync<T> returns, which comes with the usual caveats that comes with said method.
Or you could invoke a method when said visual element has been loaded:
public class MyCustomElement : VisualElement
{
public new class UxmlFactory : UxmlFactory<MyCustomElement> { }
public MyCustomElement() : base()
{
Init();
}
private async void Init()
{
VisualTreeAsset asset = await Addressables.LoadAssetAsync<VisualTreeAsset>("TreeAsset.uxml").Task;
asset.CloneTree(this);
OnVisualContentLoaded();
}
protected virtual void OnVisualContentLoaded() { }
}
On your first suggestion, I can’t use WaitForComplation due to our WebGL build target.
As for your second idea, I’m not quite sure you understood the issue: my ThoublesomeFunc is not necessarily called right after the constructor. It might, and in that case it fails. Think like the use of a RegisterValueChangedCallback, for example. The TroublesomeFunc might be called by other scripts whenever, and inside MyCustomElement this needs to be handled and made safe.
I imagine in that case, TroublesomeFunc simply does some work that in cached in this visual element, and applied to the inner VisualTreeAsset IF it is currently present, or when the OnVisualContentLoaded method is called.
Spit-ball idea:
public abstract class VisualTreeAssetElement : VisualElement
{
private VisualElement _contentContainer = null;
protected abstract string VisualTreeAssetPath { get; }
public override VisualElement contentContainer => _contentContainer;
public MyCustomElement() : base()
{
Init();
}
private async void Init()
{
string path = VisualTreeAssetPath;
VisualTreeAsset asset = await Addressables.LoadAssetAsync<VisualTreeAsset>(path).Task;
_contentContainer = asset.Instantiate();
this.hierarchy.Add(_contentContainer);
OnVisualContentLoaded();
}
protected virtual void OnVisualContentLoaded() { }
}
public class SomeFancyLabelElement : VisualTreeAssetElement
{
protected override string VisualTreeAssetPath => "ExamplePath/ExampleVisualTreeAsset.uxml";
private string _text;
public string Text
{
get => _text;
set
{
_text = value;
if (contentContainer != null)
{
ApplyText();
}
}
}
public SomeFancyLabelElement() : base() { }
public new class UxmlFactory : UxmlFactory<SomeFancyLabelElement> { }
protected override void OnVisualContentLoaded()
{
ApplyText();
}
private void ApplyText()
{
var label = contentContainer.Q<Label>();
label.text = _text;
}
}
Might not be 100% correct; wrote it in notepad++.
Might be a better way but as I work solo I don’t use visual tree assets as prefabs/templates like this; I just code all my custom visual elements and use style sheets (namely theme style sheets) to style them globally.
Caching in the intended operation in case of it not being initialized and doing it later is indeed a solution, and I considered it, I was looking for something a bit more elegant.
As for @spiney199’s code example above, it’s overly complicated and does not solve the issue. There is no need to bring in inheritance to a simple example, and ApplyText, which correct me if I’m wrong is meant to be equivalent to my TroublesomeFunc is still just called when Init is over (your OnVisualContentLoaded), or then you set the Text attribute, which is not what I asked for.
The setting of a label was used as a simple demonstration of accessing the VisualTree. TroublesomeFunc is a representative of a more complicated function that accesses the VisualTree, not simply setting a label text. It is important that it happens only if it is called, not at the end of Init regardless if the call happened or not.
One solution for this issue would be:
public class MyCustomElement : VisualElement
{
public new class UxmlFactory : UxmlFactory<MyCustomElement> { }
public MyCustomElement() : base()
{
Init();
}
private async void Init()
{
VisualTreeAsset asset = await Addressables.LoadAssetAsync<VisualTreeAsset>("TreeAsset.uxml").Task;
asset.CloneTree(this);
if (runLater)
TroublesomeFunc();
}
private bool runLater = false;
public void TroublesomeFunc()
{
// checking if Init ran. Might be done differently
if (hierarchy.childCount == 0)
runLater = true;
else
this.Q<Label>().text = "Accessing VisualTree!";
}
}
This example is merely demonstrative. Depending on specifics of the implementation, TroublesomeFunc can be called on an event instead at the end of Init (like already shown above by spiney199), the runLater bool can be a list of another data type if this can happen to multiple “troublesome fuctions” or if they can have arguments, and the child count as a shortcut for Init being done can be something else.
My question was never “How do I do this” it was “How do I do this well”.
To build upon the previous suggestion I would suggest holding a reference to the AsyncOperation. You can then check if its completed in TroublesomeFunc and even force it to complete if you wish.
public class MyCustomElement : VisualElement
{
AsyncOperation<VisualTreeAsset> m_LoadingOperation;
public MyCustomElement() : base()
{
Init();
}
private void Init()
{
m_LoadingOperation = Addressables.LoadAssetAsync<VisualTreeAsset>("TreeAsset.uxml");
if (!m_LoadingOperation.isDone)
{
m_LoadingOperation.Completed += FinishInit;
return;
}
FinishInit(m_LoadingOperation);
}
void FinishInit(AsyncOperation<VisualTreeAsset> op)
{
op.Result.CloneTree(this);
}
public void TroublesomeFunc()
{
// Option 1 - Force completion
if (!m_LoadingOperation.isDone)
{
m_LoadingOperation.WaitForCompletion();
this.Q<Label>().text = "Accessing VisualTree!";
}
// Option 2 - wait
if (!m_LoadingOperation.isDone)
m_LoadingOperation.Completed += op => this.Q<Label>().text = "Accessing VisualTree!";
}
}
Holding a reference to the operation and subscribing to it’s completion is much better than my suggestion, thanks! I believe we’ll have to go with option 2 in TroublesomeFunc due to the WebGL constraints.
One thing I didn’t quite get is why you separated what was my Init function into Init and FinishInit.
Was there an issue with this?
I used the Completed callback instead of await as I was also using it later on in TroublesomeFunc. I was trying to keep it simple by using the same approach in both areas. Both approaches are valid.