Introduce IComponent.SetParameters, moving parameter-setting and rerendering logic into component base class

This commit is contained in:
Steve Sanderson 2018-02-13 15:00:53 +00:00
parent 37217db73a
commit 861154764c
14 changed files with 205 additions and 327 deletions

View File

@ -24,6 +24,10 @@ namespace HostedInAspNet.Client
{
}
public void SetParameters(ParameterCollection parameters)
{
}
public void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "h1");

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Blazor.Components
/// Optional base class for Blazor components. Alternatively, Blazor components may
/// implement <see cref="IComponent"/> directly.
/// </summary>
public abstract class BlazorComponent : IComponent, IHandlePropertiesChanged
public abstract class BlazorComponent : IComponent
{
private RenderHandle _renderHandle;
@ -28,6 +28,16 @@ namespace Microsoft.AspNetCore.Blazor.Components
_renderHandle = renderHandle;
}
void IComponent.SetParameters(ParameterCollection parameters)
{
parameters.AssignToProperties(this);
// TODO: If we know conclusively that the parameters have not changed since last
// time (because they are all primitives and equal to the existing property values)
// then don't re-render. Can put an "out bool" parameter on AssignToProperties.
_renderHandle.Render();
}
/// <inheritdoc />
public virtual void BuildRenderTree(RenderTreeBuilder builder)
{
@ -50,11 +60,6 @@ namespace Microsoft.AspNetCore.Blazor.Components
public virtual Task ExecuteAsync()
=> throw new NotImplementedException($"Blazor components do not implement {nameof(ExecuteAsync)}.");
/// <inheritdoc />
public virtual void OnPropertiesChanged()
{
}
/// <summary>
/// Handles click events by invoking <paramref name="handler"/>.
/// </summary>

View File

@ -16,6 +16,12 @@ namespace Microsoft.AspNetCore.Blazor.Components
/// <param name="renderHandle">A <see cref="RenderHandle"/> that allows the component to be rendered.</param>
void Init(RenderHandle renderHandle);
/// <summary>
/// Sets parameters supplied by the component's parent in the render tree.
/// </summary>
/// <param name="parameters">The parameters.</param>
void SetParameters(ParameterCollection parameters);
/// <summary>
/// Builds a <see cref="RenderTree"/> representing the current state of the component.
/// </summary>

View File

@ -1,17 +0,0 @@
// 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.
namespace Microsoft.AspNetCore.Blazor.Components
{
/// <summary>
/// Interface implemented by components that receive notifications when their
/// properties are changed by their parent component.
/// </summary>
public interface IHandlePropertiesChanged
{
/// <summary>
/// Notifies the component that its properties have changed.
/// </summary>
void OnPropertiesChanged();
}
}

View File

@ -0,0 +1,71 @@
// 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 System;
using System.Reflection;
namespace Microsoft.AspNetCore.Blazor.Components
{
/// <summary>
/// Extension methods for the <see cref="ParameterCollection"/> type.
/// </summary>
public static class ParameterCollectionExtensions
{
/// <summary>
/// Iterates through the <see cref="ParameterCollection"/>, assigning each parameter
/// to a property of the same name on <paramref name="target"/>.
/// </summary>
/// <param name="parameterCollection">The <see cref="ParameterCollection"/>.</param>
/// <param name="target">An object that has a public writable property matching each parameter's name and type.</param>
public static void AssignToProperties(
this ParameterCollection parameterCollection,
object target)
{
if (target == null)
{
throw new ArgumentNullException(nameof(target));
}
foreach (var parameter in parameterCollection)
{
AssignToProperty(target, parameter);
}
}
private static void AssignToProperty(object target, Parameter parameter)
{
// TODO: Don't just use naive reflection like this. Possible ways to make it faster:
// (a) Create and cache a property-assigning open delegate for each (target type,
// property name) pair, e.g., using propertyInfo.GetSetMethod().CreateDelegate(...)
// That's much faster than caching the PropertyInfo, at least on JIT-enabled platforms.
// (b) Or possibly just code-gen an IComponent.SetParameters implementation for each
// Razor component. However that might not work well with code-behind inheritance,
// because the code-behind wouldn't be able to override it.
var propertyInfo = GetPropertyInfo(target.GetType(), parameter.Name);
try
{
propertyInfo.SetValue(target, parameter.Value);
}
catch (Exception ex)
{
throw new InvalidOperationException(
$"Unable to set property '{parameter.Name}' on object of " +
$"type '{target.GetType().FullName}'. The error was: {ex.Message}", ex);
}
}
private static PropertyInfo GetPropertyInfo(Type targetType, string propertyName)
{
var property = targetType.GetProperty(propertyName);
if (property == null)
{
throw new InvalidOperationException(
$"Object of type '{targetType.FullName}' does not have a property " +
$"matching the name '{propertyName}'.");
}
return property;
}
}
}

View File

@ -144,133 +144,23 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
int oldComponentIndex,
int newComponentIndex)
{
// The algorithm here is the same as in AppendDiffEntriesForRange, except that
// here we don't optimise for loops - we assume that both sequences are forward-only.
// That's because this is true for all currently supported scenarios, and it means
// fewer steps here.
var oldTree = diffContext.OldTree;
var newTree = diffContext.NewTree;
ref var oldComponentFrame = ref oldTree[oldComponentIndex];
ref var newComponentFrame = ref newTree[newComponentIndex];
var componentId = oldComponentFrame.ComponentId;
var componentInstance = oldComponentFrame.Component;
var hasSetAnyProperty = false;
// Preserve the actual componentInstance
newComponentFrame = newComponentFrame.WithComponentInstance(componentId, componentInstance);
// Now locate any added/changed/removed properties
var oldStartIndex = oldComponentIndex + 1;
var newStartIndex = newComponentIndex + 1;
var oldEndIndexExcl = oldComponentIndex + oldComponentFrame.ComponentSubtreeLength;
var newEndIndexExcl = newComponentIndex + newComponentFrame.ComponentSubtreeLength;
var hasMoreOld = oldEndIndexExcl > oldStartIndex;
var hasMoreNew = newEndIndexExcl > newStartIndex;
while (hasMoreOld || hasMoreNew)
{
var oldSeq = hasMoreOld ? oldTree[oldStartIndex].Sequence : int.MaxValue;
var newSeq = hasMoreNew ? newTree[newStartIndex].Sequence : int.MaxValue;
if (oldSeq == newSeq)
{
ref var oldFrame = ref oldTree[oldStartIndex];
ref var newFrame = ref newTree[newStartIndex];
var oldName = oldFrame.AttributeName;
var newName = newFrame.AttributeName;
var newPropertyValue = newFrame.AttributeValue;
if (string.Equals(oldName, newName, StringComparison.Ordinal))
{
// Using Equals to account for string comparisons, nulls, etc.
var oldPropertyValue = oldFrame.AttributeValue;
if (!Equals(oldPropertyValue, newPropertyValue))
{
SetChildComponentProperty(componentInstance, newName, newPropertyValue);
hasSetAnyProperty = true;
}
}
else
{
// Since this code path is never reachable for Razor components (because you
// can't have two different attribute names from the same source sequence), we
// could consider removing the 'name equality' check entirely for perf
SetChildComponentProperty(componentInstance, newName, newPropertyValue);
RemoveChildComponentProperty(componentInstance, oldName);
hasSetAnyProperty = true;
}
oldStartIndex++;
newStartIndex++;
hasMoreOld = oldEndIndexExcl > oldStartIndex;
hasMoreNew = newEndIndexExcl > newStartIndex;
}
else
{
// Both sequences are proceeding through the same loop block, so do a simple
// preordered merge join (picking from whichever side brings us closer to being
// back in sync)
var treatAsInsert = newSeq < oldSeq;
if (treatAsInsert)
{
ref var newFrame = ref newTree[newStartIndex];
SetChildComponentProperty(componentInstance, newFrame.AttributeName, newFrame.AttributeValue);
hasSetAnyProperty = true;
newStartIndex++;
hasMoreNew = newEndIndexExcl > newStartIndex;
}
else
{
ref var oldFrame = ref oldTree[oldStartIndex];
RemoveChildComponentProperty(componentInstance, oldFrame.AttributeName);
hasSetAnyProperty = true;
oldStartIndex++;
hasMoreOld = oldEndIndexExcl > oldStartIndex;
}
}
}
if (hasSetAnyProperty)
{
diffContext.BatchBuilder.ComponentRenderQueue.Enqueue(newComponentFrame.ComponentId);
}
}
private static void RemoveChildComponentProperty(IComponent component, string componentPropertyName)
{
var propertyInfo = GetChildComponentPropertyInfo(component.GetType(), componentPropertyName);
var defaultValue = propertyInfo.PropertyType.IsValueType
? Activator.CreateInstance(propertyInfo.PropertyType)
: null;
SetChildComponentProperty(component, componentPropertyName, defaultValue);
}
private static void SetChildComponentProperty(IComponent component, string componentPropertyName, object newPropertyValue)
{
var propertyInfo = GetChildComponentPropertyInfo(component.GetType(), componentPropertyName);
try
{
propertyInfo.SetValue(component, newPropertyValue);
}
catch (Exception ex)
{
throw new InvalidOperationException(
$"Unable to set property '{componentPropertyName}' on component of " +
$"type '{component.GetType().FullName}'. The error was: {ex.Message}", ex);
}
}
private static PropertyInfo GetChildComponentPropertyInfo(Type componentType, string componentPropertyName)
{
var property = componentType.GetProperty(componentPropertyName);
if (property == null)
{
throw new InvalidOperationException(
$"Component of type '{componentType.FullName}' does not have a property " +
$"matching the name '{componentPropertyName}'.");
}
return property;
// Supply latest parameters. They might not have changed, but it's up to the
// recipient to decide what "changed" means. Currently we only supply the new
// set of parameters and assume the recipient has enough info to do whatever
// comparisons it wants with the old values. Later we could choose to pass the
// old parameter values if we wanted.
var newParameters = new ParameterCollection(newTree, newComponentIndex);
componentInstance.SetParameters(newParameters);
}
private static int NextSiblingIndex(RenderTreeFrame frame, int frameIndex)
@ -541,18 +431,9 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
diffContext.Renderer.InstantiateChildComponentOnFrame(ref frame);
var childComponentInstance = frame.Component;
// All descendants of a component are its properties
var componentDescendantsEndIndexExcl = frameIndex + frame.ComponentSubtreeLength;
for (var attributeFrameIndex = frameIndex + 1; attributeFrameIndex < componentDescendantsEndIndexExcl; attributeFrameIndex++)
{
ref var attributeFrame = ref frames[attributeFrameIndex];
SetChildComponentProperty(
childComponentInstance,
attributeFrame.AttributeName,
attributeFrame.AttributeValue);
}
diffContext.BatchBuilder.ComponentRenderQueue.Enqueue(frame.ComponentId);
// Set initial parameters
var initialParameters = new ParameterCollection(frames, frameIndex);
childComponentInstance.SetParameters(initialParameters);
}
private static void InitializeNewAttributeFrame(ref DiffContext diffContext, ref RenderTreeFrame newFrame)

