Skip to content

Commit

Permalink
feat: initialize UIComponents on first attach to panel
Browse files Browse the repository at this point in the history
BREAKING CHANGE: UIComponents no longer start their initialization in the inherited constructor. Instead, it is started when they are first attached to a panel. Alternatively, the newly exposed Initialize method can be called to start initialization.
  • Loading branch information
jonisavo committed May 8, 2023
1 parent bdfadaf commit e8a2c60
Show file tree
Hide file tree
Showing 13 changed files with 104 additions and 13 deletions.
4 changes: 2 additions & 2 deletions Assets/UIComponents.Benchmarks/BenchmarkUtils.cs
Expand Up @@ -5,7 +5,7 @@ namespace UIComponents.Benchmarks
{
public static class BenchmarkUtils
{
public const string Version = "0.29.0.0";
public const string Version = "0.30.0.0";

private static SampleGroup[] GetProfilerMarkers()
{
Expand All @@ -14,7 +14,7 @@ private static SampleGroup[] GetProfilerMarkers()
new SampleGroup("UIComponent.DependencySetup")
};
}

public static void MeasureComponentInitWithColdCache<TComponent>() where TComponent : UIComponent, new()
{
Measure.Method(async () =>
Expand Down
6 changes: 5 additions & 1 deletion Assets/UIComponents.Tests/LayoutAttributeTests.cs
Expand Up @@ -44,6 +44,7 @@ public IEnumerator Given_Layout_Is_Loaded()
{
var testBed = new TestBed<UIComponentWithLayout>().WithSingleton(_mockResolver);
var component = testBed.CreateComponent();
component.Initialize();
yield return component.WaitForInitializationEnumerator();
_mockResolver.Received().LoadAsset<VisualTreeAsset>("Assets/MyAsset.uxml");
}
Expand All @@ -53,6 +54,7 @@ public IEnumerator Superclass_Layout_Is_Loaded_If_It_Is_Not_Overridden()
{
var testBed = new TestBed<InheritedComponentWithoutAttribute>().WithSingleton(_mockResolver);
var component = testBed.CreateComponent();
component.Initialize();
yield return component.WaitForInitializationEnumerator();
_mockResolver.Received().LoadAsset<VisualTreeAsset>("Assets/MyAsset.uxml");
}
Expand All @@ -62,6 +64,7 @@ public IEnumerator Superclass_Layout_Is_Not_Loaded_If_Overridden()
{
var testBed = new TestBed<InheritedComponentWithAttribute>().WithSingleton(_mockResolver);
var component = testBed.CreateComponent();
component.Initialize();
yield return component.WaitForInitializationEnumerator();
_mockResolver.Received().LoadAsset<VisualTreeAsset>("Assets/MyOtherAsset.uxml");
_mockResolver.DidNotReceive().LoadAsset<VisualTreeAsset>("Assets/MyAsset.uxml");
Expand All @@ -71,7 +74,8 @@ public IEnumerator Superclass_Layout_Is_Not_Loaded_If_Overridden()
public void Null_Layout_Is_Handled()
{
var testBed = new TestBed<UIComponentWithNullLayout>().WithSingleton(_mockResolver);
Assert.DoesNotThrow(() => testBed.CreateComponent());
var component = testBed.CreateComponent();
Assert.DoesNotThrow(() => component.Initialize());
}
}
}
4 changes: 4 additions & 0 deletions Assets/UIComponents.Tests/QueryAttributeTests.cs
Expand Up @@ -51,6 +51,7 @@ public IEnumerator Should_Populate_Fields()
var testBed = new TestBed<ComponentWithQueryAttribute>()
.WithSingleton(_mockLogger);
var component = testBed.CreateComponent();
component.Initialize();

yield return component.WaitForInitializationEnumerator();

Expand Down Expand Up @@ -87,6 +88,7 @@ public IEnumerator Should_Populate_Inherited_Fields()
var testBed = new TestBed<ChildComponentWithQueryAttribute>()
.WithSingleton(_mockLogger);
var component = testBed.CreateComponent();
component.Initialize();

yield return component.WaitForInitializationEnumerator();

Expand Down Expand Up @@ -115,6 +117,7 @@ public IEnumerator Should_Not_Populate_Invalid_Fields()
var testBed = new TestBed<ComponentWithInvalidQueryAttribute>()
.WithSingleton(_mockLogger);
var component = testBed.CreateComponent();
component.Initialize();

yield return component.WaitForInitializationEnumerator();

Expand All @@ -141,6 +144,7 @@ public IEnumerator Should_Log_Errors_If_Query_Yields_No_Results()
var testBed = new TestBed<ComponentWithMissingFields>()
.WithSingleton(_mockLogger);
var component = testBed.CreateComponent();
component.Initialize();

yield return component.WaitForInitializationEnumerator();

Expand Down
1 change: 1 addition & 0 deletions Assets/UIComponents.Tests/QueryClassAttributeTests.cs
Expand Up @@ -70,6 +70,7 @@ public IEnumerator UnitySetUp()
var testBed = new TestBed<QueryClassTestComponent>()
.WithSingleton(_mockLogger);
_queryClassTestComponent = testBed.CreateComponent();
_queryClassTestComponent.Initialize();
yield return _queryClassTestComponent.WaitForInitializationEnumerator();
}

Expand Down
2 changes: 2 additions & 0 deletions Assets/UIComponents.Tests/RootClassAttributeTests.cs
Expand Up @@ -14,6 +14,7 @@ private partial class ComponentWithRootClass : UIComponent {}
public IEnumerator Adds_Class_To_Component()
{
var component = new ComponentWithRootClass();
component.Initialize();
yield return component.WaitForInitializationEnumerator();

Assert.That(component.ClassListContains("test-class"), Is.True);
Expand All @@ -27,6 +28,7 @@ private partial class ChildComponentWithRootClass : ComponentWithRootClass {}
public IEnumerator Adds_Class_To_Component_And_Child_Component()
{
var component = new ChildComponentWithRootClass();
component.Initialize();
yield return component.WaitForInitializationEnumerator();

Assert.That(component.ClassListContains("test-class"), Is.True);
Expand Down
3 changes: 3 additions & 0 deletions Assets/UIComponents.Tests/StylesheetAttributeTests.cs
Expand Up @@ -43,6 +43,7 @@ public IEnumerator Given_Stylesheets_Are_Loaded()
.WithSingleton(_mockLogger)
.WithTransient(_mockResolver);
var component = testBed.CreateComponent();
component.Initialize();

yield return component.WaitForInitializationEnumerator();

Expand All @@ -58,6 +59,7 @@ public IEnumerator Inherited_Stylesheets_Are_Loaded()
.WithSingleton(_mockLogger)
.WithTransient(_mockResolver);
var component = testBed.CreateComponent();
component.Initialize();

yield return component.WaitForInitializationEnumerator();

Expand All @@ -78,6 +80,7 @@ public IEnumerator Invalid_Stylesheets_Output_Error_Message()
.WithTransient(_mockResolver);

var component = testBed.CreateComponent();
component.Initialize();

yield return component.WaitForInitializationEnumerator();

Expand Down
9 changes: 7 additions & 2 deletions Assets/UIComponents.Tests/UIComponentEffectAttributeTests.cs
@@ -1,6 +1,8 @@
using System;
using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine.TestTools;

namespace UIComponents.Tests
{
Expand Down Expand Up @@ -32,10 +34,13 @@ private partial class UIComponentWithEffects : UIComponent
public readonly List<int> AppliedEffects = new List<int>();
}

[Test]
public void Effects_Are_Sorted_By_Priority()
[UnityTest]
public IEnumerator Effects_Are_Sorted_By_Priority()
{
var component = new UIComponentWithEffects();
component.Initialize();

yield return component.WaitForInitializationEnumerator();

Assert.That(component.AppliedEffects.Count, Is.EqualTo(4));
Assert.That(component.AppliedEffects[0], Is.EqualTo(999));
Expand Down
1 change: 1 addition & 0 deletions Assets/UIComponents.Tests/UIComponentNoAttributesTests.cs
Expand Up @@ -27,6 +27,7 @@ public IEnumerator UnitySetUp()
var testBed = new TestBed<UIComponentNoAttributes>()
.WithSingleton(_mockResolver);
_component = testBed.CreateComponent();
_component.Initialize();
yield return _component.WaitForInitializationEnumerator();
}

Expand Down
57 changes: 57 additions & 0 deletions Assets/UIComponents.Tests/UIComponentTests.cs
Expand Up @@ -63,6 +63,7 @@ public void SetUp()
public void Sets_The_Initialized_Value()
{
var component = _testComponentTestBed.CreateComponent();
component.Initialize();
Assert.That(component.Initialized, Is.False);

_mockAssetResolver.CompleteLoad<VisualTreeAsset>("Layout");
Expand All @@ -76,6 +77,7 @@ public void Sets_The_Initialized_Value()
public void Completes_Initialization_Task()
{
var component = _testComponentTestBed.CreateComponent();
component.Initialize();

Assert.That(component.InitializationTask.IsCompleted, Is.False);

Expand All @@ -91,6 +93,7 @@ public void Completes_Initialization_Task()
public IEnumerator Allows_Waiting_For_Initialization()
{
var component = _testComponentTestBed.CreateComponent();
component.Initialize();

Assert.That(component.Initialized, Is.False);

Expand All @@ -114,12 +117,15 @@ private partial class NestedChildComponent : UIComponent {}
public void Does_Not_Initialize_If_Children_Are_Uninitialized()
{
var component = _testComponentTestBed.CreateComponent();
component.Initialize();

var childTestBed = new TestBed<ChildComponent>()
.WithSingleton<IAssetResolver>(_mockAssetResolver);

var firstChild = childTestBed.CreateComponent();
firstChild.Initialize();
var secondChild = childTestBed.CreateComponent();
secondChild.Initialize();

component.Add(firstChild);
component.Add(secondChild);
Expand All @@ -135,17 +141,21 @@ public void Does_Not_Initialize_If_Children_Are_Uninitialized()
public void Initializes_When_Children_Are_Initialized()
{
var component = _testComponentTestBed.CreateComponent();
component.Initialize();

var childTestBed = new TestBed<ChildComponent>()
.WithSingleton<IAssetResolver>(_mockAssetResolver);

var firstChild = childTestBed.CreateComponent();
firstChild.Initialize();
var secondChild = childTestBed.CreateComponent();
secondChild.Initialize();

var nestedChildTestBed = new TestBed<NestedChildComponent>()
.WithSingleton<IAssetResolver>(_mockAssetResolver);

var nestedChild = nestedChildTestBed.CreateComponent();
nestedChild.Initialize();

firstChild.Add(nestedChild);

Expand All @@ -170,8 +180,55 @@ private partial class BareTestComponent : UIComponent {}
public void Bare_Component_Initializes_Synchronously()
{
var component = new BareTestComponent();

component.Initialize();

Assert.That(component.Initialized, Is.True);
}

private partial class BareInitCounterComponent : UIComponent
{
public int InitCount { get; private set; }

public override void OnInit()
{
InitCount++;
}
}

[Test]
public void Synchronous_Initialization_Happens_Once()
{
var component = new BareInitCounterComponent();

component.Initialize();
component.Initialize();
component.Initialize();

Assert.That(component.InitCount, Is.EqualTo(1));
}

[Layout("Layout")]
private partial class InitCounterComponent : BareInitCounterComponent {}

[UnityTest]
public IEnumerator Asynchronous_Initialization_Happens_Once()
{
var testBed = new TestBed<InitCounterComponent>()
.WithSingleton<IAssetResolver>(_mockAssetResolver);

var component = testBed.CreateComponent();

component.Initialize();
component.Initialize();
component.Initialize();

_mockAssetResolver.CompleteLoad<VisualTreeAsset>("Layout");

yield return component.WaitForInitializationEnumerator();

Assert.That(component.InitCount, Is.EqualTo(1));
}
}
}
}
1 change: 0 additions & 1 deletion Assets/UIComponents.Tests/UIComponents.Tests.asmdef
Expand Up @@ -15,7 +15,6 @@
"nunit.framework.dll",
"NSubstitute.dll",
"Castle.Core.dll",
"System.Threading.Tasks.Extensions.dll",
"System.Runtime.CompilerServices.Unsafe.dll"
],
"autoReferenced": false,
Expand Down
2 changes: 1 addition & 1 deletion Assets/UIComponents/Core/Internal/TaskExtensions.cs
Expand Up @@ -17,7 +17,7 @@ public static IEnumerator AsEnumerator(this Task task)

if (task.IsFaulted)
{
ExceptionDispatchInfo.Capture(task.Exception).Throw();
ExceptionDispatchInfo.Capture(task.Exception!).Throw();
}

yield return null;
Expand Down
26 changes: 20 additions & 6 deletions Assets/UIComponents/Core/UIComponent.cs
Expand Up @@ -35,6 +35,7 @@ public abstract class UIComponent : VisualElement, IDependencyConsumer
/// Whether the UIComponent has been fully initialized.
/// </summary>
public bool Initialized { get; private set; }
private bool _initializationOngoing;

/// <summary>
/// A Task that completes when the UIComponent has been fully initialized.
Expand Down Expand Up @@ -70,11 +71,28 @@ protected UIComponent()

DependencySetupProfilerMarker.End();

RegisterCallback<AttachToPanelEvent>(OnFirstAttachToPanel);
}

~UIComponent()
{
UnregisterCallback<AttachToPanelEvent>(OnFirstAttachToPanel);
UIC_UnregisterEventCallbacks();
}

private void OnFirstAttachToPanel(AttachToPanelEvent evt)
{
Initialize();
UnregisterCallback<AttachToPanelEvent>(OnFirstAttachToPanel);
}

private async void Initialize()
public async void Initialize()
{
if (Initialized || _initializationOngoing)
return;

_initializationOngoing = true;

var layoutTask = UIC_StartLayoutLoad();
var stylesTask = GetStyleSheets();

Expand Down Expand Up @@ -108,14 +126,10 @@ private async void Initialize()
OnInit();

Initialized = true;
_initializationOngoing = false;
_initCompletionSource.SetResult(this);
}

~UIComponent()
{
UIC_UnregisterEventCallbacks();
}

protected virtual void UIC_RegisterEventCallbacks() {}

protected virtual void UIC_UnregisterEventCallbacks() {}
Expand Down
1 change: 1 addition & 0 deletions Assets/UIComponents/Testing/TestBed.cs
Expand Up @@ -71,6 +71,7 @@ public TComponent CreateComponent(Func<TComponent> factoryPredicate)
public async Task<TComponent> CreateComponentAsync(Func<TComponent> factoryPredicate)
{
var component = CreateComponent(factoryPredicate);
component.Initialize();

var initTask = component.InitializationTask;
var timeoutTask = Task.Delay(AsyncTimeout);
Expand Down

0 comments on commit e8a2c60

Please sign in to comment.