Remove externally-callable Renderer.RenderInNewBatch() now that rendering is triggered by components themselves

This commit is contained in:
Steve Sanderson 2018-02-13 17:06:00 +00:00
parent 804ab2d89f
commit 70a3ee3d98
5 changed files with 133 additions and 103 deletions

View File

@ -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);
/// <summary>
/// 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);
}
/// <summary>

View File

@ -11,6 +11,14 @@ namespace Microsoft.AspNetCore.Blazor.Components
/// </summary>
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;
}
/// <summary>
/// Gets an empty <see cref="ParameterCollection"/>.
/// </summary>
public static ParameterCollection Empty => _emptyCollection;
/// <summary>
/// Returns an enumerator that iterates through the <see cref="ParameterCollection"/>.
/// </summary>

View File

@ -64,52 +64,6 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
/// <param name="renderBatch">The changes to the UI since the previous call.</param>
protected abstract void UpdateDisplay(RenderBatch renderBatch);
/// <summary>
/// Updates the rendered state of the specified <see cref="IComponent"/>.
/// </summary>
/// <param name="componentId">The identifier of the <see cref="IComponent"/> to render.</param>
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);
}
}
/// <summary>
/// Notifies the specified component that an event has occurred.
/// </summary>
@ -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);

View File

@ -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();
}
}

View File

@ -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<ArgumentException>(() =>
{
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<ArgumentException>(() =>
@ -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<ArgumentException>(() =>
@ -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<ArgumentException>(() =>
@ -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<ArgumentException>(() =>
@ -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<RenderHandle> _renderHandles
= new List<RenderHandle>();
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();
}
}
}
}
}