Skip to content

Commit a081d5d

Browse files
[Blazor] Allow cascading value subscribers to get added and removed during change notification (#57243)
* Allow cascading value subscribers to change during notification * PR feedback * Use `[InlineArray]` * Suppress warning + add tests
1 parent bbad091 commit a081d5d

File tree

2 files changed

+148
-1
lines changed

2 files changed

+148
-1
lines changed

src/Components/Components/src/CascadingValueSource.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Concurrent;
5+
using System.Runtime.CompilerServices;
56
using Microsoft.AspNetCore.Components.Rendering;
67

78
namespace Microsoft.AspNetCore.Components;
@@ -96,7 +97,16 @@ public Task NotifyChangedAsync()
9697
{
9798
tasks.Add(dispatcher.InvokeAsync(() =>
9899
{
99-
foreach (var subscriber in subscribers)
100+
var subscribersBuffer = new ComponentStateBuffer();
101+
var subscribersCount = subscribers.Count;
102+
var subscribersCopy = subscribersCount <= ComponentStateBuffer.Capacity
103+
? subscribersBuffer[..subscribersCount]
104+
: new ComponentState[subscribersCount];
105+
subscribers.CopyTo(subscribersCopy);
106+
107+
// We iterate over a copy of the list because new subscribers might get
108+
// added or removed during change notification
109+
foreach (var subscriber in subscribersCopy)
100110
{
101111
subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound);
102112
}
@@ -174,4 +184,15 @@ void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in Cascading
174184
}
175185
}
176186
}
187+
188+
[InlineArray(Capacity)]
189+
internal struct ComponentStateBuffer
190+
{
191+
public const int Capacity = 64;
192+
#pragma warning disable IDE0051 // Remove unused private members
193+
#pragma warning disable IDE0044 // Add readonly modifier
194+
private ComponentState _values;
195+
#pragma warning restore IDE0044 // Add readonly modifier
196+
#pragma warning restore IDE0051 // Remove unused private members
197+
}
177198
}

src/Components/Components/test/CascadingParameterTest.cs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,61 @@ public async Task CanTriggerUpdatesOnCascadingValuesFromServiceProvider()
634634
await cascadingValueSource.NotifyChangedAsync(new MyParamType("Nobody is listening, but this shouldn't be an error"));
635635
}
636636

637+
[Fact]
638+
public async Task CanAddSubscriberDuringChangeNotification()
639+
{
640+
// Arrange
641+
var services = new ServiceCollection();
642+
var paramValue = new MyParamType("Initial value");
643+
var cascadingValueSource = new CascadingValueSource<MyParamType>(paramValue, isFixed: false);
644+
services.AddCascadingValue(_ => cascadingValueSource);
645+
var renderer = new TestRenderer(services.BuildServiceProvider());
646+
var component = new ConditionallyRenderSubscriberComponent()
647+
{
648+
RenderWhenEqualTo = "Final value",
649+
};
650+
651+
// Act/Assert: Initial render
652+
var componentId = await renderer.Dispatcher.InvokeAsync(() => renderer.AssignRootComponentId(component));
653+
renderer.RenderRootComponent(componentId);
654+
var firstBatch = renderer.Batches.Single();
655+
var diff = firstBatch.DiffsByComponentId[componentId].Single();
656+
Assert.Collection(diff.Edits,
657+
edit =>
658+
{
659+
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
660+
AssertFrame.Text(
661+
firstBatch.ReferenceFrames[edit.ReferenceFrameIndex],
662+
"CascadingParameter=Initial value");
663+
});
664+
Assert.Equal(1, component.NumRenders);
665+
666+
// Act: Second render
667+
paramValue.ChangeValue("Final value");
668+
await cascadingValueSource.NotifyChangedAsync();
669+
var secondBatch = renderer.Batches[1];
670+
var diff2 = secondBatch.DiffsByComponentId[componentId].Single();
671+
672+
// Assert: Subscriber can get added during change notification and receive the cascading value
673+
AssertFrame.Text(
674+
secondBatch.ReferenceFrames[diff2.Edits[0].ReferenceFrameIndex],
675+
"CascadingParameter=Final value");
676+
Assert.Equal(2, component.NumRenders);
677+
678+
// Assert: Subscriber can get added during change notification and receive the cascading value
679+
var nestedComponent = FindComponent<SimpleSubscriberComponent>(secondBatch, out var nestedComponentId);
680+
var nestedComponentDiff = secondBatch.DiffsByComponentId[nestedComponentId].Single();
681+
Assert.Collection(nestedComponentDiff.Edits,
682+
edit =>
683+
{
684+
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
685+
AssertFrame.Text(
686+
secondBatch.ReferenceFrames[edit.ReferenceFrameIndex],
687+
"CascadingParameter=Final value");
688+
});
689+
Assert.Equal(1, nestedComponent.NumRenders);
690+
}
691+
637692
[Fact]
638693
public async Task AfterSupplyingValueThroughNotifyChanged_InitialValueFactoryIsNotUsed()
639694
{
@@ -772,6 +827,40 @@ public void CanUseTryAddPatternForCascadingValuesInServiceCollection_CascadingVa
772827
Assert.Equal(2, services.Count());
773828
}
774829

