Skip to content
This repository was archived by the owner on Feb 25, 2021. It is now read-only.

"Fixed" mode and E2E tests for <CascadingValue> #1566

Merged
merged 3 commits into from
Oct 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/Microsoft.AspNetCore.Blazor/Components/CascadingValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class CascadingValue<T> : ICascadingValueComponent, IComponent
{
private RenderHandle _renderHandle;
private HashSet<ComponentState> _subscribers; // Lazily instantiated
private bool _hasSetParametersPreviously;

/// <summary>
/// The content to which the value should be provided.
Expand All @@ -35,8 +36,18 @@ public class CascadingValue<T> : ICascadingValueComponent, IComponent
/// </summary>
[Parameter] private string Name { get; set; }

/// <summary>
/// If true, indicates that <see cref="Value"/> will not change. This is a
/// performance optimization that allows the framework to skip setting up
/// change notifications. Set this flag only if you will not change
/// <see cref="Value"/> during the component's lifetime.
/// </summary>
[Parameter] private bool IsFixed { get; set; }

object ICascadingValueComponent.CurrentValue => Value;

bool ICascadingValueComponent.CurrentValueIsFixed => IsFixed;

/// <inheritdoc />
public void Init(RenderHandle renderHandle)
{
Expand All @@ -52,9 +63,11 @@ public void SetParameters(ParameterCollection parameters)

var hasSuppliedValue = false;
var previousValue = Value;
var previousFixed = IsFixed;
Value = default;
ChildContent = null;
Name = null;
IsFixed = false;

foreach (var parameter in parameters)
{
Expand All @@ -75,12 +88,23 @@ public void SetParameters(ParameterCollection parameters)
throw new ArgumentException($"The parameter '{nameof(Name)}' for component '{nameof(CascadingValue<T>)}' does not allow null or empty values.");
}
}
else if (parameter.Name.Equals(nameof(IsFixed), StringComparison.OrdinalIgnoreCase))
{
IsFixed = (bool)parameter.Value;
}
else
{
throw new ArgumentException($"The component '{nameof(CascadingValue<T>)}' does not accept a parameter with the name '{parameter.Name}'.");
}
}

if (_hasSetParametersPreviously && IsFixed != previousFixed)
{
throw new InvalidOperationException($"The value of {nameof(IsFixed)} cannot be changed dynamically.");
}

_hasSetParametersPreviously = true;

