Skip to content

Commit 83dad2c

Browse files
Sections support (#46727)
* sections support --------- Co-authored-by: Mackinnon Buck <[email protected]>
1 parent bb41532 commit 83dad2c

File tree

13 files changed

+355
-66
lines changed

13 files changed

+355
-66
lines changed

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

+12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.
44
Microsoft.AspNetCore.Components.RenderHandle.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
55
*REMOVED*Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string! relativeUri) -> System.Uri!
66
Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string? relativeUri) -> System.Uri!
7+
Microsoft.AspNetCore.Components.Sections.SectionContent
8+
Microsoft.AspNetCore.Components.Sections.SectionContent.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment?
9+
Microsoft.AspNetCore.Components.Sections.SectionContent.ChildContent.set -> void
10+
Microsoft.AspNetCore.Components.Sections.SectionContent.Dispose() -> void
11+
Microsoft.AspNetCore.Components.Sections.SectionContent.SectionContent() -> void
12+
Microsoft.AspNetCore.Components.Sections.SectionContent.SectionId.get -> object!
13+
Microsoft.AspNetCore.Components.Sections.SectionContent.SectionId.set -> void
14+
Microsoft.AspNetCore.Components.Sections.SectionOutlet
15+
Microsoft.AspNetCore.Components.Sections.SectionOutlet.Dispose() -> void
16+
Microsoft.AspNetCore.Components.Sections.SectionOutlet.SectionId.get -> object!
17+
Microsoft.AspNetCore.Components.Sections.SectionOutlet.SectionId.set -> void
18+
Microsoft.AspNetCore.Components.Sections.SectionOutlet.SectionOutlet() -> void
719
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentParameter(int sequence, string! name, object? value) -> void
820
override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int
921
override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool

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

+35-17
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,25 @@
44
namespace Microsoft.AspNetCore.Components.Sections;
55

66
/// <summary>
7-
/// Provides content to <see cref="SectionOutlet"/> components with matching <see cref="Name"/>s.
7+
/// Provides content to <see cref="SectionOutlet"/> components with matching <see cref="SectionId"/>s.
88
/// </summary>
9-
internal sealed class SectionContent : ISectionContentProvider, IComponent, IDisposable
9+
public sealed class SectionContent : ISectionContentProvider, IComponent, IDisposable
1010
{
11-
private string? _registeredName;
11+
private object? _registeredSectionId;
12+
private bool? _registeredIsDefaultContent;
1213
private SectionRegistry _registry = default!;
1314

1415
/// <summary>
15-
/// Gets or sets the name that determines which <see cref="SectionOutlet"/> instance will render
16+
/// Gets or sets the ID that determines which <see cref="SectionOutlet"/> instance will render
1617
/// the content of this instance.
1718
/// </summary>
18-
[Parameter] public string Name { get; set; } = default!;
19+
[Parameter, EditorRequired] public object SectionId { get; set; } = default!;
1920

2021
/// <summary>
2122
/// Gets or sets whether this component should provide the default content for the target
2223
/// <see cref="SectionOutlet"/>.
2324
/// </summary>
24-
[Parameter] public bool IsDefaultContent { get; set; }
25+
internal bool IsDefaultContent { get; set; }
2526

2627
/// <summary>
2728
/// Gets or sets the content to be rendered in corresponding <see cref="SectionOutlet"/> instances.
@@ -37,35 +38,52 @@ void IComponent.Attach(RenderHandle renderHandle)
3738

3839
Task IComponent.SetParametersAsync(ParameterView parameters)
3940
{
40-
parameters.SetParameterProperties(this);
41+
foreach (var param in parameters)
42+
{
43+
switch (param.Name)
44+
{
45+
case nameof(SectionContent.SectionId):
46+
SectionId = param.Value;
47+
break;
48+
case nameof(SectionContent.IsDefaultContent):
49+
IsDefaultContent = (bool)param.Value;
50+
break;
51+
case nameof(SectionContent.ChildContent):
52+
ChildContent = (RenderFragment)param.Value;
53+
break;
54+
default:
55+
throw new ArgumentException($"Unknown parameter '{param.Name}'");
56+
}
57+
}
4158

42-
if (string.IsNullOrEmpty(Name))
59+
if (SectionId is null)
4360
{
44-
throw new InvalidOperationException($"{GetType()} requires a non-empty string parameter '{nameof(Name)}'.");
61+
throw new InvalidOperationException($"{nameof(SectionContent)} requires a non-null value for the parameter '{nameof(SectionId)}'.");
4562
}
4663

47-
if (Name != _registeredName)
64+
if (!object.Equals(SectionId, _registeredSectionId) || IsDefaultContent != _registeredIsDefaultContent)
4865
{
49-
if (_registeredName is not null)
66+
if (_registeredSectionId is not null)
5067
{
51-
_registry.RemoveProvider(_registeredName, this);
68+
_registry.RemoveProvider(_registeredSectionId, this);
5269
}
5370

54-
_registry.AddProvider(Name, this, IsDefaultContent);
55-
_registeredName = Name;
71+
_registry.AddProvider(SectionId, this, IsDefaultContent);
72+
_registeredSectionId = SectionId;
73+
_registeredIsDefaultContent = IsDefaultContent;
5674
}
5775

58-
_registry.NotifyContentChanged(Name, this);
76+
_registry.NotifyContentChanged(SectionId, this);
5977

6078
return Task.CompletedTask;
6179
}
6280

6381
/// <inheritdoc/>
6482
public void Dispose()
6583
{
66-
if (_registeredName is not null)
84+
if (_registeredSectionId is not null)
6785
{
68-
_registry.RemoveProvider(_registeredName, this);
86+
_registry.RemoveProvider(_registeredSectionId, this);
6987
}
7088
}
7189
}

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

