Skip to content

Commit 6e515a2

Browse files
Sections with other features (#48518)
* Added LogicalParentComponentState to fix how CascadingParameters, ErrorBoundary and StreamingRendering interact with Sections * Changed the way SectionOutlet renders content --------- Co-authored-by: Steve Sanderson <[email protected]>
1 parent 74bd963 commit 6e515a2

25 files changed

+616
-124
lines changed

src/Components/Components/src/CascadingParameterState.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(Com
6565
return valueSupplier;
6666
}
6767

68-
candidate = candidate.ParentComponentState;
68+
candidate = candidate.LogicalParentComponentState;
6969
} while (candidate != null);
7070

7171
// No match

src/Components/Components/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Microsoft.AspNetCore.Components.ParameterView.ToDictionary() -> System.Collectio
4141
Microsoft.AspNetCore.Components.RenderHandle.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
4242
*REMOVED*Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string! relativeUri) -> System.Uri!
4343
Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string? relativeUri) -> System.Uri!
44+
Microsoft.AspNetCore.Components.Rendering.ComponentState.LogicalParentComponentState.get -> Microsoft.AspNetCore.Components.Rendering.ComponentState?
4445
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.SetEventHandlerName(string! eventHandlerName) -> void
4546
*REMOVED*Microsoft.AspNetCore.Components.RouteData.RouteData(System.Type! pageType, System.Collections.Generic.IReadOnlyDictionary<string!, object!>! routeValues) -> void
4647
*REMOVED*Microsoft.AspNetCore.Components.RouteData.RouteValues.get -> System.Collections.Generic.IReadOnlyDictionary<string!, object!>!

src/Components/Components/src/RenderTree/Renderer.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid
126126
protected ComponentState GetComponentState(int componentId)
127127
=> GetRequiredComponentState(componentId);
128128

129+
internal ComponentState GetComponentState(IComponent component)
130+
=> _componentStateByComponent.GetValueOrDefault(component);
131+
129132
private async void RenderRootComponentsOnHotReload()
130133
{
131134
// Before re-rendering the root component, also clear any well-known caches in the framework
@@ -1038,7 +1041,7 @@ private void HandleExceptionViaErrorBoundary(Exception error, ComponentState? er
10381041
return; // Handled successfully
10391042
}
10401043

1041-
candidate = candidate.ParentComponentState;
1044+
candidate = candidate.LogicalParentComponentState;
10421045
}
10431046

10441047
// It's unhandled, so treat as fatal

src/Components/Components/src/Rendering/ComponentState.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Diagnostics;
55
using Microsoft.AspNetCore.Components.RenderTree;
6+
using Microsoft.AspNetCore.Components.Sections;
67

78
namespace Microsoft.AspNetCore.Components.Rendering;
89

@@ -34,6 +35,9 @@ public ComponentState(Renderer renderer, int componentId, IComponent component,
3435
ComponentId = componentId;
3536
ParentComponentState = parentComponentState;
3637
Component = component ?? throw new ArgumentNullException(nameof(component));
38+
LogicalParentComponentState = component is SectionOutlet.SectionOutletContentRenderer
39+
? (GetSectionOutletLogicalParent(renderer, (SectionOutlet)parentComponentState!.Component) ?? parentComponentState)
40+
: parentComponentState;
3741
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
3842
_cascadingParameters = CascadingParameterState.FindCascadingParameters(this);
3943
CurrentRenderTree = new RenderTreeBuilder();
@@ -46,6 +50,18 @@ public ComponentState(Renderer renderer, int componentId, IComponent component,
4650
}
4751
}
4852

53+
private static ComponentState? GetSectionOutletLogicalParent(Renderer renderer, SectionOutlet sectionOutlet)
54+
{
55+
// This will return null if the SectionOutlet is not currently rendering any content
56+
if (sectionOutlet.CurrentLogicalParent is { } logicalParent
57+
&& renderer.GetComponentState(logicalParent) is { } logicalParentComponentState)
58+
{
59+
return logicalParentComponentState;
60+
}
61+
62+
return null;
63+
}
64+
4965
/// <summary>
5066
/// Gets the component ID.
5167
/// </summary>
@@ -61,6 +77,11 @@ public ComponentState(Renderer renderer, int componentId, IComponent component,
6177
/// </summary>
6278
public ComponentState? ParentComponentState { get; }
6379

80+
/// <summary>
81+
/// Gets the <see cref="ComponentState"/> of the logical parent component, or null if this is a root component.
82+
/// </summary>
83+
public ComponentState? LogicalParentComponentState { get; }
84+
6485
internal RenderTreeBuilder CurrentRenderTree { get; set; }
6586

6687
internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, out Exception? renderFragmentException)