// It's OK for the value to be null, but some "Value" param must be suppled
// because it serves no useful purpose to have a <CascadingValue> otherwise.
if (!hasSuppliedValue)
Expand Down Expand Up @@ -120,6 +144,15 @@ bool ICascadingValueComponent.CanSupplyValue(Type requestedType, string requeste

void ICascadingValueComponent.Subscribe(ComponentState subscriber)
{
#if DEBUG
if (IsFixed)
{
// Should not be possible. User code cannot trigger this.
// Checking only to catch possible future framework bugs.
throw new InvalidOperationException($"Cannot subscribe to a {typeof(CascadingValue<>).Name} when {nameof(IsFixed)} is true.");
}
#endif

if (_subscribers == null)
{
_subscribers = new HashSet<ComponentState>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ internal interface ICascadingValueComponent

object CurrentValue { get; }

bool CurrentValueIsFixed { get; }

void Subscribe(ComponentState subscriber);

void Unsubscribe(ComponentState subscriber);
Expand Down
29 changes: 19 additions & 10 deletions src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal class ComponentState
private readonly IComponent _component;
private readonly Renderer _renderer;
private readonly IReadOnlyList<CascadingParameterState> _cascadingParameters;
private readonly bool _hasAnyCascadingParameterSubscriptions;
private RenderTreeBuilder _renderTreeBuilderCurrent;
private RenderTreeBuilder _renderTreeBuilderPrevious;
private ArrayBuilder<RenderTreeFrame> _latestDirectParametersSnapshot; // Lazily instantiated
Expand Down Expand Up @@ -48,7 +49,7 @@ public ComponentState(Renderer renderer, int componentId, IComponent component,

if (_cascadingParameters != null)
{
AddCascadingParameterSubscriptions();
_hasAnyCascadingParameterSubscriptions = AddCascadingParameterSubscriptions();
}
}

Expand Down Expand Up @@ -88,7 +89,7 @@ public void DisposeInBatch(RenderBatchBuilder batchBuilder)

RenderTreeDiffBuilder.DisposeFrames(batchBuilder, _renderTreeBuilderCurrent.GetFrames());

if (_cascadingParameters != null)
if (_hasAnyCascadingParameterSubscriptions)
{
RemoveCascadingParameterSubscriptions();
}
Expand Down Expand Up @@ -119,12 +120,7 @@ public void SetDirectParameters(ParameterCollection parameters)
// If we bypass this, the component won't receive the cascading parameters nor
// will it update its snapshot of direct parameters.

// TODO: Consider adding a "static" mode for tree params in which we don't
// subscribe for updates, and hence don't have to do any of the parameter
// snapshotting. This would be useful for things like FormContext that aren't
// going to change.

if (_cascadingParameters != null)
if (_hasAnyCascadingParameterSubscriptions)
{
// We may need to replay these direct parameters later (in NotifyCascadingValueChanged),
// but we can't guarantee that the original underlying data won't have mutated in the
Expand All @@ -133,8 +129,12 @@ public void SetDirectParameters(ParameterCollection parameters)
{
_latestDirectParametersSnapshot = new ArrayBuilder<RenderTreeFrame>();
}

parameters.CaptureSnapshot(_latestDirectParametersSnapshot);
}

if (_cascadingParameters != null)
{
parameters = parameters.WithCascadingParameters(_cascadingParameters);
}

Expand All @@ -150,13 +150,22 @@ public void NotifyCascadingValueChanged()
Component.SetParameters(allParams);
}

private void AddCascadingParameterSubscriptions()
private bool AddCascadingParameterSubscriptions()
{
var hasSubscription = false;
var numCascadingParameters = _cascadingParameters.Count;

for (var i = 0; i < numCascadingParameters; i++)
{
_cascadingParameters[i].ValueSupplier.Subscribe(this);
var valueSupplier = _cascadingParameters[i].ValueSupplier;
if (!valueSupplier.CurrentValueIsFixed)
{
valueSupplier.Subscribe(this);
hasSubscription = true;
}
}

return hasSubscription;
}

private void RemoveCascadingParameterSubscriptions()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,12 @@ public ServerRoutingTest(BrowserFixture browserFixture, ToggleExecutionModeServe
{
}
}

public class ServerCascadingValueTest : CascadingValueTest
{
public ServerCascadingValueTest(BrowserFixture browserFixture, ToggleExecutionModeServerFixture<Program> serverFixture, ITestOutputHelper output)
: base(browserFixture, serverFixture.WithServerExecution(), output)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// 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 BasicTestApp;
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;

namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
{
public class CascadingValueTest : BasicTestAppTestBase
{
public CascadingValueTest(
BrowserFixture browserFixture,
ToggleExecutionModeServerFixture<Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
Navigate(ServerPathBase, noReload: !serverFixture.UsingAspNetHost);
MountTestComponent<BasicTestApp.CascadingValueTest.CascadingValueSupplier>();
}

[Fact]
public void CanUpdateValuesMatchedByType()
{
var currentCount = Browser.FindElement(By.Id("current-count"));
var incrementButton = Browser.FindElement(By.Id("increment-count"));

// We have the correct initial value
WaitAssert.Equal("100", () => currentCount.Text);

// Updates are propagated
incrementButton.Click();
WaitAssert.Equal("101", () => currentCount.Text);
incrementButton.Click();
WaitAssert.Equal("102", () => currentCount.Text);

// Didn't re-render unrelated descendants
Assert.Equal("1", Browser.FindElement(By.Id("receive-by-interface-num-renders")).Text);
}

[Fact]
public void CanUpdateValuesMatchedByName()
{
var currentFlag1Value = Browser.FindElement(By.Id("flag-1"));
var currentFlag2Value = Browser.FindElement(By.Id("flag-2"));

WaitAssert.Equal("False", () => currentFlag1Value.Text);
WaitAssert.Equal("False", () => currentFlag2Value.Text);

// Observe that the correct cascading parameter updates
Browser.FindElement(By.Id("toggle-flag-1")).Click();
WaitAssert.Equal("True", () => currentFlag1Value.Text);
WaitAssert.Equal("False", () => currentFlag2Value.Text);
Browser.FindElement(By.Id("toggle-flag-2")).Click();
WaitAssert.Equal("True", () => currentFlag1Value.Text);
WaitAssert.Equal("True", () => currentFlag2Value.Text);

// Didn't re-render unrelated descendants
Assert.Equal("1", Browser.FindElement(By.Id("receive-by-interface-num-renders")).Text);
}

[Fact]
public void CanUpdateFixedValuesMatchedByInterface()
{
var currentCount = Browser.FindElement(By.Id("current-count"));
var decrementButton = Browser.FindElement(By.Id("decrement-count"));

// We have the correct initial value
WaitAssert.Equal("100", () => currentCount.Text);

// Updates are propagated
decrementButton.Click();
WaitAssert.Equal("99", () => currentCount.Text);
decrementButton.Click();
WaitAssert.Equal("98", () => currentCount.Text);

// Renders the descendant the same number of times we triggered
// events on it, because we always re-render components after they
// have an event
Assert.Equal("3", Browser.FindElement(By.Id("receive-by-interface-num-renders")).Text);
}
}
}
108 changes: 108 additions & 0 deletions test/Microsoft.AspNetCore.Blazor.Test/CascadingParameterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.RenderTree;
using Microsoft.AspNetCore.Blazor.Test.Helpers;
using System;
using System.Linq;
using Xunit;

Expand Down Expand Up @@ -236,6 +237,113 @@ public void StopsNotifyingDescendantsIfTheyAreRemoved()
Assert.Equal(2, nestedComponent.NumSetParametersCalls);
}

[Fact]
public void DoesNotNotifyDescendantsOfUpdatedCascadingParameterValuesWhenFixed()
{
// Arrange
var providedValue = "Initial value";
var shouldIncludeChild = true;
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.OpenComponent<CascadingValue<string>>(0);
builder.AddAttribute(1, "Value", providedValue);
builder.AddAttribute(2, "IsFixed", true);
builder.AddAttribute(3, RenderTreeBuilder.ChildContent, new RenderFragment(childBuilder =>
{
if (shouldIncludeChild)
{
childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(0);
childBuilder.AddAttribute(1, "RegularParameter", "Goodbye");
childBuilder.CloseComponent();
}
}));
builder.CloseComponent();
});

// Act 1: Initial render; capture nested component ID
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var firstBatch = renderer.Batches.Single();
var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(firstBatch, out var nestedComponentId);
Assert.Equal(1, nestedComponent.NumRenders);

// Assert: Initial value is supplied to descendant
var nestedComponentDiff = firstBatch.DiffsByComponentId[nestedComponentId].Single();
Assert.Collection(nestedComponentDiff.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(
firstBatch.ReferenceFrames[edit.ReferenceFrameIndex],
"CascadingParameter=Initial value; RegularParameter=Goodbye");
});

