diff --git a/Assets/UIComponents.Benchmarks/BenchmarkUtils.cs b/Assets/UIComponents.Benchmarks/BenchmarkUtils.cs index aa1348f2..75a8c010 100644 --- a/Assets/UIComponents.Benchmarks/BenchmarkUtils.cs +++ b/Assets/UIComponents.Benchmarks/BenchmarkUtils.cs @@ -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() { @@ -14,7 +14,7 @@ private static SampleGroup[] GetProfilerMarkers() new SampleGroup("UIComponent.DependencySetup") }; } - + public static void MeasureComponentInitWithColdCache() where TComponent : UIComponent, new() { Measure.Method(async () => diff --git a/Assets/UIComponents.Tests/LayoutAttributeTests.cs b/Assets/UIComponents.Tests/LayoutAttributeTests.cs index 7132c6d4..a2904640 100644 --- a/Assets/UIComponents.Tests/LayoutAttributeTests.cs +++ b/Assets/UIComponents.Tests/LayoutAttributeTests.cs @@ -44,6 +44,7 @@ public IEnumerator Given_Layout_Is_Loaded() { var testBed = new TestBed().WithSingleton(_mockResolver); var component = testBed.CreateComponent(); + component.Initialize(); yield return component.WaitForInitializationEnumerator(); _mockResolver.Received().LoadAsset("Assets/MyAsset.uxml"); } @@ -53,6 +54,7 @@ public IEnumerator Superclass_Layout_Is_Loaded_If_It_Is_Not_Overridden() { var testBed = new TestBed().WithSingleton(_mockResolver); var component = testBed.CreateComponent(); + component.Initialize(); yield return component.WaitForInitializationEnumerator(); _mockResolver.Received().LoadAsset("Assets/MyAsset.uxml"); } @@ -62,6 +64,7 @@ public IEnumerator Superclass_Layout_Is_Not_Loaded_If_Overridden() { var testBed = new TestBed().WithSingleton(_mockResolver); var component = testBed.CreateComponent(); + component.Initialize(); yield return component.WaitForInitializationEnumerator(); _mockResolver.Received().LoadAsset("Assets/MyOtherAsset.uxml"); _mockResolver.DidNotReceive().LoadAsset("Assets/MyAsset.uxml"); @@ -71,7 +74,8 @@ public IEnumerator Superclass_Layout_Is_Not_Loaded_If_Overridden() public void Null_Layout_Is_Handled() { var testBed = new TestBed().WithSingleton(_mockResolver); - Assert.DoesNotThrow(() => testBed.CreateComponent()); + var component = testBed.CreateComponent(); + Assert.DoesNotThrow(() => component.Initialize()); } } } diff --git a/Assets/UIComponents.Tests/QueryAttributeTests.cs b/Assets/UIComponents.Tests/QueryAttributeTests.cs index e305db15..92245d61 100644 --- a/Assets/UIComponents.Tests/QueryAttributeTests.cs +++ b/Assets/UIComponents.Tests/QueryAttributeTests.cs @@ -51,6 +51,7 @@ public IEnumerator Should_Populate_Fields() var testBed = new TestBed() .WithSingleton(_mockLogger); var component = testBed.CreateComponent(); + component.Initialize(); yield return component.WaitForInitializationEnumerator(); @@ -87,6 +88,7 @@ public IEnumerator Should_Populate_Inherited_Fields() var testBed = new TestBed() .WithSingleton(_mockLogger); var component = testBed.CreateComponent(); + component.Initialize(); yield return component.WaitForInitializationEnumerator(); @@ -115,6 +117,7 @@ public IEnumerator Should_Not_Populate_Invalid_Fields() var testBed = new TestBed() .WithSingleton(_mockLogger); var component = testBed.CreateComponent(); + component.Initialize(); yield return component.WaitForInitializationEnumerator(); @@ -141,6 +144,7 @@ public IEnumerator Should_Log_Errors_If_Query_Yields_No_Results() var testBed = new TestBed() .WithSingleton(_mockLogger); var component = testBed.CreateComponent(); + component.Initialize(); yield return component.WaitForInitializationEnumerator(); diff --git a/Assets/UIComponents.Tests/QueryClassAttributeTests.cs b/Assets/UIComponents.Tests/QueryClassAttributeTests.cs index d9930629..6bf71863 100644 --- a/Assets/UIComponents.Tests/QueryClassAttributeTests.cs +++ b/Assets/UIComponents.Tests/QueryClassAttributeTests.cs @@ -70,6 +70,7 @@ public IEnumerator UnitySetUp() var testBed = new TestBed() .WithSingleton(_mockLogger); _queryClassTestComponent = testBed.CreateComponent(); + _queryClassTestComponent.Initialize(); yield return _queryClassTestComponent.WaitForInitializationEnumerator(); } diff --git a/Assets/UIComponents.Tests/RootClassAttributeTests.cs b/Assets/UIComponents.Tests/RootClassAttributeTests.cs index 43fc606f..369b15a9 100644 --- a/Assets/UIComponents.Tests/RootClassAttributeTests.cs +++ b/Assets/UIComponents.Tests/RootClassAttributeTests.cs @@ -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); @@ -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); diff --git a/Assets/UIComponents.Tests/StylesheetAttributeTests.cs b/Assets/UIComponents.Tests/StylesheetAttributeTests.cs index 6223cd37..9f21c7b8 100644 --- a/Assets/UIComponents.Tests/StylesheetAttributeTests.cs +++ b/Assets/UIComponents.Tests/StylesheetAttributeTests.cs @@ -43,6 +43,7 @@ public IEnumerator Given_Stylesheets_Are_Loaded() .WithSingleton(_mockLogger) .WithTransient(_mockResolver); var component = testBed.CreateComponent(); + component.Initialize(); yield return component.WaitForInitializationEnumerator(); @@ -58,6 +59,7 @@ public IEnumerator Inherited_Stylesheets_Are_Loaded() .WithSingleton(_mockLogger) .WithTransient(_mockResolver); var component = testBed.CreateComponent(); + component.Initialize(); yield return component.WaitForInitializationEnumerator(); @@ -78,6 +80,7 @@ public IEnumerator Invalid_Stylesheets_Output_Error_Message() .WithTransient(_mockResolver); var component = testBed.CreateComponent(); + component.Initialize(); yield return component.WaitForInitializationEnumerator(); diff --git a/Assets/UIComponents.Tests/UIComponentEffectAttributeTests.cs b/Assets/UIComponents.Tests/UIComponentEffectAttributeTests.cs index e6f10316..ab501852 100644 --- a/Assets/UIComponents.Tests/UIComponentEffectAttributeTests.cs +++ b/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 { @@ -32,10 +34,13 @@ private partial class UIComponentWithEffects : UIComponent public readonly List AppliedEffects = new List(); } - [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)); diff --git a/Assets/UIComponents.Tests/UIComponentNoAttributesTests.cs b/Assets/UIComponents.Tests/UIComponentNoAttributesTests.cs index 1c0661ee..d10dc3aa 100644 --- a/Assets/UIComponents.Tests/UIComponentNoAttributesTests.cs +++ b/Assets/UIComponents.Tests/UIComponentNoAttributesTests.cs @@ -27,6 +27,7 @@ public IEnumerator UnitySetUp() var testBed = new TestBed() .WithSingleton(_mockResolver); _component = testBed.CreateComponent(); + _component.Initialize(); yield return _component.WaitForInitializationEnumerator(); } diff --git a/Assets/UIComponents.Tests/UIComponentTests.cs b/Assets/UIComponents.Tests/UIComponentTests.cs index 0986a111..a0475e1c 100644 --- a/Assets/UIComponents.Tests/UIComponentTests.cs +++ b/Assets/UIComponents.Tests/UIComponentTests.cs @@ -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("Layout"); @@ -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); @@ -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); @@ -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() .WithSingleton(_mockAssetResolver); var firstChild = childTestBed.CreateComponent(); + firstChild.Initialize(); var secondChild = childTestBed.CreateComponent(); + secondChild.Initialize(); component.Add(firstChild); component.Add(secondChild); @@ -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() .WithSingleton(_mockAssetResolver); var firstChild = childTestBed.CreateComponent(); + firstChild.Initialize(); var secondChild = childTestBed.CreateComponent(); + secondChild.Initialize(); var nestedChildTestBed = new TestBed() .WithSingleton(_mockAssetResolver); var nestedChild = nestedChildTestBed.CreateComponent(); + nestedChild.Initialize(); firstChild.Add(nestedChild); @@ -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() + .WithSingleton(_mockAssetResolver); + + var component = testBed.CreateComponent(); + + component.Initialize(); + component.Initialize(); + component.Initialize(); + + _mockAssetResolver.CompleteLoad("Layout"); + + yield return component.WaitForInitializationEnumerator(); + + Assert.That(component.InitCount, Is.EqualTo(1)); + } } } } diff --git a/Assets/UIComponents.Tests/UIComponents.Tests.asmdef b/Assets/UIComponents.Tests/UIComponents.Tests.asmdef index a143ee2c..05286925 100644 --- a/Assets/UIComponents.Tests/UIComponents.Tests.asmdef +++ b/Assets/UIComponents.Tests/UIComponents.Tests.asmdef @@ -15,7 +15,6 @@ "nunit.framework.dll", "NSubstitute.dll", "Castle.Core.dll", - "System.Threading.Tasks.Extensions.dll", "System.Runtime.CompilerServices.Unsafe.dll" ], "autoReferenced": false, diff --git a/Assets/UIComponents/Core/Internal/TaskExtensions.cs b/Assets/UIComponents/Core/Internal/TaskExtensions.cs index 5404f607..a1399a80 100644 --- a/Assets/UIComponents/Core/Internal/TaskExtensions.cs +++ b/Assets/UIComponents/Core/Internal/TaskExtensions.cs @@ -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; diff --git a/Assets/UIComponents/Core/UIComponent.cs b/Assets/UIComponents/Core/UIComponent.cs index 36133b5c..d26f41dc 100644 --- a/Assets/UIComponents/Core/UIComponent.cs +++ b/Assets/UIComponents/Core/UIComponent.cs @@ -35,6 +35,7 @@ public abstract class UIComponent : VisualElement, IDependencyConsumer /// Whether the UIComponent has been fully initialized. /// public bool Initialized { get; private set; } + private bool _initializationOngoing; /// /// A Task that completes when the UIComponent has been fully initialized. @@ -70,11 +71,28 @@ protected UIComponent() DependencySetupProfilerMarker.End(); + RegisterCallback(OnFirstAttachToPanel); + } + + ~UIComponent() + { + UnregisterCallback(OnFirstAttachToPanel); + UIC_UnregisterEventCallbacks(); + } + + private void OnFirstAttachToPanel(AttachToPanelEvent evt) + { Initialize(); + UnregisterCallback(OnFirstAttachToPanel); } - private async void Initialize() + public async void Initialize() { + if (Initialized || _initializationOngoing) + return; + + _initializationOngoing = true; + var layoutTask = UIC_StartLayoutLoad(); var stylesTask = GetStyleSheets(); @@ -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() {} diff --git a/Assets/UIComponents/Testing/TestBed.cs b/Assets/UIComponents/Testing/TestBed.cs index 934e29b8..0b0d055d 100644 --- a/Assets/UIComponents/Testing/TestBed.cs +++ b/Assets/UIComponents/Testing/TestBed.cs @@ -71,6 +71,7 @@ public TComponent CreateComponent(Func factoryPredicate) public async Task CreateComponentAsync(Func factoryPredicate) { var component = CreateComponent(factoryPredicate); + component.Initialize(); var initTask = component.InitializationTask; var timeoutTask = Task.Delay(AsyncTimeout);