+14-14
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,22 @@
44
namespace Microsoft.AspNetCore.Components.Sections;
55

66
/// <summary>
7-
/// Renders content provided by <see cref="SectionContent"/> components with matching <see cref="Name"/>s.
7+
/// Renders content provided by <see cref="SectionContent"/> components with matching <see cref="SectionId"/>s.
88
/// </summary>
9-
internal sealed class SectionOutlet : ISectionContentSubscriber, IComponent, IDisposable
9+
public sealed class SectionOutlet : ISectionContentSubscriber, IComponent, IDisposable
1010
{
1111
private static readonly RenderFragment _emptyRenderFragment = _ => { };
1212

13-
private string? _subscribedName;
13+
private object? _subscribedSectionId;
1414
private RenderHandle _renderHandle;
1515
private SectionRegistry _registry = default!;
1616
private RenderFragment? _content;
1717

1818
/// <summary>
19-
/// Gets or sets the name that determines which <see cref="SectionContent"/> instances will provide
19+
/// Gets or sets the ID that determines which <see cref="SectionContent"/> instances will provide
2020
/// content to this instance.
2121
/// </summary>
22-
[Parameter] public string Name { get; set; } = default!;
22+
[Parameter, EditorRequired] public object SectionId { get; set; } = default!;
2323

2424
void IComponent.Attach(RenderHandle renderHandle)
2525
{
@@ -31,20 +31,20 @@ Task IComponent.SetParametersAsync(ParameterView parameters)
3131
{
3232
parameters.SetParameterProperties(this);
3333

34-
if (string.IsNullOrEmpty(Name))
34+
if (SectionId is null)
3535
{
36-
throw new InvalidOperationException($"{GetType()} requires a non-empty string parameter '{nameof(Name)}'.");
36+
throw new InvalidOperationException($"{nameof(SectionOutlet)} requires a non-null value for the parameter '{nameof(SectionId)}'.");
3737
}
3838

39-
if (Name != _subscribedName)
39+
if (!object.Equals(SectionId, _subscribedSectionId))
4040
{
41-
if (_subscribedName is not null)
41+
if (_subscribedSectionId is not null)
4242
{
43-
_registry.Unsubscribe(_subscribedName);
43+
_registry.Unsubscribe(_subscribedSectionId);
4444
}
4545

46-
_registry.Subscribe(Name, this);
47-
_subscribedName = Name;
46+
_registry.Subscribe(SectionId, this);
47+
_subscribedSectionId = SectionId;
4848
}
4949

5050
RenderContent();
@@ -74,9 +74,9 @@ private void RenderContent()
7474
/// <inheritdoc/>
7575
public void Dispose()
7676
{
77-
if (_subscribedName is not null)
77+
if (_subscribedSectionId is not null)
7878
{
79-
_registry.Unsubscribe(_subscribedName);
79+
_registry.Unsubscribe(_subscribedSectionId);
8080
}
8181
}
8282
}

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

+26-26
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ namespace Microsoft.AspNetCore.Components.Sections;
55

66
internal sealed class SectionRegistry
77
{
8-
private readonly Dictionary<string, ISectionContentSubscriber> _subscribersByName = new();
9-
private readonly Dictionary<string, List<ISectionContentProvider>> _providersByName = new();
8+
private readonly Dictionary<object, ISectionContentSubscriber> _subscribersBySectionId = new();
9+
private readonly Dictionary<object, List<ISectionContentProvider>> _providersBySectionId = new();
1010

11-
public void AddProvider(string name, ISectionContentProvider provider, bool isDefaultProvider)
11+
public void AddProvider(object sectionId, ISectionContentProvider provider, bool isDefaultProvider)
1212
{
13-
if (!_providersByName.TryGetValue(name, out var providers))
13+
if (!_providersBySectionId.TryGetValue(sectionId, out var providers))
1414
{
1515
providers = new();
16-
_providersByName.Add(name, providers);
16+
_providersBySectionId.Add(sectionId, providers);
1717
}
1818

1919
if (isDefaultProvider)
@@ -26,18 +26,18 @@ public void AddProvider(string name, ISectionContentProvider provider, bool isDe
2626
}
2727
}
2828

29-
public void RemoveProvider(string name, ISectionContentProvider provider)
29+
public void RemoveProvider(object sectionId, ISectionContentProvider provider)
3030
{
31-
if (!_providersByName.TryGetValue(name, out var providers))
31+
if (!_providersBySectionId.TryGetValue(sectionId, out var providers))
3232
{
33-
throw new InvalidOperationException($"There are no content providers with the name '{name}'.");
33+
throw new InvalidOperationException($"There are no content providers with the given section ID '{sectionId}'.");
3434
}
3535

3636
var index = providers.LastIndexOf(provider);
3737

3838
if (index < 0)
3939
{
40-
throw new InvalidOperationException($"The provider was not found in the providers list of name '{name}'.");
40+
throw new InvalidOperationException($"The provider was not found in the providers list of the given section ID '{sectionId}'.");
4141
}
4242

4343
providers.RemoveAt(index);
@@ -47,44 +47,44 @@ public void RemoveProvider(string name, ISectionContentProvider provider)
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.
4949
var content = GetCurrentProviderContentOrDefault(providers);
50-
NotifyContentChangedForSubscriber(name, content);
50+
NotifyContentChangedForSubscriber(sectionId, content);
5151
}
5252
}
5353

54-
public void Subscribe(string name, ISectionContentSubscriber subscriber)
54+
public void Subscribe(object sectionId, ISectionContentSubscriber subscriber)
5555
{
56-
if (_subscribersByName.ContainsKey(name))
56+
if (_subscribersBySectionId.ContainsKey(sectionId))
5757
{
58-
throw new InvalidOperationException($"There is already a subscriber to the content '{name}'.");
58+
throw new InvalidOperationException($"There is already a subscriber to the content with the given section ID '{sectionId}'.");
5959
}
6060

6161
// Notify the new subscriber with any existing content.
62-
var content = GetCurrentProviderContentOrDefault(name);
62+
var content = GetCurrentProviderContentOrDefault(sectionId);
6363
subscriber.ContentChanged(content);
6464

65-
_subscribersByName.Add(name, subscriber);
65+
_subscribersBySectionId.Add(sectionId, subscriber);
6666
}
6767

68-
public void Unsubscribe(string name)
68+
public void Unsubscribe(object sectionId)
6969
{
70-
if (!_subscribersByName.Remove(name))
70+
if (!_subscribersBySectionId.Remove(sectionId))
7171
{
72-
throw new InvalidOperationException($"The subscriber with name '{name}' is already unsubscribed.");
72+
throw new InvalidOperationException($"The subscriber with the given section ID '{sectionId}' is already unsubscribed.");
7373
}
7474
}
7575

76-
public void NotifyContentChanged(string name, ISectionContentProvider provider)
76+
public void NotifyContentChanged(object sectionId, ISectionContentProvider provider)
7777
{
78-
if (!_providersByName.TryGetValue(name, out var providers))
78+
if (!_providersBySectionId.TryGetValue(sectionId, out var providers))
7979
{
80-
throw new InvalidOperationException($"There are no content providers with the name '{name}'.");
80+
throw new InvalidOperationException($"There are no content providers with the given section ID '{sectionId}'.");
8181
}
8282

8383
// We only notify content changed for subscribers when the content of the
8484
// most recently added provider changes.
8585
if (providers.Count != 0 && providers[^1] == provider)
8686
{
87-
NotifyContentChangedForSubscriber(name, provider.Content);
87+
NotifyContentChangedForSubscriber(sectionId, provider.Content);
8888
}
8989
}
9090

@@ -93,14 +93,14 @@ public void NotifyContentChanged(string name, ISectionContentProvider provider)
9393
? providers[^1].Content
9494
: null;
9595

96-
private RenderFragment? GetCurrentProviderContentOrDefault(string name)
97-
=> _providersByName.TryGetValue(name, out var existingList)
96+
private RenderFragment? GetCurrentProviderContentOrDefault(object sectionId)
97+
=> _providersBySectionId.TryGetValue(sectionId, out var existingList)
9898
? GetCurrentProviderContentOrDefault(existingList)
9999
: null;
100100

101-
private void NotifyContentChangedForSubscriber(string name, RenderFragment? content)
101+
private void NotifyContentChangedForSubscriber(object sectionId, RenderFragment? content)
102102
{
103-
if (_subscribersByName.TryGetValue(name, out var subscriber))
103+
if (_subscribersBySectionId.TryGetValue(sectionId, out var subscriber))
104104
{
105105
subscriber.ContentChanged(content);
106106
}

src/Components/Web/src/Head/HeadContent.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public sealed class HeadContent : ComponentBase
2121
protected override void BuildRenderTree(RenderTreeBuilder builder)
2222
{
2323
builder.OpenComponent<SectionContent>(0);
24-
builder.AddComponentParameter(1, nameof(SectionContent.Name), HeadOutlet.HeadSectionOutletName);
24+
builder.AddComponentParameter(1, nameof(SectionContent.SectionId), HeadOutlet.HeadSectionId);
2525
builder.AddComponentParameter(2, nameof(SectionContent.ChildContent), ChildContent);
2626
builder.CloseComponent();
2727
}

src/Components/Web/src/Head/HeadOutlet.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ public sealed class HeadOutlet : ComponentBase
1414
{
1515
private const string GetAndRemoveExistingTitle = "Blazor._internal.PageTitle.getAndRemoveExistingTitle";
1616

17-
internal const string HeadSectionOutletName = "head";
18-
internal const string TitleSectionOutletName = "title";
17+
internal static readonly object HeadSectionId = new();
18+
internal static readonly object TitleSectionId = new();
1919

2020
private string? _defaultTitle;
2121

@@ -37,22 +37,22 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
3737
{
3838
// Render the title content
3939
builder.OpenComponent<SectionOutlet>(0);
40-
builder.AddComponentParameter(1, nameof(SectionOutlet.Name), TitleSectionOutletName);
40+
builder.AddComponentParameter(1, nameof(SectionOutlet.SectionId), TitleSectionId);
4141
builder.CloseComponent();
4242

4343
// Render the default title if it exists
4444
if (!string.IsNullOrEmpty(_defaultTitle))
4545
{
4646
builder.OpenComponent<SectionContent>(2);
47-
builder.AddComponentParameter(3, nameof(SectionContent.Name), TitleSectionOutletName);
47+
builder.AddComponentParameter(3, nameof(SectionContent.SectionId), TitleSectionId);
4848
builder.AddComponentParameter(4, nameof(SectionContent.IsDefaultContent), true);
4949
builder.AddComponentParameter(5, nameof(SectionContent.ChildContent), (RenderFragment)BuildDefaultTitleRenderTree);
5050
builder.CloseComponent();
5151
}
5252

5353
// Render the rest of the head metadata
5454
builder.OpenComponent<SectionOutlet>(6);
55-
builder.AddComponentParameter(7, nameof(SectionOutlet.Name), HeadSectionOutletName);
55+
builder.AddComponentParameter(7, nameof(SectionOutlet.SectionId), HeadSectionId);
5656
builder.CloseComponent();
5757
}
5858

src/Components/Web/src/Head/PageTitle.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public sealed class PageTitle : ComponentBase
2121
protected override void BuildRenderTree(RenderTreeBuilder builder)
2222
{
2323
builder.OpenComponent<SectionContent>(0);
24-
builder.AddComponentParameter(1, nameof(SectionContent.Name), HeadOutlet.TitleSectionOutletName);
24+
builder.AddComponentParameter(1, nameof(SectionContent.SectionId), HeadOutlet.TitleSectionId);
2525
builder.AddComponentParameter(2, nameof(SectionContent.ChildContent), (RenderFragment)BuildTitleRenderTree);
2626
builder.CloseComponent();
2727
}

src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -819,15 +819,15 @@ await htmlRenderer.Dispatcher.InvokeAsync(async () =>
819819
// Arrange/Act/Assert 1: initially get some empty output
820820
var first = await htmlRenderer.RenderComponentAsync<SectionOutlet>(ParameterView.FromDictionary(new Dictionary<string, object>
821821
{
822-
{ nameof(SectionOutlet.Name), "testsection" }
822+
{ nameof(SectionOutlet.SectionId), "testsection" }
823823
}));
824824

825825
Assert.Empty(first.ToHtmlString());
826826

827827
// Act/Assert 2: cause it to be updated
828828
var second = await htmlRenderer.RenderComponentAsync<SectionContent>(ParameterView.FromDictionary(new Dictionary<string, object>
829829
{
830-
{ nameof(SectionContent.Name), "testsection" },
830+
{ nameof(SectionContent.SectionId), "testsection" },
831831
{ nameof(SectionContent.ChildContent), (RenderFragment)(builder =>
832832
{
833833
builder.AddContent(0, "Hello from the section content provider");

0 commit comments

Comments
 (0)