// Act 2: Re-render CascadingValue with new value
providedValue = "Updated value";
component.TriggerRender();

// Assert: We did not re-render the descendant
Assert.Equal(2, renderer.Batches.Count);
var secondBatch = renderer.Batches[1];
Assert.Equal(2, secondBatch.DiffsByComponentId.Count); // Root + CascadingValue, but not nested one
Assert.Equal(1, nestedComponent.NumSetParametersCalls);
Assert.Equal(1, nestedComponent.NumRenders);

// Act 3: Dispose
shouldIncludeChild = false;
component.TriggerRender();

// Assert: Absence of an exception here implies we didn't cause a problem by
// trying to remove a non-existent subscription
}

[Fact]
public void CascadingValueThrowsIfFixedFlagChangesToTrue()
{
// Arrange
var renderer = new TestRenderer();
var isFixed = false;
var component = new TestComponent(builder =>
{
builder.OpenComponent<CascadingValue<object>>(0);
builder.AddAttribute(1, "IsFixed", isFixed);
builder.AddAttribute(2, "Value", new object());
builder.CloseComponent();
});
renderer.AssignRootComponentId(component);
component.TriggerRender();

// Act/Assert
isFixed = true;
var ex = Assert.Throws<InvalidOperationException>(() => component.TriggerRender());
Assert.Equal("The value of IsFixed cannot be changed dynamically.", ex.Message);
}

[Fact]
public void CascadingValueThrowsIfFixedFlagChangesToFalse()
{
// Arrange
var renderer = new TestRenderer();
var isFixed = true;
var component = new TestComponent(builder =>
{
builder.OpenComponent<CascadingValue<object>>(0);
if (isFixed) // Showing also that "unset" is treated as "false"
{
builder.AddAttribute(1, "IsFixed", true);
}
builder.AddAttribute(2, "Value", new object());
builder.CloseComponent();
});
renderer.AssignRootComponentId(component);
component.TriggerRender();

// Act/Assert
isFixed = false;
var ex = Assert.Throws<InvalidOperationException>(() => component.TriggerRender());
Assert.Equal("The value of IsFixed cannot be changed dynamically.", ex.Message);
}

private static T FindComponent<T>(CapturedBatch batch, out int componentId)
{
var componentFrame = batch.ReferenceFrames.Single(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,8 @@ public TestCascadingValue(object value)

public object CurrentValue { get; }

public bool CurrentValueIsFixed => false;

public bool CanSupplyValue(Type valueType, string valueName)
=> throw new NotImplementedException();

Expand Down
Loading