diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs index 49d711bc8c..2752515df9 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs +++ b/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs @@ -29,9 +29,6 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering internal void DispatchBrowserEvent(int componentId, int eventHandlerId, UIEventArgs eventArgs) => DispatchEvent(componentId, eventHandlerId, eventArgs); - 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. @@ -59,7 +56,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering _browserRendererId, domElementSelector, componentId); - RenderNewBatch(componentId); + component.SetParameters(ParameterCollection.Empty); } /// diff --git a/src/Microsoft.AspNetCore.Blazor/Components/ParameterCollection.cs b/src/Microsoft.AspNetCore.Blazor/Components/ParameterCollection.cs index e2e92be52e..1c8df0dbe4 100644 --- a/src/Microsoft.AspNetCore.Blazor/Components/ParameterCollection.cs +++ b/src/Microsoft.AspNetCore.Blazor/Components/ParameterCollection.cs @@ -11,6 +11,14 @@ namespace Microsoft.AspNetCore.Blazor.Components /// public readonly struct ParameterCollection { + private static readonly RenderTreeFrame[] _emptyCollectionFrames = new RenderTreeFrame[] + { + RenderTreeFrame.Element(0, string.Empty).WithComponentSubtreeLength(1) + }; + + private static readonly ParameterCollection _emptyCollection + = new ParameterCollection(_emptyCollectionFrames, 0); + private readonly RenderTreeFrame[] _frames; private readonly int _ownerIndex; @@ -20,6 +28,11 @@ namespace Microsoft.AspNetCore.Blazor.Components _ownerIndex = ownerIndex; } + /// + /// Gets an empty . + /// + public static ParameterCollection Empty => _emptyCollection; + /// /// Returns an enumerator that iterates through the . /// diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs index 5bfb2088b2..e1e14e28aa 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs @@ -64,52 +64,6 @@ namespace Microsoft.AspNetCore.Blazor.Rendering /// The changes to the UI since the previous call. protected abstract void UpdateDisplay(RenderBatch renderBatch); - /// - /// Updates the rendered state of the specified . - /// - /// The identifier of the to render. - 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 - // easily get hard-to-debug infinite loops. - // Since rendering is currently synchronous and single-threaded, we can enforce the above by - // checking here that no other rendering process is already underway. This also means we only - // need a single _renderBatchBuilder instance that can be reused throughout the lifetime of - // the Renderer instance, which also means we're not allocating on a typical render cycle. - // In the future, if rendering becomes async, we'll need a more sophisticated system of - // capturing successive diffs from each component and probably serializing them for the - // interop calls instead of using shared memory. - - // Note that Monitor.TryEnter is not yet supported in Mono WASM, so using the following instead - var renderAlreadyRunning = Interlocked.CompareExchange(ref _renderBatchLock, 1, 0) == 1; - if (renderAlreadyRunning) - { - throw new InvalidOperationException("Cannot render while a render is already in progress. " + - "Render logic must not have side-effects such as manually triggering other rendering."); - } - - _sharedRenderBatchBuilder.ComponentRenderQueue.Enqueue(componentId); - - try - { - // Process render queue until empty - while (_sharedRenderBatchBuilder.ComponentRenderQueue.Count > 0) - { - var nextComponentIdToRender = _sharedRenderBatchBuilder.ComponentRenderQueue.Dequeue(); - RenderInExistingBatch(_sharedRenderBatchBuilder, nextComponentIdToRender); - } - - UpdateDisplay(_sharedRenderBatchBuilder.ToBatch()); - } - finally - { - RemoveEventHandlerIds(_sharedRenderBatchBuilder.DisposedEventHandlerIds.ToRange()); - _sharedRenderBatchBuilder.Clear(); - Interlocked.Exchange(ref _renderBatchLock, 0); - } - } - /// /// Notifies the specified component that an event has occurred. /// @@ -171,6 +125,48 @@ namespace Microsoft.AspNetCore.Blazor.Rendering ? componentState : throw new ArgumentException($"The renderer does not have a component with ID {componentId}."); + private 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 + // easily get hard-to-debug infinite loops. + // Since rendering is currently synchronous and single-threaded, we can enforce the above by + // checking here that no other rendering process is already underway. This also means we only + // need a single _renderBatchBuilder instance that can be reused throughout the lifetime of + // the Renderer instance, which also means we're not allocating on a typical render cycle. + // In the future, if rendering becomes async, we'll need a more sophisticated system of + // capturing successive diffs from each component and probably serializing them for the + // interop calls instead of using shared memory. + + // Note that Monitor.TryEnter is not yet supported in Mono WASM, so using the following instead + var renderAlreadyRunning = Interlocked.CompareExchange(ref _renderBatchLock, 1, 0) == 1; + if (renderAlreadyRunning) + { + throw new InvalidOperationException("Cannot render while a render is already in progress. " + + "Render logic must not have side-effects such as manually triggering other rendering."); + } + + _sharedRenderBatchBuilder.ComponentRenderQueue.Enqueue(componentId); + + try + { + // Process render queue until empty + while (_sharedRenderBatchBuilder.ComponentRenderQueue.Count > 0) + { + var nextComponentIdToRender = _sharedRenderBatchBuilder.ComponentRenderQueue.Dequeue(); + RenderInExistingBatch(_sharedRenderBatchBuilder, nextComponentIdToRender); + } + + UpdateDisplay(_sharedRenderBatchBuilder.ToBatch()); + } + finally + { + RemoveEventHandlerIds(_sharedRenderBatchBuilder.DisposedEventHandlerIds.ToRange()); + _sharedRenderBatchBuilder.Clear(); + Interlocked.Exchange(ref _renderBatchLock, 0); + } + } + private void RenderInExistingBatch(RenderBatchBuilder batchBuilder, int componentId) { GetRequiredComponentState(componentId).RenderIntoBatch(batchBuilder); diff --git a/test/Microsoft.AspNetCore.Blazor.Test/AutoRenderComponent.cs b/test/Microsoft.AspNetCore.Blazor.Test/AutoRenderComponent.cs index 148d8c35d7..e85a3c4ddf 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/AutoRenderComponent.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/AutoRenderComponent.cs @@ -20,7 +20,10 @@ namespace Microsoft.AspNetCore.Blazor.Test public void SetParameters(ParameterCollection parameters) { parameters.AssignToProperties(this); - _renderHandle.Render(); + TriggerRender(); } + + public void TriggerRender() + => _renderHandle.Render(); } } diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs index f7a960ce74..3616af07e1 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs @@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Blazor.Test // Act var componentId = renderer.AssignComponentId(component); - renderer.RenderNewBatch(componentId); + component.TriggerRender(); // Assert var batch = renderer.Batches.Single(); @@ -58,7 +58,7 @@ namespace Microsoft.AspNetCore.Blazor.Test // Act/Assert var componentId = renderer.AssignComponentId(component); - renderer.RenderNewBatch(componentId); + component.TriggerRender(); var batch = renderer.Batches.Single(); var componentFrame = batch.ReferenceFrames .Single(frame => frame.FrameType == RenderTreeFrameType.Component); @@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.Blazor.Test var componentId = renderer.AssignComponentId(component); // Act/Assert: first render - renderer.RenderNewBatch(componentId); + component.TriggerRender(); var batch = renderer.Batches.Single(); var firstDiff = batch.DiffsByComponentId[componentId].Single(); Assert.Collection(firstDiff.Edits, @@ -104,7 +104,7 @@ namespace Microsoft.AspNetCore.Blazor.Test // Act/Assert: second render component.Message = "Modified message"; - renderer.RenderNewBatch(componentId); + component.TriggerRender(); var secondBatch = renderer.Batches.Skip(1).Single(); var secondDiff = secondBatch.DiffsByComponentId[componentId].Single(); Assert.Collection(secondDiff.Edits, @@ -127,7 +127,7 @@ namespace Microsoft.AspNetCore.Blazor.Test builder.CloseComponent(); }); var parentComponentId = renderer.AssignComponentId(parentComponent); - renderer.RenderNewBatch(parentComponentId); + parentComponent.TriggerRender(); var nestedComponentFrame = renderer.Batches.Single() .ReferenceFrames .Single(frame => frame.FrameType == RenderTreeFrameType.Component); @@ -136,7 +136,7 @@ namespace Microsoft.AspNetCore.Blazor.Test // Assert: inital render nestedComponent.Message = "Render 1"; - renderer.RenderNewBatch(nestedComponentId); + nestedComponent.TriggerRender(); var batch = renderer.Batches[1]; var firstDiff = batch.DiffsByComponentId[nestedComponentId].Single(); Assert.Collection(firstDiff.Edits, @@ -149,7 +149,7 @@ namespace Microsoft.AspNetCore.Blazor.Test // Act/Assert: re-render nestedComponent.Message = "Render 2"; - renderer.RenderNewBatch(nestedComponentId); + nestedComponent.TriggerRender(); var secondBatch = renderer.Batches[2]; var secondDiff = secondBatch.DiffsByComponentId[nestedComponentId].Single(); Assert.Collection(secondDiff.Edits, @@ -173,7 +173,7 @@ namespace Microsoft.AspNetCore.Blazor.Test Handler = args => { receivedArgs = args; } }; var componentId = renderer.AssignComponentId(component); - renderer.RenderNewBatch(componentId); + component.TriggerRender(); var eventHandlerId = renderer.Batches.Single() .ReferenceFrames @@ -202,7 +202,7 @@ namespace Microsoft.AspNetCore.Blazor.Test builder.CloseComponent(); }); var parentComponentId = renderer.AssignComponentId(parentComponent); - renderer.RenderNewBatch(parentComponentId); + parentComponent.TriggerRender(); // Arrange: Render nested component var nestedComponentFrame = renderer.Batches.Single() @@ -211,7 +211,7 @@ namespace Microsoft.AspNetCore.Blazor.Test var nestedComponent = (EventComponent)nestedComponentFrame.Component; nestedComponent.Handler = args => { receivedArgs = args; }; var nestedComponentId = nestedComponentFrame.ComponentId; - renderer.RenderNewBatch(nestedComponentId); + nestedComponent.TriggerRender(); // Find nested component's event handler ID var eventHandlerId = renderer.Batches[1] @@ -242,7 +242,7 @@ namespace Microsoft.AspNetCore.Blazor.Test }); var componentId = renderer.AssignComponentId(component); - renderer.RenderNewBatch(componentId); + component.TriggerRender(); var eventHandlerId = renderer.Batches.Single() .ReferenceFrames @@ -259,19 +259,6 @@ namespace Microsoft.AspNetCore.Blazor.Test $"events because it does not implement {typeof(IHandleEvent).FullName}.", ex.Message); } - [Fact] - public void CannotRenderUnknownComponents() - { - // Arrange - var renderer = new TestRenderer(); - - // Act/Assert - Assert.Throws(() => - { - renderer.RenderNewBatch(123); - }); - } - [Fact] public void CannotDispatchEventsToUnknownComponents() { @@ -291,19 +278,32 @@ namespace Microsoft.AspNetCore.Blazor.Test // Arrange var renderer1 = new TestRenderer(); var renderer2 = new TestRenderer(); - var component = new MessageComponent { Message = "Hello, world!" }; + var component = new MultiRendererComponent(); var renderer1ComponentId = renderer1.AssignComponentId(component); renderer2.AssignComponentId(new TestComponent(null)); // Just so they don't get the same IDs var renderer2ComponentId = renderer2.AssignComponentId(component); - // Act/Assert: Render component in renderer1 - renderer1.RenderNewBatch(renderer1ComponentId); - Assert.True(renderer1.Batches.Single().DiffsByComponentId.ContainsKey(renderer1ComponentId)); - Assert.Empty(renderer2.Batches); + // Act/Assert + component.TriggerRender(); + var renderer1Batch = renderer1.Batches.Single(); + var renderer1Diff = renderer1Batch.DiffsByComponentId[renderer1ComponentId].Single(); + Assert.Collection(renderer1Diff.Edits, + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + AssertFrame.Text(renderer1Batch.ReferenceFrames[edit.ReferenceFrameIndex], + $"Hello from {nameof(MultiRendererComponent)}", 0); + }); - // Act/Assert: Render same component in renderer2 - renderer2.RenderNewBatch(renderer2ComponentId); - Assert.True(renderer2.Batches.Single().DiffsByComponentId.ContainsKey(renderer2ComponentId)); + var renderer2Batch = renderer2.Batches.Single(); + var renderer2Diff = renderer2Batch.DiffsByComponentId[renderer2ComponentId].Single(); + Assert.Collection(renderer2Diff.Edits, + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + AssertFrame.Text(renderer2Batch.ReferenceFrames[edit.ReferenceFrameIndex], + $"Hello from {nameof(MultiRendererComponent)}", 0); + }); } [Fact] @@ -320,7 +320,7 @@ namespace Microsoft.AspNetCore.Blazor.Test }); var rootComponentId = renderer.AssignComponentId(component); - renderer.RenderNewBatch(rootComponentId); + component.TriggerRender(); var nestedComponentFrame = renderer.Batches.Single() .ReferenceFrames @@ -329,7 +329,7 @@ namespace Microsoft.AspNetCore.Blazor.Test // Act: Second render message = "Modified message"; - renderer.RenderNewBatch(rootComponentId); + component.TriggerRender(); // Assert var batch = renderer.Batches[1]; @@ -361,7 +361,7 @@ namespace Microsoft.AspNetCore.Blazor.Test }); var rootComponentId = renderer.AssignComponentId(component); - renderer.RenderNewBatch(rootComponentId); + component.TriggerRender(); var originalComponentFrame = renderer.Batches.Single() .ReferenceFrames @@ -375,7 +375,7 @@ namespace Microsoft.AspNetCore.Blazor.Test // Act: Second render firstRender = false; - renderer.RenderNewBatch(rootComponentId); + component.TriggerRender(); // Assert Assert.Equal(256, childComponentInstance.IntProperty); @@ -397,7 +397,7 @@ namespace Microsoft.AspNetCore.Blazor.Test }); var rootComponentId = renderer.AssignComponentId(component); - renderer.RenderNewBatch(rootComponentId); + component.TriggerRender(); var childComponentId = renderer.Batches.Single() .ReferenceFrames @@ -406,7 +406,7 @@ namespace Microsoft.AspNetCore.Blazor.Test // Act: Second render firstRender = false; - renderer.RenderNewBatch(rootComponentId); + component.TriggerRender(); var diff = renderer.Batches[1].DiffsByComponentId[childComponentId].Single(); // Assert @@ -441,7 +441,7 @@ namespace Microsoft.AspNetCore.Blazor.Test var rootComponentId = renderer.AssignComponentId(component); // Act/Assert 1: First render, capturing child component IDs - renderer.RenderNewBatch(rootComponentId); + component.TriggerRender(); var batch = renderer.Batches.Single(); var rootComponentDiff = batch.DiffsByComponentId[rootComponentId].Single(); var childComponentIds = rootComponentDiff @@ -454,7 +454,7 @@ namespace Microsoft.AspNetCore.Blazor.Test // Act: Second render firstRender = false; - renderer.RenderNewBatch(rootComponentId); + component.TriggerRender(); // Assert: Applicable children are included in disposal list Assert.Equal(new[] { 1, 3 }, renderer.Batches[1].DisposedComponentIDs); @@ -469,7 +469,7 @@ namespace Microsoft.AspNetCore.Blazor.Test UIEventHandler origEventHandler = args => { eventCount++; }; var component = new EventComponent { Handler = origEventHandler }; var componentId = renderer.AssignComponentId(component); - renderer.RenderNewBatch(componentId); + component.TriggerRender(); var origEventHandlerId = renderer.Batches.Single() .ReferenceFrames .Where(f => f.FrameType == RenderTreeFrameType.Attribute) @@ -484,7 +484,7 @@ namespace Microsoft.AspNetCore.Blazor.Test // Now change the attribute value var newEventCount = 0; component.Handler = args => { newEventCount++; }; - renderer.RenderNewBatch(componentId); + component.TriggerRender(); // Act/Assert 2: Can no longer fire the original event, but can fire the new event Assert.Throws(() => @@ -506,7 +506,7 @@ namespace Microsoft.AspNetCore.Blazor.Test UIEventHandler origEventHandler = args => { eventCount++; }; var component = new EventComponent { Handler = origEventHandler }; var componentId = renderer.AssignComponentId(component); - renderer.RenderNewBatch(componentId); + component.TriggerRender(); var origEventHandlerId = renderer.Batches.Single() .ReferenceFrames .Where(f => f.FrameType == RenderTreeFrameType.Attribute) @@ -520,7 +520,7 @@ namespace Microsoft.AspNetCore.Blazor.Test // Now remove the event attribute component.Handler = null; - renderer.RenderNewBatch(componentId); + component.TriggerRender(); // Act/Assert 2: Can no longer fire the original event Assert.Throws(() => @@ -546,7 +546,7 @@ namespace Microsoft.AspNetCore.Blazor.Test } }; var rootComponentId = renderer.AssignComponentId(component); - renderer.RenderNewBatch(rootComponentId); + component.TriggerRender(); var batch = renderer.Batches.Single(); var rootComponentDiff = batch.DiffsByComponentId[rootComponentId].Single(); var rootComponentFrame = batch.ReferenceFrames[0]; @@ -569,7 +569,7 @@ namespace Microsoft.AspNetCore.Blazor.Test // Now remove the EventComponent component.IncludeChild = false; - renderer.RenderNewBatch(rootComponentId); + component.TriggerRender(); // Act/Assert 2: Can no longer fire the original event Assert.Throws(() => @@ -588,7 +588,7 @@ namespace Microsoft.AspNetCore.Blazor.Test UIEventHandler origEventHandler = args => { eventCount++; }; var component = new EventComponent { Handler = origEventHandler }; var componentId = renderer.AssignComponentId(component); - renderer.RenderNewBatch(componentId); + component.TriggerRender(); var origEventHandlerId = renderer.Batches.Single() .ReferenceFrames .Where(f => f.FrameType == RenderTreeFrameType.Attribute) @@ -602,7 +602,7 @@ namespace Microsoft.AspNetCore.Blazor.Test // Now remove the ancestor element component.SkipElement = true; - renderer.RenderNewBatch(componentId); + component.TriggerRender(); // Act/Assert 2: Can no longer fire the original event Assert.Throws(() => @@ -675,7 +675,7 @@ namespace Microsoft.AspNetCore.Blazor.Test var parentComponentId = renderer.AssignComponentId(parent); // Act - renderer.RenderNewBatch(parentComponentId); + parent.TriggerRender(); // Assert var batch = renderer.Batches.Single(); @@ -739,9 +739,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public new int AssignComponentId(IComponent component) => base.AssignComponentId(component); - public new void RenderNewBatch(int componentId) - => base.RenderNewBatch(componentId); - public new void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs args) => base.DispatchEvent(componentId, eventHandlerId, args); @@ -910,5 +907,29 @@ namespace Microsoft.AspNetCore.Blazor.Test builder.AddText(0, "Child is here"); } } + + private class MultiRendererComponent : IComponent + { + private readonly List _renderHandles + = new List(); + + public void BuildRenderTree(RenderTreeBuilder builder) + => builder.AddText(0, $"Hello from {nameof(MultiRendererComponent)}"); + + public void Init(RenderHandle renderHandle) + => _renderHandles.Add(renderHandle); + + public void SetParameters(ParameterCollection parameters) + { + } + + public void TriggerRender() + { + foreach (var renderHandle in _renderHandles) + { + renderHandle.Render(); + } + } + } } }