src/Components/Components/src/Sections/ISectionContentProvider.cs

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/Components/Components/src/Sections/ISectionContentSubscriber.cs

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/Components/Components/src/Sections/SectionContent.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Components.Sections;
66
/// <summary>
77
/// Provides content to <see cref="SectionOutlet"/> components with matching <see cref="SectionId"/>s.
88
/// </summary>
9-
public sealed class SectionContent : ISectionContentProvider, IComponent, IDisposable
9+
public sealed class SectionContent : IComponent, IDisposable
1010
{
1111
private object? _registeredIdentifier;
1212
private bool? _registeredIsDefaultContent;
@@ -35,8 +35,6 @@ public sealed class SectionContent : ISectionContentProvider, IComponent, IDispo
3535
/// </summary>
3636
[Parameter] public RenderFragment? ChildContent { get; set; }
3737

38-
RenderFragment? ISectionContentProvider.Content => ChildContent;
39-
4038
void IComponent.Attach(RenderHandle renderHandle)
4139
{
4240
_registry = renderHandle.Dispatcher.SectionRegistry;
@@ -79,7 +77,7 @@ Task IComponent.SetParametersAsync(ParameterView parameters)
7977
_registeredIsDefaultContent = IsDefaultContent;
8078
}
8179

82-
_registry.NotifyContentChanged(identifier, this);
80+
_registry.NotifyContentProviderChanged(identifier, this);
8381

8482
return Task.CompletedTask;
8583
}

src/Components/Components/src/Sections/SectionOutlet.cs

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.AspNetCore.Components.Rendering;
5+
46
namespace Microsoft.AspNetCore.Components.Sections;
57

68
/// <summary>
79
/// Renders content provided by <see cref="SectionContent"/> components with matching <see cref="SectionId"/>s.
810
/// </summary>
9-
[StreamRendering(true)] // Because the content may be provided by a streaming component
10-
public sealed class SectionOutlet : ISectionContentSubscriber, IComponent, IDisposable
11+
public sealed class SectionOutlet : IComponent, IDisposable
1112
{
1213
private static readonly RenderFragment _emptyRenderFragment = _ => { };
13-
1414
private object? _subscribedIdentifier;
1515
private RenderHandle _renderHandle;
1616
private SectionRegistry _registry = default!;
17-
private RenderFragment? _content;
17+
private SectionContent? _currentContentProvider;
1818

1919
/// <summary>
2020
/// Gets or sets the <see cref="string"/> ID that determines which <see cref="SectionContent"/> instances will provide
@@ -28,6 +28,8 @@ public sealed class SectionOutlet : ISectionContentSubscriber, IComponent, IDisp
2828
/// </summary>
2929
[Parameter] public object? SectionId { get; set; }
3030

31+
internal IComponent? CurrentLogicalParent => _currentContentProvider;
32+
3133
void IComponent.Attach(RenderHandle renderHandle)
3234
{
3335
_renderHandle = renderHandle;
@@ -73,9 +75,9 @@ Task IComponent.SetParametersAsync(ParameterView parameters)
7375
return Task.CompletedTask;
7476
}
7577

76-
void ISectionContentSubscriber.ContentChanged(RenderFragment? content)
78+
internal void ContentUpdated(SectionContent? provider)
7779
{
78-
_content = content;
80+
_currentContentProvider = provider;
7981
RenderContent();
8082
}
8183

@@ -89,7 +91,17 @@ private void RenderContent()
8991
return;
9092
}
9193

92-
_renderHandle.Render(_content ?? _emptyRenderFragment);
94+
_renderHandle.Render(BuildRenderTree);
95+
}
96+
97+
private void BuildRenderTree(RenderTreeBuilder builder)
98+
{
99+
var fragment = _currentContentProvider?.ChildContent ?? _emptyRenderFragment;
100+
101+
builder.OpenComponent<SectionOutletContentRenderer>(0);
102+
builder.SetKey(fragment);
103+
builder.AddComponentParameter(1, SectionOutletContentRenderer.ContentParameterName, fragment);
104+
builder.CloseComponent();
93105
}
94106

95107
/// <inheritdoc/>
@@ -100,4 +112,33 @@ public void Dispose()
100112
_registry.Unsubscribe(_subscribedIdentifier);
101113
}
102114
}
115+
116+
// This component simply renders the RenderFragment it is given
117+
// The reason for rendering SectionOutlet output via this component is so that
118+
// [1] We can use @key to guarantee that we only preserve descendant component
119+
// instances when they come from the same SectionContent, not unrelated ones
120+
// [2] We know that whenever the SectionContent is changed to another one, there
121+
// will be a new ComponentState established to represent this intermediate
122+
// component, and it will already have the correct LogicalParentComponentState
123+
// so anything computed from this (e.g., whether or not streaming rendering is
124+
// enabled) will be freshly re-evaluated, without that information having to
125+
// change in place on an existing ComponentState.
126+
internal sealed class SectionOutletContentRenderer : IComponent
127+
{
128+
public const string ContentParameterName = "content";
129+
130+
private RenderHandle _renderHandle;
131+
132+
public void Attach(RenderHandle renderHandle)
133+
{
134+
_renderHandle = renderHandle;
135+
}
136+
137+
public Task SetParametersAsync(ParameterView parameters)
138+
{
139+
var fragment = parameters.GetValueOrDefault<RenderFragment>(ContentParameterName)!;
140+
_renderHandle.Render(fragment);
141+
return Task.CompletedTask;
142+
}
143+
}
103144
}

