diff --git a/samples/HostedInAspNet.Client/Program.cs b/samples/HostedInAspNet.Client/Program.cs index 9feb4dceb6..bcd36e2473 100644 --- a/samples/HostedInAspNet.Client/Program.cs +++ b/samples/HostedInAspNet.Client/Program.cs @@ -14,7 +14,7 @@ namespace HostedInAspNet.Client { // Temporarily render this test component until there's a proper mechanism // for testing this. - new BrowserRenderer().AddComponent("app", new MyComponent()); + new BrowserRenderer().AddComponent("app"); } } diff --git a/samples/StandaloneApp/Program.cs b/samples/StandaloneApp/Program.cs index 06d043f1fa..e4863042a8 100644 --- a/samples/StandaloneApp/Program.cs +++ b/samples/StandaloneApp/Program.cs @@ -9,8 +9,7 @@ namespace StandaloneApp { public static void Main(string[] args) { - new BrowserRenderer() - .AddComponent("app", new Home()); + new BrowserRenderer().AddComponent("app"); } } } diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs index 7f6c09c1d7..49d711bc8c 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs +++ b/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs @@ -18,12 +18,6 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering { private readonly int _browserRendererId; - // Ensures the explicitly-added components aren't GCed, because the browser - // will still send events referencing them by ID. We only need to store the - // top-level components, because the associated ComponentState will reference - // all the reachable descendant components of each. - private IList _rootComponents = new List(); - /// /// Constructs an instance of . /// @@ -38,22 +32,33 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering internal void RenderNewBatchInternal(int componentId) => RenderNewBatch(componentId); + /// + /// Attaches a new root component to the renderer, + /// causing it to be displayed in the specified DOM element. + /// + /// The type of the component. + /// A CSS selector that uniquely identifies a DOM element. + public void AddComponent(string domElementSelector) + where TComponent: IComponent + { + AddComponent(typeof(TComponent), domElementSelector); + } + /// /// Associates the with the , /// causing it to be displayed in the specified DOM element. /// + /// The type of the component. /// A CSS selector that uniquely identifies a DOM element. - /// The . - public void AddComponent(string domElementSelector, IComponent component) + public void AddComponent(Type componentType, string domElementSelector) { + var component = InstantiateComponent(componentType); var componentId = AssignComponentId(component); RegisteredFunction.InvokeUnmarshalled( "attachComponentToElement", _browserRendererId, domElementSelector, componentId); - _rootComponents.Add(component); - RenderNewBatch(componentId); } diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs index 6c16a78380..1ca2b1ff5f 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs @@ -10,36 +10,6 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree { internal static class RenderTreeDiffBuilder { - private struct DiffContext - { - // Exists only so that the various methods in this class can call each other without - // constantly building up long lists of parameters. Is private to this class, so the - // fact that it's a mutable struct is manageable. - // Always pass by ref to avoid copying, and because the 'SiblingIndex' is mutable. - - public readonly Renderer Renderer; - public readonly RenderBatchBuilder BatchBuilder; - public readonly RenderTreeFrame[] OldTree; - public readonly RenderTreeFrame[] NewTree; - public readonly ArrayBuilder Edits; - public readonly ArrayBuilder ReferenceFrames; - public int SiblingIndex; - - public DiffContext( - Renderer renderer, - RenderBatchBuilder batchBuilder, - RenderTreeFrame[] oldTree, RenderTreeFrame[] newTree) - { - Renderer = renderer; - BatchBuilder = batchBuilder; - OldTree = oldTree; - NewTree = newTree; - Edits = batchBuilder.EditsBuffer; - ReferenceFrames = batchBuilder.ReferenceFramesBuffer; - SiblingIndex = 0; - } - } - public static RenderTreeDiff ComputeDiff( Renderer renderer, RenderBatchBuilder batchBuilder, @@ -419,7 +389,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree { if (oldFrame.AttributeEventHandlerId > 0) { - diffContext.BatchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId); + diffContext.BatchBuilder.DisposedEventHandlerIds.Append(oldFrame.AttributeEventHandlerId); } InitializeNewAttributeFrame(ref diffContext, ref newFrame); var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame); @@ -490,7 +460,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree diffContext.Edits.Append(RenderTreeEdit.RemoveAttribute(diffContext.SiblingIndex, oldFrame.AttributeName)); if (oldFrame.AttributeEventHandlerId > 0) { - diffContext.BatchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId); + diffContext.BatchBuilder.DisposedEventHandlerIds.Append(oldFrame.AttributeEventHandlerId); } break; } @@ -568,7 +538,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree throw new InvalidOperationException($"Child component already exists during {nameof(InitializeNewComponentFrame)}"); } - diffContext.Renderer.InstantiateChildComponent(ref frame); + diffContext.Renderer.InstantiateChildComponentOnFrame(ref frame); var childComponentInstance = frame.Component; // All descendants of a component are its properties @@ -604,9 +574,42 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree } else if (frame.FrameType == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerId > 0) { - batchBuilder.AddDisposedEventHandlerId(frame.AttributeEventHandlerId); + batchBuilder.DisposedEventHandlerIds.Append(frame.AttributeEventHandlerId); } } } + + /// + /// Exists only so that the various methods in this class can call each other without + /// constantly building up long lists of parameters. Is private to this class, so the + /// fact that it's a mutable struct is manageable. + /// + /// Always pass by ref to avoid copying, and because the 'SiblingIndex' is mutable. + /// + private struct DiffContext + { + public readonly Renderer Renderer; + public readonly RenderBatchBuilder BatchBuilder; + public readonly RenderTreeFrame[] OldTree; + public readonly RenderTreeFrame[] NewTree; + public readonly ArrayBuilder Edits; + public readonly ArrayBuilder ReferenceFrames; + public int SiblingIndex; + + public DiffContext( + Renderer renderer, + RenderBatchBuilder batchBuilder, + RenderTreeFrame[] oldTree, + RenderTreeFrame[] newTree) + { + Renderer = renderer; + BatchBuilder = batchBuilder; + OldTree = oldTree; + NewTree = newTree; + Edits = batchBuilder.EditsBuffer; + ReferenceFrames = batchBuilder.ReferenceFramesBuffer; + SiblingIndex = 0; + } + } } } diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs index 33a03669ad..d6922ecca6 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs @@ -35,11 +35,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering _renderTreeBuilderPrevious = new RenderTreeBuilder(renderer); } - /// - /// Regenerates the and adds the changes to the - /// . - /// - public void Render(Renderer renderer, RenderBatchBuilder batchBuilder) + public void RenderIntoBatch(RenderBatchBuilder batchBuilder) { if (_component is IHandlePropertiesChanged notifyableComponent) { @@ -59,19 +55,9 @@ namespace Microsoft.AspNetCore.Blazor.Rendering _renderTreeBuilderPrevious.GetFrames(), _renderTreeBuilderCurrent.GetFrames()); batchBuilder.UpdatedComponentDiffs.Append(diff); - - // Process disposal queue now in case it causes further component renders to be enqueued - while (batchBuilder.ComponentDisposalQueue.Count > 0) - { - var disposeComponentId = batchBuilder.ComponentDisposalQueue.Dequeue(); - renderer.DisposeInExistingBatch(batchBuilder, disposeComponentId); - } } - /// - /// Notifies the component that it is being disposed. - /// - public void NotifyDisposed(RenderBatchBuilder batchBuilder) + public void DisposeInBatch(RenderBatchBuilder batchBuilder) { // TODO: Handle components throwing during dispose. Shouldn't break the whole render batch. if (_component is IDisposable disposable) diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs index c6ca306f1e..7f6b6f2a9a 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs @@ -6,45 +6,41 @@ using Microsoft.AspNetCore.Blazor.RenderTree; namespace Microsoft.AspNetCore.Blazor.Rendering { + /// + /// Collects the data produced by the rendering system during the course + /// of rendering a single batch. This tracks both the final output data + /// and the intermediate states (such as the queue of components still to + /// be rendered). + /// internal class RenderBatchBuilder { + // Primary result data + public ArrayBuilder UpdatedComponentDiffs { get; } = new ArrayBuilder(); + public ArrayBuilder DisposedComponentIds { get; } = new ArrayBuilder(); + public ArrayBuilder DisposedEventHandlerIds { get; } = new ArrayBuilder(); + + // Buffers referenced by UpdatedComponentDiffs public ArrayBuilder EditsBuffer { get; } = new ArrayBuilder(); public ArrayBuilder ReferenceFramesBuffer { get; } = new ArrayBuilder(); + // State of render pipeline public Queue ComponentRenderQueue { get; } = new Queue(); - public Queue ComponentDisposalQueue { get; } = new Queue(); - public ArrayBuilder UpdatedComponentDiffs { get; set; } - = new ArrayBuilder(); - - private readonly ArrayBuilder _disposedComponentIds = new ArrayBuilder(); - - private readonly ArrayBuilder _disposedEventHandlerIds = new ArrayBuilder(); - - public ArrayRange GetDisposedEventHandlerIds() - => _disposedEventHandlerIds.ToRange(); - public void Clear() { EditsBuffer.Clear(); ReferenceFramesBuffer.Clear(); ComponentRenderQueue.Clear(); UpdatedComponentDiffs.Clear(); - _disposedComponentIds.Clear(); - _disposedEventHandlerIds.Clear(); + DisposedComponentIds.Clear(); + DisposedEventHandlerIds.Clear(); } public RenderBatch ToBatch() => new RenderBatch( UpdatedComponentDiffs.ToRange(), ReferenceFramesBuffer.ToRange(), - _disposedComponentIds.ToRange()); - - public void AddDisposedComponentId(int componentId) - => _disposedComponentIds.Append(componentId); - - public void AddDisposedEventHandlerId(int attributeEventHandlerId) - => _disposedEventHandlerIds.Append(attributeEventHandlerId); + DisposedComponentIds.ToRange()); } } diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs index 9d9b761071..b47d135a7d 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.RenderTree; using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Threading; namespace Microsoft.AspNetCore.Blazor.Rendering @@ -16,15 +15,9 @@ namespace Microsoft.AspNetCore.Blazor.Rendering /// public abstract class Renderer { - // Methods for tracking associations between component IDs, instances, and states, - // without pinning any of them in memory here. The explictly GC rooted items are the - // components explicitly added to the renderer (i.e., top-level components). In turn - // these reference descendant components and associated ComponentState instances. - private readonly WeakValueDictionary _componentStateById - = new WeakValueDictionary(); - private readonly ConditionalWeakTable _componentStateByComponent - = new ConditionalWeakTable(); private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it + private readonly Dictionary _componentStateById + = new Dictionary(); // Because rendering is currently synchronous and single-threaded, we can keep re-using the // same RenderBatchBuilder instance to avoid reallocating @@ -35,35 +28,46 @@ namespace Microsoft.AspNetCore.Blazor.Rendering private readonly Dictionary _eventHandlersById = new Dictionary(); + /// + /// Constructs a new component of the specified type. + /// + /// The type of the component to instantiate. + /// The component instance. + protected IComponent InstantiateComponent(Type componentType) + { + if (!typeof(IComponent).IsAssignableFrom(componentType)) + { + throw new ArgumentException($"Must implement {nameof(IComponent)}", nameof(componentType)); + } + + return (IComponent)Activator.CreateInstance(componentType); + } + /// /// Associates the with the , assigning /// an identifier that is unique within the scope of the . /// - /// The . - /// The assigned identifier for the . + /// The component. + /// The component's assigned identifier. protected int AssignComponentId(IComponent component) { - lock (_componentStateById) - { - var componentId = _nextComponentId++; - var componentState = new ComponentState(this, componentId, component); - _componentStateById.Add(componentId, componentState); - _componentStateByComponent.Add(component, componentState); // Ensure the componentState lives for at least as long as the component - return componentId; - } + var componentId = _nextComponentId++; + var componentState = new ComponentState(this, componentId, component); + _componentStateById.Add(componentId, componentState); + return componentId; } /// /// Updates the visible UI. /// /// The changes to the UI since the previous call. - internal protected abstract void UpdateDisplay(RenderBatch renderBatch); + protected abstract void UpdateDisplay(RenderBatch renderBatch); /// /// Updates the rendered state of the specified . /// /// The identifier of the to render. - protected internal void RenderNewBatch(int componentId) + protected void RenderNewBatch(int componentId) { // It's very important that components' rendering logic has no side-effects, and in particular // components must *not* trigger Render from inside their render logic, otherwise you could @@ -84,11 +88,11 @@ namespace Microsoft.AspNetCore.Blazor.Rendering "Render logic must not have side-effects such as manually triggering other rendering."); } + _sharedRenderBatchBuilder.ComponentRenderQueue.Enqueue(componentId); + try { - RenderInExistingBatch(_sharedRenderBatchBuilder, componentId); - - // Process + // Process render queue until empty while (_sharedRenderBatchBuilder.ComponentRenderQueue.Count > 0) { var nextComponentIdToRender = _sharedRenderBatchBuilder.ComponentRenderQueue.Dequeue(); @@ -96,24 +100,15 @@ namespace Microsoft.AspNetCore.Blazor.Rendering } UpdateDisplay(_sharedRenderBatchBuilder.ToBatch()); - RemoveEventHandlerIds(_sharedRenderBatchBuilder.GetDisposedEventHandlerIds()); } finally { + RemoveEventHandlerIds(_sharedRenderBatchBuilder.DisposedEventHandlerIds.ToRange()); _sharedRenderBatchBuilder.Clear(); Interlocked.Exchange(ref _renderBatchLock, 0); } } - internal void RenderInExistingBatch(RenderBatchBuilder batchBuilder, int componentId) - => GetRequiredComponentState(componentId).Render(this, batchBuilder); - - internal void DisposeInExistingBatch(RenderBatchBuilder batchBuilder, int componentId) - { - GetRequiredComponentState(componentId).NotifyDisposed(batchBuilder); - batchBuilder.AddDisposedComponentId(componentId); - } - /// /// Notifies the specified component that an event has occurred. /// @@ -136,7 +131,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering } } - internal void InstantiateChildComponent(ref RenderTreeFrame frame) + internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame) { if (frame.FrameType != RenderTreeFrameType.Component) { @@ -148,7 +143,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering throw new ArgumentException($"The frame already has a non-null component instance", nameof(frame)); } - var newComponent = (IComponent)Activator.CreateInstance(frame.ComponentType); + var newComponent = InstantiateComponent(frame.ComponentType); var newComponentId = AssignComponentId(newComponent); frame = frame.WithComponentInstance(newComponentId, newComponent); } @@ -160,6 +155,25 @@ namespace Microsoft.AspNetCore.Blazor.Rendering frame = frame.WithAttributeEventHandlerId(id); } + private ComponentState GetRequiredComponentState(int componentId) + => _componentStateById.TryGetValue(componentId, out var componentState) + ? componentState + : throw new ArgumentException($"The renderer does not have a component with ID {componentId}."); + + private void RenderInExistingBatch(RenderBatchBuilder batchBuilder, int componentId) + { + GetRequiredComponentState(componentId).RenderIntoBatch(batchBuilder); + + // Process disposal queue now in case it causes further component renders to be enqueued + while (batchBuilder.ComponentDisposalQueue.Count > 0) + { + var disposeComponentId = batchBuilder.ComponentDisposalQueue.Dequeue(); + GetRequiredComponentState(disposeComponentId).DisposeInBatch(batchBuilder); + _componentStateById.Remove(disposeComponentId); + batchBuilder.DisposedComponentIds.Append(disposeComponentId); + } + } + private void RemoveEventHandlerIds(ArrayRange eventHandlerIds) { var array = eventHandlerIds.Array; @@ -169,10 +183,5 @@ namespace Microsoft.AspNetCore.Blazor.Rendering _eventHandlersById.Remove(array[i]); } } - - private ComponentState GetRequiredComponentState(int componentId) - => _componentStateById.TryGetValue(componentId, out var componentState) - ? componentState - : throw new ArgumentException($"The renderer does not have a component with ID {componentId}."); } } diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs index 64ed61224e..6d00b77159 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs @@ -297,7 +297,7 @@ namespace Microsoft.AspNetCore.Blazor.Test private class TestRenderer : Renderer { - protected internal override void UpdateDisplay(RenderBatch renderBatch) + protected override void UpdateDisplay(RenderBatch renderBatch) => throw new NotImplementedException(); } } diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs index 8e302a4572..7b6a59c059 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs @@ -785,7 +785,7 @@ namespace Microsoft.AspNetCore.Blazor.Test private class FakeRenderer : Renderer { - internal protected override void UpdateDisplay(RenderBatch renderBatch) + protected override void UpdateDisplay(RenderBatch renderBatch) { } } diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs index 8c1f719dcd..6b16735276 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs @@ -275,78 +275,6 @@ namespace Microsoft.AspNetCore.Blazor.Test Assert.True(renderer2.Batches.Single().DiffsByComponentId.ContainsKey(renderer2ComponentId)); } - [Fact] - public void ComponentsAreNotPinnedInMemory() - { - // It's important that the Renderer base class does not itself pin in memory - // any of the component instances that were attached to it (or by extension, - // their descendants). This is because as the set of active components changes - // over time, we need the GC to be able to release unused ones, and there isn't - // any other mechanism for explicitly destroying components when they stop - // being referenced. - var renderer = new NoOpRenderer(); - - AssertCanBeCollected(() => - { - var component = new TestComponent(null); - renderer.AssignComponentId(component); - return component; - }); - } - - [Fact] - public void CannotRenderComponentsIfGCed() - { - // Arrange - var renderer = new NoOpRenderer(); - - // Act - var componentId = new Func(() => - { - var component = new TestComponent(builder => - throw new NotImplementedException("Should not be invoked")); - - return renderer.AssignComponentId(component); - })(); - - // Since there are no unit test references to 'component' here, the GC - // should be able to collect it - GC.Collect(); - GC.WaitForPendingFinalizers(); - - // Assert - Assert.ThrowsAny(() => - { - renderer.RenderNewBatch(componentId); - }); - } - - [Fact] - public void CanRenderComponentsIfNotGCed() - { - // Arrange - var renderer = new NoOpRenderer(); - var didRender = false; - - // Act - var component = new TestComponent(builder => - { - didRender = true; - }); - var componentId = renderer.AssignComponentId(component); - - // Unlike the preceding test, we still have a reference to the component - // instance on the stack here, so the following should not cause it to - // be collected. Then when we call RenderComponent, there should be no error. - GC.Collect(); - GC.WaitForPendingFinalizers(); - - renderer.RenderNewBatch(componentId); - - // Assert - Assert.True(didRender); - } - [Fact] public void PreservesChildComponentInstancesWithNoAttributes() { @@ -721,7 +649,7 @@ namespace Microsoft.AspNetCore.Blazor.Test public new void RenderNewBatch(int componentId) => base.RenderNewBatch(componentId); - protected internal override void UpdateDisplay(RenderBatch renderBatch) + protected override void UpdateDisplay(RenderBatch renderBatch) { } } @@ -740,7 +668,7 @@ namespace Microsoft.AspNetCore.Blazor.Test public new void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs args) => base.DispatchEvent(componentId, eventHandlerId, args); - protected internal override void UpdateDisplay(RenderBatch renderBatch) + protected override void UpdateDisplay(RenderBatch renderBatch) { var capturedBatch = new CapturedBatch(); Batches.Add(capturedBatch); @@ -879,17 +807,6 @@ namespace Microsoft.AspNetCore.Blazor.Test } } - void AssertCanBeCollected(Func targetFactory) - { - // We have to construct the WeakReference in a separate scope - // otherwise its target won't be collected on this GC cycle - var weakRef = new Func( - () => new WeakReference(targetFactory()))(); - GC.Collect(); - GC.WaitForPendingFinalizers(); - Assert.Null(weakRef.Target); - } - (int, T) FirstWithIndex(IEnumerable items, Predicate predicate) { var index = 0; diff --git a/test/testapps/BasicTestApp/Program.cs b/test/testapps/BasicTestApp/Program.cs index 351a8bd491..c8b87d4968 100644 --- a/test/testapps/BasicTestApp/Program.cs +++ b/test/testapps/BasicTestApp/Program.cs @@ -19,8 +19,7 @@ namespace BasicTestApp public static void MountTestComponent(string componentTypeName) { var componentType = Type.GetType(componentTypeName); - var componentInstance = (IComponent)Activator.CreateInstance(componentType); - new BrowserRenderer().AddComponent("app", componentInstance); + new BrowserRenderer().AddComponent(componentType, "app"); } } }