830+
[Theory]
831+
[InlineData(0)]
832+
[InlineData(1)]
833+
[InlineData(CascadingValueSource<MyParamType>.ComponentStateBuffer.Capacity - 1)]
834+
[InlineData(CascadingValueSource<MyParamType>.ComponentStateBuffer.Capacity)]
835+
[InlineData(CascadingValueSource<MyParamType>.ComponentStateBuffer.Capacity + 1)]
836+
[InlineData(CascadingValueSource<MyParamType>.ComponentStateBuffer.Capacity * 2)]
837+
public async Task CanHaveManySubscribers(int numSubscribers)
838+
{
839+
// Arrange
840+
var services = new ServiceCollection();
841+
var paramValue = new MyParamType("Initial value");
842+
var cascadingValueSource = new CascadingValueSource<MyParamType>(paramValue, isFixed: false);
843+
services.AddCascadingValue(_ => cascadingValueSource);
844+
var renderer = new TestRenderer(services.BuildServiceProvider());
845+
var components = Enumerable.Range(0, numSubscribers).Select(_ => new SimpleSubscriberComponent()).ToArray();
846+
847+
// Act/Assert: Initial render
848+
foreach (var component in components)
849+
{
850+
await renderer.Dispatcher.InvokeAsync(() => renderer.AssignRootComponentId(component));
851+
component.TriggerRender();
852+
Assert.Equal(1, component.NumRenders);
853+
}
854+
855+
// Act/Assert: All components re-render when the cascading value changes
856+
paramValue.ChangeValue("Final value");
857+
await cascadingValueSource.NotifyChangedAsync();
858+
foreach (var component in components)
859+
{
860+
Assert.Equal(2, component.NumRenders);
861+
}
862+
}
863+
775864
private class SingleDeliveryValue(string text)
776865
{
777866
public string Text => text;
@@ -861,6 +950,43 @@ public void AttemptIllegalAccessToLastParameterView()
861950
}
862951
}
863952

953+
class ConditionallyRenderSubscriberComponent : AutoRenderComponent
954+
{
955+
public int NumRenders { get; private set; }
956+
957+
public SimpleSubscriberComponent NestedSubscriber { get; private set; }
958+
959+
[Parameter] public string RenderWhenEqualTo { get; set; }
960+
961+
[CascadingParameter] MyParamType CascadingParameter { get; set; }
962+
963+
protected override void BuildRenderTree(RenderTreeBuilder builder)
964+
{
965+
NumRenders++;
966+
builder.AddContent(0, $"CascadingParameter={CascadingParameter}");
967+
968+
if (string.Equals(RenderWhenEqualTo, CascadingParameter.ToString(), StringComparison.OrdinalIgnoreCase))
969+
{
970+
builder.OpenComponent<SimpleSubscriberComponent>(1);
971+
builder.AddComponentReferenceCapture(2, component => NestedSubscriber = component as SimpleSubscriberComponent);
972+
builder.CloseComponent();
973+
}
974+
}
975+
}
976+
977+
class SimpleSubscriberComponent : AutoRenderComponent
978+
{
979+
public int NumRenders { get; private set; }
980+
981+
[CascadingParameter] MyParamType CascadingParameter { get; set; }
982+
983+
protected override void BuildRenderTree(RenderTreeBuilder builder)
984+
{
985+
NumRenders++;
986+
builder.AddContent(0, $"CascadingParameter={CascadingParameter}");
987+
}
988+
}
989+
864990
class SingleDeliveryParameterConsumerComponent : AutoRenderComponent
865991
{
866992
public int NumSetParametersCalls { get; private set; }

0 commit comments

Comments
 (0)