diff --git a/samples/HostedInAspNet.Client/Program.cs b/samples/HostedInAspNet.Client/Program.cs
index bcd36e2473..c16d768296 100644
--- a/samples/HostedInAspNet.Client/Program.cs
+++ b/samples/HostedInAspNet.Client/Program.cs
@@ -20,6 +20,10 @@ namespace HostedInAspNet.Client
internal class MyComponent : IComponent
{
+ public void Init(RenderHandle renderHandle)
+ {
+ }
+
public void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "h1");
diff --git a/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs b/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs
index 90f7c30186..44a9c200b4 100644
--- a/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs
@@ -13,6 +13,21 @@ namespace Microsoft.AspNetCore.Blazor.Components
///
public abstract class BlazorComponent : IComponent, IHandlePropertiesChanged
{
+ private RenderHandle _renderHandle;
+
+ void IComponent.Init(RenderHandle renderHandle)
+ {
+ // This implicitly means a BlazorComponent can only be associated with a single
+ // renderer. That's the only use case we have right now. If there was ever a need,
+ // a component could hold a collection of render handles.
+ if (_renderHandle.IsInitalised)
+ {
+ throw new InvalidOperationException($"The render handle is already set. Cannot initialize a {nameof(BlazorComponent)} more than once.");
+ }
+
+ _renderHandle = renderHandle;
+ }
+
///
public virtual void BuildRenderTree(RenderTreeBuilder builder)
{
diff --git a/src/Microsoft.AspNetCore.Blazor/Components/IComponent.cs b/src/Microsoft.AspNetCore.Blazor/Components/IComponent.cs
index 446c620b74..494d2e1ac8 100644
--- a/src/Microsoft.AspNetCore.Blazor/Components/IComponent.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Components/IComponent.cs
@@ -10,6 +10,12 @@ namespace Microsoft.AspNetCore.Blazor.Components
///
public interface IComponent
{
+ ///
+ /// Initializes the component.
+ ///
+ /// A that allows the component to be rendered.
+ void Init(RenderHandle renderHandle);
+
///
/// Builds a representing the current state of the component.
///
diff --git a/src/Microsoft.AspNetCore.Blazor/Components/RenderHandle.cs b/src/Microsoft.AspNetCore.Blazor/Components/RenderHandle.cs
new file mode 100644
index 0000000000..df8031045a
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor/Components/RenderHandle.cs
@@ -0,0 +1,43 @@
+// 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.Rendering;
+using System;
+
+namespace Microsoft.AspNetCore.Blazor.Components
+{
+ ///
+ /// Allows a component to notify the renderer that it should be rendered.
+ ///
+ public readonly struct RenderHandle
+ {
+ private readonly Renderer _renderer;
+ private readonly int _componentId;
+
+ internal RenderHandle(Renderer renderer, int componentId)
+ {
+ _renderer = renderer ?? throw new System.ArgumentNullException(nameof(renderer));
+ _componentId = componentId;
+ }
+
+ ///
+ /// Gets a value that indicates whether the has been
+ /// initialised and is ready to use.
+ ///
+ public bool IsInitalised
+ => _renderer != null;
+
+ ///
+ /// Notifies the renderer that the component should be rendered.
+ ///
+ public void Render()
+ {
+ if (_renderer == null)
+ {
+ throw new InvalidOperationException("The render handle is not yet assigned.");
+ }
+
+ _renderer.ComponentRequestedRender(_componentId);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs
index b47d135a7d..e512ecff1b 100644
--- a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs
@@ -54,6 +54,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
var componentId = _nextComponentId++;
var componentState = new ComponentState(this, componentId, component);
_componentStateById.Add(componentId, componentState);
+ component.Init(new RenderHandle(this, componentId));
return componentId;
}
@@ -155,6 +156,20 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
frame = frame.WithAttributeEventHandlerId(id);
}
+ internal void ComponentRequestedRender(int componentId)
+ {
+ // 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)
+ {
+ RenderNewBatch(componentId);
+ }
+ else
+ {
+ _sharedRenderBatchBuilder.ComponentRenderQueue.Enqueue(componentId);
+ }
+ }
+
private ComponentState GetRequiredComponentState(int componentId)
=> _componentStateById.TryGetValue(componentId, out var componentState)
? componentState
diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs
index 9c140e7368..b0f8022e3e 100644
--- a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs
@@ -478,6 +478,10 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
public class TestComponent : IComponent
{
+ public void Init(RenderHandle renderHandle)
+ {
+ }
+
public void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddText(0, $"Hello from {nameof(TestComponent)}");
diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs
index 6d00b77159..a24d6e90e4 100644
--- a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs
@@ -291,6 +291,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class TestComponent : IComponent
{
+ public void Init(RenderHandle renderHandle) { }
+
public void BuildRenderTree(RenderTreeBuilder builder)
=> throw new NotImplementedException();
}
diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs
index 7b6a59c059..5faf3c4ae0 100644
--- a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs
@@ -798,13 +798,14 @@ namespace Microsoft.AspNetCore.Blazor.Test
public string ReadonlyProperty { get; private set; }
private string PrivateProperty { get; set; }
- public void BuildRenderTree(RenderTreeBuilder builder)
- {
- }
+ public void Init(RenderHandle renderHandle) { }
+ public void BuildRenderTree(RenderTreeBuilder builder) { }
}
private class FakeComponent2 : IComponent
{
+ public void Init(RenderHandle renderHandle) { }
+
public void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddText(100, $"Hello from {nameof(FakeComponent2)}");
@@ -815,11 +816,16 @@ namespace Microsoft.AspNetCore.Blazor.Test
{
public int DisposalCount { get; private set; }
public void Dispose() => DisposalCount++;
+
+ public void Init(RenderHandle renderHandle) { }
+
public void BuildRenderTree(RenderTreeBuilder builder) { }
}
private class NonDisposableComponent : IComponent
{
+ public void Init(RenderHandle renderHandle) { }
+
public void BuildRenderTree(RenderTreeBuilder builder) { }
}
diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs
index 6b16735276..7f8300bd49 100644
--- a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs
@@ -641,6 +641,110 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Equal(1, eventCount);
}
+ [Fact]
+ public void ComponentCannotTriggerRenderBeforeRenderHandleAssigned()
+ {
+ // Arrange
+ var component = new TestComponent(builder => { });
+
+ // Act/Assert
+ var ex = Assert.Throws(() =>
+ {
+ component.TriggerRender();
+ });
+ Assert.Equal("The render handle is not yet assigned.", ex.Message);
+ }
+
+ [Fact]
+ public void ComponentCanTriggerRenderWhenNoBatchIsInProgress()
+ {
+ // Arrange
+ var renderer = new TestRenderer();
+ var renderCount = 0;
+ var component = new TestComponent(builder =>
+ {
+ builder.AddText(0, $"Render count: {++renderCount}");
+ });
+ var componentId = renderer.AssignComponentId(component);
+
+ // Act/Assert: Can trigger initial render
+ Assert.Equal(0, renderCount);
+ component.TriggerRender();
+ Assert.Equal(1, renderCount);
+ var batch1 = renderer.Batches.Single();
+ var edit1 = batch1.DiffsByComponentId[componentId].Single().Edits.Single();
+ Assert.Equal(RenderTreeEditType.PrependFrame, edit1.Type);
+ AssertFrame.Text(batch1.ReferenceFrames[edit1.ReferenceFrameIndex],
+ "Render count: 1", 0);
+
+ // Act/Assert: Can trigger subsequent render
+ component.TriggerRender();
+ Assert.Equal(2, renderCount);
+ var batch2 = renderer.Batches.Skip(1).Single();
+ var edit2 = batch2.DiffsByComponentId[componentId].Single().Edits.Single();
+ Assert.Equal(RenderTreeEditType.UpdateText, edit2.Type);
+ AssertFrame.Text(batch2.ReferenceFrames[edit2.ReferenceFrameIndex],
+ "Render count: 2", 0);
+ }
+
+ [Fact]
+ public void ComponentCanTriggerRenderWhenExistingBatchIsInProgress()
+ {
+ // Arrange
+ var renderer = new TestRenderer();
+ TestComponent parent = null;
+ var parentRenderCount = 0;
+ parent = new TestComponent(builder =>
+ {
+ builder.OpenComponent(0);
+ builder.AddAttribute(1, nameof(ReRendersParentComponent.Parent), parent);
+ builder.CloseComponent();
+ builder.AddText(2, $"Parent render count: {++parentRenderCount}");
+ });
+ var parentComponentId = renderer.AssignComponentId(parent);
+
+ // Act
+ renderer.RenderNewBatch(parentComponentId);
+
+ // Assert
+ var batch = renderer.Batches.Single();
+ Assert.Equal(3, batch.DiffsInOrder.Count);
+
+ // First is the parent component's initial render
+ var diff1 = batch.DiffsInOrder[0];
+ Assert.Equal(parentComponentId, diff1.ComponentId);
+ Assert.Collection(diff1.Edits,
+ edit =>
+ {
+ Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
+ AssertFrame.Component(
+ batch.ReferenceFrames[edit.ReferenceFrameIndex]);
+ },
+ edit =>
+ {
+ Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
+ AssertFrame.Text(
+ batch.ReferenceFrames[edit.ReferenceFrameIndex],
+ "Parent render count: 1");
+ });
+
+ // Second is the child component's single render
+ var diff2 = batch.DiffsInOrder[1];
+ Assert.NotEqual(parentComponentId, diff2.ComponentId);
+ var diff2edit = diff2.Edits.Single();
+ Assert.Equal(RenderTreeEditType.PrependFrame, diff2edit.Type);
+ AssertFrame.Text(batch.ReferenceFrames[diff2edit.ReferenceFrameIndex],
+ "Child is here");
+
+ // Third is the parent's triggered render
+ var diff3 = batch.DiffsInOrder[2];
+ Assert.Equal(parentComponentId, diff3.ComponentId);
+ var diff3edit = diff3.Edits.Single();
+ Assert.Equal(RenderTreeEditType.UpdateText, diff3edit.Type);
+ AssertFrame.Text(batch.ReferenceFrames[diff3edit.ReferenceFrameIndex],
+ "Parent render count: 2");
+ }
+
private class NoOpRenderer : Renderer
{
public new int AssignComponentId(IComponent component)
@@ -690,6 +794,9 @@ namespace Microsoft.AspNetCore.Blazor.Test
public IDictionary> DiffsByComponentId { get; }
= new Dictionary>();
+ public IList DiffsInOrder { get; }
+ = new List();
+
public IList DisposedComponentIDs { get; set; }
public RenderTreeFrame[] ReferenceFrames { get; set; }
@@ -702,14 +809,17 @@ namespace Microsoft.AspNetCore.Blazor.Test
}
// Clone the diff, because its underlying storage will get reused in subsequent batches
- DiffsByComponentId[componentId].Add(new RenderTreeDiff(
+ var diffClone = new RenderTreeDiff(
diff.ComponentId,
- new ArraySegment(diff.Edits.ToArray())));
+ new ArraySegment(diff.Edits.ToArray()));
+ DiffsByComponentId[componentId].Add(diffClone);
+ DiffsInOrder.Add(diffClone);
}
}
private class TestComponent : IComponent
{
+ private RenderHandle _renderHandle;
private Action _renderAction;
public TestComponent(Action renderAction)
@@ -717,14 +827,26 @@ namespace Microsoft.AspNetCore.Blazor.Test
_renderAction = renderAction;
}
+ public void Init(RenderHandle renderHandle)
+ {
+ _renderHandle = renderHandle;
+ }
+
public void BuildRenderTree(RenderTreeBuilder builder)
=> _renderAction(builder);
+
+ public void TriggerRender()
+ => _renderHandle.Render();
}
private class MessageComponent : IComponent
{
public string Message { get; set; }
+ public void Init(RenderHandle renderHandle)
+ {
+ }
+
public void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddText(0, Message);
@@ -737,6 +859,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
public string StringProperty { get; set; }
public object ObjectProperty { get; set; }
+ public void Init(RenderHandle renderHandle)
+ {
+ }
+
public void BuildRenderTree(RenderTreeBuilder builder)
{
}
@@ -747,6 +873,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
public UIEventHandler Handler { get; set; }
public bool SkipElement { get; set; }
+ public void Init(RenderHandle renderHandle)
+ {
+ }
+
public void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "grandparent");
@@ -770,6 +900,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
public bool IncludeChild { get; set; }
public IDictionary ChildParameters { get; set; }
+ public void Init(RenderHandle renderHandle)
+ {
+ }
+
public void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddText(0, "Parent here");
@@ -796,6 +930,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
public int IntProperty { get; set; }
+ public void Init(RenderHandle renderHandle)
+ {
+ }
+
public void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddText(0, $"Notifications: {NotificationsCount}");
@@ -806,21 +944,24 @@ namespace Microsoft.AspNetCore.Blazor.Test
NotificationsCount++;
}
}
-
- (int, T) FirstWithIndex(IEnumerable items, Predicate predicate)
+
+ private class ReRendersParentComponent : IComponent
{
- var index = 0;
- foreach (var item in items)
+ public TestComponent Parent { get; set; }
+ private bool _isFirstTime = true;
+
+ public void Init(RenderHandle renderHandle) { }
+
+ public void BuildRenderTree(RenderTreeBuilder builder)
{
- if (predicate(item))
+ if (_isFirstTime) // Don't want an infinite loop
{
- return (index, item);
+ _isFirstTime = false;
+ Parent.TriggerRender();
}
- index++;
+ builder.AddText(0, "Child is here");
}
-
- throw new InvalidOperationException("No matching element was found.");
}
}
}