diff --git a/samples/HostedInAspNet.Client/Program.cs b/samples/HostedInAspNet.Client/Program.cs index 69897ac083..651950b15e 100644 --- a/samples/HostedInAspNet.Client/Program.cs +++ b/samples/HostedInAspNet.Client/Program.cs @@ -27,24 +27,5 @@ namespace HostedInAspNet.Client public void SetParameters(ParameterCollection parameters) { } - - public void BuildRenderTree(RenderTreeBuilder builder) - { - builder.OpenElement(0, "h1"); - builder.AddText(1, "Hello from RenderTree"); - builder.CloseElement(); - - builder.OpenElement(2, "ul"); - - builder.OpenElement(3, "li"); - builder.AddText(4, "First item"); - builder.CloseElement(); - - builder.OpenElement(5, "li"); - builder.AddText(6, "Second item"); - builder.CloseElement(); - - builder.CloseElement(); - } } } diff --git a/src/Microsoft.AspNetCore.Blazor.Build/Core/RazorCompilation/Engine/BlazorLoweringPhase.cs b/src/Microsoft.AspNetCore.Blazor.Build/Core/RazorCompilation/Engine/BlazorLoweringPhase.cs index 1f6a11a1a1..125521f73c 100644 --- a/src/Microsoft.AspNetCore.Blazor.Build/Core/RazorCompilation/Engine/BlazorLoweringPhase.cs +++ b/src/Microsoft.AspNetCore.Blazor.Build/Core/RazorCompilation/Engine/BlazorLoweringPhase.cs @@ -42,9 +42,9 @@ namespace Microsoft.AspNetCore.Blazor.Build.Core.RazorCompilation.Engine // from here. We inject the parameter later in RazorCompiler. var primaryMethod = documentNode.FindPrimaryMethod(); primaryMethod.ReturnType = "void"; - primaryMethod.MethodName = nameof(BlazorComponent.BuildRenderTree); + primaryMethod.MethodName = BlazorComponent.BuildRenderTreeMethodName; primaryMethod.Modifiers.Clear(); - primaryMethod.Modifiers.Add("public"); + primaryMethod.Modifiers.Add("protected"); primaryMethod.Modifiers.Add("override"); var line = new CSharpCodeIntermediateNode(); diff --git a/src/Microsoft.AspNetCore.Blazor.Build/Core/RazorCompilation/RazorCompiler.cs b/src/Microsoft.AspNetCore.Blazor.Build/Core/RazorCompilation/RazorCompiler.cs index fa3aabc626..96a4346232 100644 --- a/src/Microsoft.AspNetCore.Blazor.Build/Core/RazorCompilation/RazorCompiler.cs +++ b/src/Microsoft.AspNetCore.Blazor.Build/Core/RazorCompilation/RazorCompiler.cs @@ -105,7 +105,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Core.RazorCompilation // Add parameters to the primary method via string manipulation because // DefaultDocumentWriter's VisitMethodDeclaration can't emit parameters - var primaryMethodSource = $"public override void {nameof(BlazorComponent.BuildRenderTree)}"; + var primaryMethodSource = $"protected override void {BlazorComponent.BuildRenderTreeMethodName}"; generatedCode = generatedCode.Replace( $"{primaryMethodSource}()", $"{primaryMethodSource}({typeof(RenderTreeBuilder).FullName} builder)"); diff --git a/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs b/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs index ee064f5afa..688edd3bdc 100644 --- a/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs +++ b/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs @@ -13,12 +13,23 @@ namespace Microsoft.AspNetCore.Blazor.Components /// public abstract class BlazorComponent : IComponent, IHandleEvent { + public const string BuildRenderTreeMethodName = nameof(BuildRenderTree); + + private readonly Action _renderAction; private RenderHandle _renderHandle; private bool _hasNeverRendered = true; private bool _hasPendingQueuedRender; - /// - public virtual void BuildRenderTree(RenderTreeBuilder builder) + public BlazorComponent() + { + _renderAction = BuildRenderTree; + } + + /// + /// Renders the component to the supplied . + /// + /// A that will receive the render output. + protected virtual void BuildRenderTree(RenderTreeBuilder builder) { // Developers can either override this method in derived classes, or can use Razor // syntax to define a derived class and have the compiler generate the method. @@ -48,7 +59,7 @@ namespace Microsoft.AspNetCore.Blazor.Components if (_hasNeverRendered || ShouldRender()) { _hasPendingQueuedRender = true; - _renderHandle.Render(); + _renderHandle.Render(_renderAction); } } diff --git a/src/Microsoft.AspNetCore.Blazor/Components/IComponent.cs b/src/Microsoft.AspNetCore.Blazor/Components/IComponent.cs index 0abc9100a4..23a07c94b0 100644 --- a/src/Microsoft.AspNetCore.Blazor/Components/IComponent.cs +++ b/src/Microsoft.AspNetCore.Blazor/Components/IComponent.cs @@ -1,8 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Blazor.RenderTree; - namespace Microsoft.AspNetCore.Blazor.Components { /// @@ -21,11 +19,5 @@ namespace Microsoft.AspNetCore.Blazor.Components /// /// The parameters. void SetParameters(ParameterCollection parameters); - - /// - /// Builds a representing the current state of the component. - /// - /// A to which the rendered frames should be appended. - void BuildRenderTree(RenderTreeBuilder builder); } } diff --git a/src/Microsoft.AspNetCore.Blazor/Components/RenderHandle.cs b/src/Microsoft.AspNetCore.Blazor/Components/RenderHandle.cs index df8031045a..600380654a 100644 --- a/src/Microsoft.AspNetCore.Blazor/Components/RenderHandle.cs +++ b/src/Microsoft.AspNetCore.Blazor/Components/RenderHandle.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Blazor.Rendering; +using Microsoft.AspNetCore.Blazor.RenderTree; using System; namespace Microsoft.AspNetCore.Blazor.Components @@ -30,14 +31,14 @@ namespace Microsoft.AspNetCore.Blazor.Components /// /// Notifies the renderer that the component should be rendered. /// - public void Render() + public void Render(Action renderAction) { if (_renderer == null) { throw new InvalidOperationException("The render handle is not yet assigned."); } - _renderer.ComponentRequestedRender(_componentId); + _renderer.AddToRenderQueue(new RenderQueueEntry(_componentId, renderAction)); } } } diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs index 84990d8af5..e52ad0be19 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs @@ -35,13 +35,13 @@ namespace Microsoft.AspNetCore.Blazor.Rendering _renderTreeBuilderPrevious = new RenderTreeBuilder(renderer); } - public void RenderIntoBatch(RenderBatchBuilder batchBuilder) + public void RenderIntoBatch(RenderBatchBuilder batchBuilder, Action renderAction) { // Swap the old and new tree builders (_renderTreeBuilderCurrent, _renderTreeBuilderPrevious) = (_renderTreeBuilderPrevious, _renderTreeBuilderCurrent); _renderTreeBuilderCurrent.Clear(); - _component.BuildRenderTree(_renderTreeBuilderCurrent); + renderAction(_renderTreeBuilderCurrent); var diff = RenderTreeDiffBuilder.ComputeDiff( _renderer, diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs index 7f6b6f2a9a..e380cc753e 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering public ArrayBuilder ReferenceFramesBuffer { get; } = new ArrayBuilder(); // State of render pipeline - public Queue ComponentRenderQueue { get; } = new Queue(); + public Queue ComponentRenderQueue { get; } = new Queue(); public Queue ComponentDisposalQueue { get; } = new Queue(); public void Clear() diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/RenderQueueEntry.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/RenderQueueEntry.cs new file mode 100644 index 0000000000..2d5e611318 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/RenderQueueEntry.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Blazor.RenderTree; +using System; + +namespace Microsoft.AspNetCore.Blazor.Rendering +{ + internal readonly struct RenderQueueEntry + { + public readonly int ComponentId; + public readonly Action RenderAction; + + public RenderQueueEntry(int componentId, Action renderAction) + { + ComponentId = componentId; + RenderAction = renderAction ?? throw new ArgumentNullException(nameof(renderAction)); + } + } +} diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs index e1e14e28aa..583596d252 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.Threading; namespace Microsoft.AspNetCore.Blazor.Rendering { @@ -19,10 +18,8 @@ namespace Microsoft.AspNetCore.Blazor.Rendering 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 - private readonly RenderBatchBuilder _sharedRenderBatchBuilder = new RenderBatchBuilder(); - private int _renderBatchLock = 0; + private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder(); + private bool _isBatchInProgress; private int _lastEventHandlerId = 0; private readonly Dictionary _eventHandlersById @@ -106,17 +103,13 @@ namespace Microsoft.AspNetCore.Blazor.Rendering frame = frame.WithAttributeEventHandlerId(id); } - internal void ComponentRequestedRender(int componentId) + internal void AddToRenderQueue(RenderQueueEntry renderQueueEntry) { - // TODO: Clean up the locking around rendering. The Renderer doesn't really need - // to be thread-safe, and the following code isn't actually thread-safe anyway. - if (_renderBatchLock == 0) + _batchBuilder.ComponentRenderQueue.Enqueue(renderQueueEntry); + + if (!_isBatchInProgress) { - RenderNewBatch(componentId); - } - else - { - _sharedRenderBatchBuilder.ComponentRenderQueue.Enqueue(componentId); + ProcessRenderQueue(); } } @@ -125,59 +118,42 @@ namespace Microsoft.AspNetCore.Blazor.Rendering ? componentState : throw new ArgumentException($"The renderer does not have a component with ID {componentId}."); - private void RenderNewBatch(int componentId) + private void ProcessRenderQueue() { - // 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); + _isBatchInProgress = true; try { // Process render queue until empty - while (_sharedRenderBatchBuilder.ComponentRenderQueue.Count > 0) + while (_batchBuilder.ComponentRenderQueue.Count > 0) { - var nextComponentIdToRender = _sharedRenderBatchBuilder.ComponentRenderQueue.Dequeue(); - RenderInExistingBatch(_sharedRenderBatchBuilder, nextComponentIdToRender); + var nextToRender = _batchBuilder.ComponentRenderQueue.Dequeue(); + RenderInExistingBatch(nextToRender); } - UpdateDisplay(_sharedRenderBatchBuilder.ToBatch()); + UpdateDisplay(_batchBuilder.ToBatch()); } finally { - RemoveEventHandlerIds(_sharedRenderBatchBuilder.DisposedEventHandlerIds.ToRange()); - _sharedRenderBatchBuilder.Clear(); - Interlocked.Exchange(ref _renderBatchLock, 0); + RemoveEventHandlerIds(_batchBuilder.DisposedEventHandlerIds.ToRange()); + _batchBuilder.Clear(); + _isBatchInProgress = false; } } - private void RenderInExistingBatch(RenderBatchBuilder batchBuilder, int componentId) + private void RenderInExistingBatch(RenderQueueEntry renderQueueEntry) { - GetRequiredComponentState(componentId).RenderIntoBatch(batchBuilder); + var componentId = renderQueueEntry.ComponentId; + GetRequiredComponentState(componentId) + .RenderIntoBatch(_batchBuilder, renderQueueEntry.RenderAction); // Process disposal queue now in case it causes further component renders to be enqueued - while (batchBuilder.ComponentDisposalQueue.Count > 0) + while (_batchBuilder.ComponentDisposalQueue.Count > 0) { - var disposeComponentId = batchBuilder.ComponentDisposalQueue.Dequeue(); - GetRequiredComponentState(disposeComponentId).DisposeInBatch(batchBuilder); + var disposeComponentId = _batchBuilder.ComponentDisposalQueue.Dequeue(); + GetRequiredComponentState(disposeComponentId).DisposeInBatch(_batchBuilder); _componentStateById.Remove(disposeComponentId); - batchBuilder.DisposedComponentIds.Append(disposeComponentId); + _batchBuilder.DisposedComponentIds.Append(disposeComponentId); } } diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs index 8d9c43ae49..60d3ba7c5b 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs @@ -484,11 +484,6 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test public void SetParameters(ParameterCollection parameters) { } - - public void BuildRenderTree(RenderTreeBuilder builder) - { - builder.AddText(0, $"Hello from {nameof(TestComponent)}"); - } } } } diff --git a/test/Microsoft.AspNetCore.Blazor.Test/AutoRenderComponent.cs b/test/Microsoft.AspNetCore.Blazor.Test/AutoRenderComponent.cs index e85a3c4ddf..dcbf6a05cd 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/AutoRenderComponent.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/AutoRenderComponent.cs @@ -10,8 +10,6 @@ namespace Microsoft.AspNetCore.Blazor.Test { private RenderHandle _renderHandle; - public abstract void BuildRenderTree(RenderTreeBuilder builder); - public void Init(RenderHandle renderHandle) { _renderHandle = renderHandle; @@ -24,6 +22,8 @@ namespace Microsoft.AspNetCore.Blazor.Test } public void TriggerRender() - => _renderHandle.Render(); + => _renderHandle.Render(BuildRenderTree); + + protected abstract void BuildRenderTree(RenderTreeBuilder builder); } } diff --git a/test/Microsoft.AspNetCore.Blazor.Test/ParameterCollectionTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/ParameterCollectionTest.cs index 4d058e4053..924dd04f94 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/ParameterCollectionTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/ParameterCollectionTest.cs @@ -109,9 +109,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void SetParameters(ParameterCollection parameters) => throw new NotImplementedException(); - - public void BuildRenderTree(RenderTreeBuilder builder) - => throw new NotImplementedException(); } } } diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs index 28db0e82a6..84fdcc0c37 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs @@ -295,9 +295,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void SetParameters(ParameterCollection parameters) => throw new NotImplementedException(); - - public void BuildRenderTree(RenderTreeBuilder builder) - => throw new NotImplementedException(); } private class TestRenderer : Renderer diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs index d171c441ac..847a484b4f 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs @@ -775,7 +775,6 @@ namespace Microsoft.AspNetCore.Blazor.Test { parameters.AssignToProperties(this); } - public void BuildRenderTree(RenderTreeBuilder builder) { } } private class FakeComponent2 : IComponent @@ -787,11 +786,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void SetParameters(ParameterCollection parameters) { } - - public void BuildRenderTree(RenderTreeBuilder builder) - { - builder.AddText(100, $"Hello from {nameof(FakeComponent2)}"); - } } private class DisposableComponent : IComponent, IDisposable @@ -802,8 +796,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void Init(RenderHandle renderHandle) { } public void SetParameters(ParameterCollection parameters) { } - - public void BuildRenderTree(RenderTreeBuilder builder) { } } private class NonDisposableComponent : IComponent @@ -811,8 +803,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void Init(RenderHandle renderHandle) { } public void SetParameters(ParameterCollection parameters) { } - - public void BuildRenderTree(RenderTreeBuilder builder) { } } private static void AssertEdit( diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs index 3616af07e1..f52564754a 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs @@ -803,20 +803,17 @@ namespace Microsoft.AspNetCore.Blazor.Test } public void SetParameters(ParameterCollection parameters) - => _renderHandle.Render(); - - public void BuildRenderTree(RenderTreeBuilder builder) - => _renderAction(builder); + => TriggerRender(); public void TriggerRender() - => _renderHandle.Render(); + => _renderHandle.Render(_renderAction); } private class MessageComponent : AutoRenderComponent { public string Message { get; set; } - public override void BuildRenderTree(RenderTreeBuilder builder) + protected override void BuildRenderTree(RenderTreeBuilder builder) { builder.AddText(0, Message); } @@ -828,10 +825,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public string StringProperty { get; set; } public object ObjectProperty { get; set; } - public void BuildRenderTree(RenderTreeBuilder builder) - { - } - public void Init(RenderHandle renderHandle) { } @@ -845,7 +838,7 @@ namespace Microsoft.AspNetCore.Blazor.Test public UIEventHandler Handler { get; set; } public bool SkipElement { get; set; } - public override void BuildRenderTree(RenderTreeBuilder builder) + protected override void BuildRenderTree(RenderTreeBuilder builder) { builder.OpenElement(0, "grandparent"); if (!SkipElement) @@ -871,7 +864,7 @@ namespace Microsoft.AspNetCore.Blazor.Test public bool IncludeChild { get; set; } public IDictionary ChildParameters { get; set; } - public override void BuildRenderTree(RenderTreeBuilder builder) + protected override void BuildRenderTree(RenderTreeBuilder builder) { builder.AddText(0, "Parent here"); @@ -896,7 +889,7 @@ namespace Microsoft.AspNetCore.Blazor.Test public TestComponent Parent { get; set; } private bool _isFirstTime = true; - public override void BuildRenderTree(RenderTreeBuilder builder) + protected override void BuildRenderTree(RenderTreeBuilder builder) { if (_isFirstTime) // Don't want an infinite loop { @@ -913,9 +906,6 @@ namespace Microsoft.AspNetCore.Blazor.Test 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); @@ -927,7 +917,10 @@ namespace Microsoft.AspNetCore.Blazor.Test { foreach (var renderHandle in _renderHandles) { - renderHandle.Render(); + renderHandle.Render(builder => + { + builder.AddText(0, $"Hello from {nameof(MultiRendererComponent)}"); + }); } } }