src/Components/Components/src/Sections/SectionRegistry.cs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ namespace Microsoft.AspNetCore.Components.Sections;
55

66
internal sealed class SectionRegistry
77
{
8-
private readonly Dictionary<object, ISectionContentSubscriber> _subscribersByIdentifier = new();
9-
private readonly Dictionary<object, List<ISectionContentProvider>> _providersByIdentifier = new();
8+
private readonly Dictionary<object, SectionOutlet> _subscribersByIdentifier = new();
9+
private readonly Dictionary<object, List<SectionContent>> _providersByIdentifier = new();
1010

11-
public void AddProvider(object identifier, ISectionContentProvider provider, bool isDefaultProvider)
11+
public void AddProvider(object identifier, SectionContent provider, bool isDefaultProvider)
1212
{
1313
if (!_providersByIdentifier.TryGetValue(identifier, out var providers))
1414
{
@@ -26,7 +26,7 @@ public void AddProvider(object identifier, ISectionContentProvider provider, boo
2626
}
2727
}
2828

29-
public void RemoveProvider(object identifier, ISectionContentProvider provider)
29+
public void RemoveProvider(object identifier, SectionContent provider)
3030
{
3131
if (!_providersByIdentifier.TryGetValue(identifier, out var providers))
3232
{
@@ -46,21 +46,21 @@ public void RemoveProvider(object identifier, ISectionContentProvider provider)
4646
{
4747
// We just removed the most recently added provider, meaning we need to change
4848
// the current content to that of second most recently added provider.
49-
var content = GetCurrentProviderContentOrDefault(providers);
50-
NotifyContentChangedForSubscriber(identifier, content);
49+
var contentProvider = GetCurrentProviderContentOrDefault(providers);
50+
NotifyContentChangedForSubscriber(identifier, contentProvider);
5151
}
5252
}
5353

54-
public void Subscribe(object identifier, ISectionContentSubscriber subscriber)
54+
public void Subscribe(object identifier, SectionOutlet subscriber)
5555
{
5656
if (_subscribersByIdentifier.ContainsKey(identifier))
5757
{
5858
throw new InvalidOperationException($"There is already a subscriber to the content with the given section ID '{identifier}'.");
5959
}
6060

6161
// Notify the new subscriber with any existing content.
62-
var content = GetCurrentProviderContentOrDefault(identifier);
63-
subscriber.ContentChanged(content);
62+
var provider = GetCurrentProviderContentOrDefault(identifier);
63+
subscriber.ContentUpdated(provider);
6464

6565
_subscribersByIdentifier.Add(identifier, subscriber);
6666
}
@@ -73,7 +73,7 @@ public void Unsubscribe(object identifier)
7373
}
7474
}
7575

76-
public void NotifyContentChanged(object identifier, ISectionContentProvider provider)
76+
public void NotifyContentProviderChanged(object identifier, SectionContent provider)
7777
{
7878
if (!_providersByIdentifier.TryGetValue(identifier, out var providers))
7979
{
@@ -84,25 +84,25 @@ public void NotifyContentChanged(object identifier, ISectionContentProvider prov
8484
// most recently added provider changes.
8585
if (providers.Count != 0 && providers[^1] == provider)
8686
{
87-
NotifyContentChangedForSubscriber(identifier, provider.Content);
87+
NotifyContentChangedForSubscriber(identifier, provider);
8888
}
8989
}
9090

91-
private static RenderFragment? GetCurrentProviderContentOrDefault(List<ISectionContentProvider> providers)
91+
private static SectionContent? GetCurrentProviderContentOrDefault(List<SectionContent> providers)
9292
=> providers.Count != 0
93-
? providers[^1].Content
93+
? providers[^1]
9494
: null;
9595

96-
private RenderFragment? GetCurrentProviderContentOrDefault(object identifier)
96+
private SectionContent? GetCurrentProviderContentOrDefault(object identifier)
9797
=> _providersByIdentifier.TryGetValue(identifier, out var existingList)
9898
? GetCurrentProviderContentOrDefault(existingList)
9999
: null;
100100

101-
private void NotifyContentChangedForSubscriber(object identifier, RenderFragment? content)
101+
private void NotifyContentChangedForSubscriber(object identifier, SectionContent? provider)
102102
{
103103
if (_subscribersByIdentifier.TryGetValue(identifier, out var subscriber))
104104
{
105-
subscriber.ContentChanged(content);
105+
subscriber.ContentUpdated(provider);
106106
}
107107
}
108108
}

src/Components/Endpoints/src/Rendering/EndpointComponentState.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public EndpointComponentState(Renderer renderer, int componentId, IComponent com
2828
}
2929
else
3030
{
31-
var parentEndpointComponentState = (EndpointComponentState?)parentComponentState;
31+
var parentEndpointComponentState = (EndpointComponentState?)LogicalParentComponentState;
3232
StreamRendering = parentEndpointComponentState?.StreamRendering ?? false;
3333
}
3434
}

0 commit comments

Comments
 (0)