View File

@ -37,11 +37,6 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
public void RenderIntoBatch(RenderBatchBuilder batchBuilder)
{
if (_component is IHandlePropertiesChanged notifyableComponent)
{
notifyableComponent.OnPropertiesChanged();
}
// Swap the old and new tree builders
(_renderTreeBuilderCurrent, _renderTreeBuilderPrevious) = (_renderTreeBuilderPrevious, _renderTreeBuilderCurrent);

View File

@ -482,6 +482,10 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
{
}
public void SetParameters(ParameterCollection parameters)
{
}
public void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddText(0, $"Hello from {nameof(TestComponent)}");

View File

@ -0,0 +1,26 @@
// 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.Components;
using Microsoft.AspNetCore.Blazor.RenderTree;
namespace Microsoft.AspNetCore.Blazor.Test
{
public abstract class AutoRenderComponent : IComponent
{
private RenderHandle _renderHandle;
public abstract void BuildRenderTree(RenderTreeBuilder builder);
public void Init(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}
public void SetParameters(ParameterCollection parameters)
{
parameters.AssignToProperties(this);
_renderHandle.Render();
}
}
}

View File

@ -107,6 +107,9 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void Init(RenderHandle renderHandle)
=> throw new NotImplementedException();
public void SetParameters(ParameterCollection parameters)
=> throw new NotImplementedException();
public void BuildRenderTree(RenderTreeBuilder builder)
=> throw new NotImplementedException();
}

View File

@ -293,6 +293,9 @@ namespace Microsoft.AspNetCore.Blazor.Test
{
public void Init(RenderHandle renderHandle) { }
public void SetParameters(ParameterCollection parameters)
=> throw new NotImplementedException();
public void BuildRenderTree(RenderTreeBuilder builder)
=> throw new NotImplementedException();
}

View File

@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
appendAction(newTree);
// Act
var (result, referenceFrames) = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent(initializeFromFrames: true);
// Assert
Assert.Empty(result.Edits);
@ -51,7 +51,11 @@ namespace Microsoft.AspNetCore.Blazor.Test
builder.AddAttribute(1, "My attribute", "My value");
builder.CloseElement();
},
builder => builder.OpenComponent<FakeComponent>(0)
builder =>
{
builder.OpenComponent<FakeComponent>(0);
builder.CloseComponent();
}
}.Select(x => new object[] { x });
[Fact]
@ -319,7 +323,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Arrange
oldTree.OpenComponent<FakeComponent>(123);
oldTree.CloseComponent();
GetRenderedBatch(new RenderTreeBuilder(renderer), oldTree); // Assign initial IDs
GetRenderedBatch(new RenderTreeBuilder(renderer), oldTree, false); // Assign initial IDs
newTree.OpenComponent<FakeComponent2>(123);
newTree.CloseComponent();
var batchBuilder = new RenderBatchBuilder();
@ -327,11 +331,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Act
var diff = RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, oldTree.GetFrames(), newTree.GetFrames());
// Assert: We're going to dispose the old component, and render the new one
// Assert: We're going to dispose the old component and render the new one
Assert.Equal(new[] { 0 }, batchBuilder.ComponentDisposalQueue);
Assert.Equal(new[] { 1 }, batchBuilder.ComponentRenderQueue);
// Assert: Got correct info in diff
Assert.Collection(diff.Edits,
entry => AssertEdit(entry, RenderTreeEditType.RemoveFrame, 0),
entry =>
@ -616,7 +617,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
}
[Fact]
public void SetsKnownPropertiesOnChildComponents()
public void SetsParametersOnChildComponents()
{
// Arrange
var testObject = new object();
@ -640,41 +641,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Same(testObject, componentInstance.ObjectProperty);
}
[Fact]
public void ThrowsIfAssigningUnknownPropertiesToChildComponents()
{
// Arrange
var testObject = new object();
newTree.OpenComponent<FakeComponent>(0);
newTree.AddAttribute(1, "SomeUnknownProperty", 123);
newTree.CloseComponent();
// Act/Assert
var ex = Assert.Throws<InvalidOperationException>(() =>
{
RenderTreeDiffBuilder.ComputeDiff(renderer, new RenderBatchBuilder(), 0, oldTree.GetFrames(), newTree.GetFrames());
});
Assert.Equal($"Component of type '{typeof(FakeComponent).FullName}' does not have a property matching the name 'SomeUnknownProperty'.", ex.Message);
}
[Fact]
public void ThrowsIfAssigningReadOnlyPropertiesToChildComponents()
{
// Arrange
var testObject = new object();
newTree.OpenComponent<FakeComponent>(0);
newTree.AddAttribute(1, nameof(FakeComponent.ReadonlyProperty), 123);
newTree.CloseComponent();
// Act/Assert
var ex = Assert.Throws<InvalidOperationException>(() =>
{
RenderTreeDiffBuilder.ComputeDiff(renderer, new RenderBatchBuilder(), 0, oldTree.GetFrames(), newTree.GetFrames());
});
Assert.StartsWith($"Unable to set property '{nameof(FakeComponent.ReadonlyProperty)}' on " +
$"component of type '{typeof(FakeComponent).FullName}'.", ex.Message);
}
[Fact]
public void RetainsChildComponentsForExistingFrames()
{
@ -712,7 +678,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
}
[Fact]
public void UpdatesChangedPropertiesOnRetainedChildComponents()
public void SetsUpdatedParametersOnChildComponents()
{
// Arrange
var objectWillNotChange = new object();
@ -727,7 +693,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
RenderTreeDiffBuilder.ComputeDiff(renderer, new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames());
var originalComponentInstance = (FakeComponent)oldTree.GetFrames().Array[0].Component;
originalComponentInstance.ObjectProperty = null; // So we can see it doesn't get reassigned
// Act
var renderBatch = GetRenderedBatch();
@ -737,7 +702,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Equal(1, renderBatch.UpdatedComponents.Count); // Because the diff builder only queues child component renders; it doesn't actually perfom them itself
Assert.Same(originalComponentInstance, newComponentInstance);
Assert.Equal("String did change", newComponentInstance.StringProperty);
Assert.Null(newComponentInstance.ObjectProperty); // To observe that the property wasn't even written, we nulled it out on the original
Assert.Same(objectWillNotChange, newComponentInstance.ObjectProperty);
}
[Fact]
@ -764,19 +729,26 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Equal(new[] { 0, 1 }, batchBuilder.ComponentDisposalQueue);
}
private (RenderTreeDiff, RenderTreeFrame[]) GetSingleUpdatedComponent()
private (RenderTreeDiff, RenderTreeFrame[]) GetSingleUpdatedComponent(bool initializeFromFrames = false)
{
var batch = GetRenderedBatch();
var batch = GetRenderedBatch(initializeFromFrames);
var diffsInBatch = batch.UpdatedComponents;
Assert.Equal(1, diffsInBatch.Count);
return (diffsInBatch.Array[0], batch.ReferenceFrames.ToArray());
}
private RenderBatch GetRenderedBatch()
=> GetRenderedBatch(oldTree, newTree);
private RenderBatch GetRenderedBatch(bool initializeFromFrames = false)
=> GetRenderedBatch(oldTree, newTree, initializeFromFrames);
private RenderBatch GetRenderedBatch(RenderTreeBuilder from, RenderTreeBuilder to)
private RenderBatch GetRenderedBatch(RenderTreeBuilder from, RenderTreeBuilder to, bool initializeFromFrames)
{
if (initializeFromFrames)
{
var emptyFrames = new RenderTreeBuilder(renderer).GetFrames();
var oldFrames = from.GetFrames();
RenderTreeDiffBuilder.ComputeDiff(renderer, new RenderBatchBuilder(), 0, emptyFrames, oldFrames);
}
var batchBuilder = new RenderBatchBuilder();
var diff = RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, from.GetFrames(), to.GetFrames());
batchBuilder.UpdatedComponentDiffs.Append(diff);
@ -799,12 +771,22 @@ namespace Microsoft.AspNetCore.Blazor.Test
private string PrivateProperty { get; set; }
public void Init(RenderHandle renderHandle) { }
public void SetParameters(ParameterCollection parameters)
{
parameters.AssignToProperties(this);
}
public void BuildRenderTree(RenderTreeBuilder builder) { }
}
private class FakeComponent2 : IComponent
{
public void Init(RenderHandle renderHandle) { }
public void Init(RenderHandle renderHandle)
{
}
public void SetParameters(ParameterCollection parameters)
{
}
public void BuildRenderTree(RenderTreeBuilder builder)
{
@ -819,6 +801,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void Init(RenderHandle renderHandle) { }
public void SetParameters(ParameterCollection parameters) { }
public void BuildRenderTree(RenderTreeBuilder builder) { }
}
@ -826,10 +810,11 @@ namespace Microsoft.AspNetCore.Blazor.Test
{
public void Init(RenderHandle renderHandle) { }
public void SetParameters(ParameterCollection parameters) { }
public void BuildRenderTree(RenderTreeBuilder builder) { }
}
private static void AssertEdit(
RenderTreeEdit edit,
RenderTreeEditType type,

View File

@ -12,7 +12,7 @@ using Xunit;
namespace Microsoft.AspNetCore.Blazor.Test
{
public class RendererTest
public partial class RendererTest
{
[Fact]
public void CanRenderTopLevelComponents()
@ -310,7 +310,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Equal(0, edit.ReferenceFrameIndex);
});
AssertFrame.Text(batch.ReferenceFrames[0], "Modified message");
Assert.False(batch.DiffsByComponentId.ContainsKey(nestedComponentFrame.ComponentId));
Assert.Empty(batch.DiffsByComponentId[nestedComponentFrame.ComponentId].Single().Edits);
}
[Fact]
@ -388,66 +388,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
AssertFrame.Text(renderer.Batches[1].ReferenceFrames[0], "second");
}
[Fact]
public void NotifiesIHandlePropertiesChangedBeforeFirstRender()
{
// Arrange
var renderer = new TestRenderer();
var component = new HandlePropertiesChangedComponent();
var rootComponentId = renderer.AssignComponentId(component);
// Act
renderer.RenderNewBatch(rootComponentId);
// Assert
AssertFrame.Text(renderer.Batches.Single().ReferenceFrames[0], "Notifications: 1", 0);
}
[Fact]
public void NotifiesIHandlePropertiesChangedWhenChanged()
{
// Arrange
var renderer = new TestRenderer();
var component = new ConditionalParentComponent<HandlePropertiesChangedComponent>
{
IncludeChild = true,
ChildParameters = new Dictionary<string, object>
{
{ nameof(HandlePropertiesChangedComponent.IntProperty), 123 }
}
};
var rootComponentId = renderer.AssignComponentId(component);
// Act/Assert 0: Initial render
renderer.RenderNewBatch(rootComponentId);
var batch1 = renderer.Batches.Single();
var childComponentFrame = batch1
.ReferenceFrames.Where(frame => frame.FrameType == RenderTreeFrameType.Component).Single();
var childComponentId = childComponentFrame.ComponentId;
var diffForChildComponent0 = batch1.DiffsByComponentId[childComponentId].Single();
var childComponentInstance = (HandlePropertiesChangedComponent)childComponentFrame.Component;
Assert.Equal(1, childComponentInstance.NotificationsCount);
Assert.Collection(diffForChildComponent0.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(batch1.ReferenceFrames[edit.ReferenceFrameIndex], "Notifications: 1", 0);
});
// Act/Assert 1: If properties didn't change, we don't notify
renderer.RenderNewBatch(rootComponentId);
Assert.Equal(1, childComponentInstance.NotificationsCount);
// Act/Assert 2: If properties did change, we do notify
component.ChildParameters[nameof(HandlePropertiesChangedComponent.IntProperty)] = 456;
renderer.RenderNewBatch(rootComponentId);
Assert.Equal(2, childComponentInstance.NotificationsCount);
var batch3 = renderer.Batches.Skip(2).Single();
var diffForChildComponent2 = batch3.DiffsByComponentId[childComponentId].Single();
Assert.Equal(2, childComponentInstance.NotificationsCount);
AssertFrame.Text(batch3.ReferenceFrames[0], "Notifications: 2", 0);
}
[Fact]
public void RenderBatchIncludesListOfDisposedComponents()
{
@ -708,7 +648,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Assert
var batch = renderer.Batches.Single();
Assert.Equal(3, batch.DiffsInOrder.Count);
Assert.Equal(4, batch.DiffsInOrder.Count);
// First is the parent component's initial render
var diff1 = batch.DiffsInOrder[0];
@ -743,6 +683,11 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Equal(RenderTreeEditType.UpdateText, diff3edit.Type);
AssertFrame.Text(batch.ReferenceFrames[diff3edit.ReferenceFrameIndex],
"Parent render count: 2");
// Fourth is child's rerender due to parent rendering
var diff4 = batch.DiffsInOrder[3];
Assert.NotEqual(parentComponentId, diff4.ComponentId);
Assert.Empty(diff4.Edits);
}
private class NoOpRenderer : Renderer
@ -750,9 +695,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public new int AssignComponentId(IComponent component)
=> base.AssignComponentId(component);
public new void RenderNewBatch(int componentId)
=> base.RenderNewBatch(componentId);
protected override void UpdateDisplay(RenderBatch renderBatch)
{
}
@ -832,6 +774,9 @@ namespace Microsoft.AspNetCore.Blazor.Test
_renderHandle = renderHandle;
}
public void SetParameters(ParameterCollection parameters)
=> _renderHandle.Render();
public void BuildRenderTree(RenderTreeBuilder builder)
=> _renderAction(builder);
@ -839,15 +784,11 @@ namespace Microsoft.AspNetCore.Blazor.Test
=> _renderHandle.Render();
}
private class MessageComponent : IComponent
private class MessageComponent : AutoRenderComponent
{
public string Message { get; set; }
public void Init(RenderHandle renderHandle)
{
}
public void BuildRenderTree(RenderTreeBuilder builder)
public override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddText(0, Message);
}
@ -859,25 +800,24 @@ 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)
{
}
public void BuildRenderTree(RenderTreeBuilder builder)
{
}
public void SetParameters(ParameterCollection parameters)
=> parameters.AssignToProperties(this);
}
private class EventComponent : IComponent
private class EventComponent : AutoRenderComponent, IComponent
{
public UIEventHandler Handler { get; set; }
public bool SkipElement { get; set; }
public void Init(RenderHandle renderHandle)
{
}
public void BuildRenderTree(RenderTreeBuilder builder)
public override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "grandparent");
if (!SkipElement)
@ -895,16 +835,12 @@ namespace Microsoft.AspNetCore.Blazor.Test
}
}
private class ConditionalParentComponent<T> : IComponent where T : IComponent
private class ConditionalParentComponent<T> : AutoRenderComponent where T : IComponent
{
public bool IncludeChild { get; set; }
public IDictionary<string, object> ChildParameters { get; set; }
public void Init(RenderHandle renderHandle)
{
}
public void BuildRenderTree(RenderTreeBuilder builder)
public override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddText(0, "Parent here");
@ -923,36 +859,13 @@ namespace Microsoft.AspNetCore.Blazor.Test
}
}
}
private class HandlePropertiesChangedComponent : IComponent, IHandlePropertiesChanged
{
public int NotificationsCount { get; private set; }
public int IntProperty { get; set; }
public void Init(RenderHandle renderHandle)
{
}
public void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddText(0, $"Notifications: {NotificationsCount}");
}
public void OnPropertiesChanged()
{
NotificationsCount++;
}
}
private class ReRendersParentComponent : IComponent
private class ReRendersParentComponent : AutoRenderComponent
{
public TestComponent Parent { get; set; }
private bool _isFirstTime = true;
public void Init(RenderHandle renderHandle) { }
public void BuildRenderTree(RenderTreeBuilder builder)
public override void BuildRenderTree(RenderTreeBuilder builder)
{
if (_isFirstTime) // Don't want an infinite loop
{

View File

@ -3,10 +3,9 @@
@functions {
public int SuppliedValue { get; set; }
private int ComputedValue { get; set; }
public override void OnPropertiesChanged()
{
ComputedValue = SuppliedValue * 2;
}
// TODO: Instead of computing this in the getter, override OnParametersChanged or whatever
// it ends up being called when that's implemented on BlazorComponent
private int ComputedValue
=> SuppliedValue * 2;
}