Skip to content

Commit 90d11d3

Browse files
authored
feat(BootstrapBlazorOutlet): add BootstrapBlazorOutlet component (#5482)
* feat: 增加 RootOutlet/Content 组件 * feat: 增加 RootOutlet 组件 * refactor: Outlet/Content 组件 * feat: 增加 BootstrapBlazorRootRegisterService 服务 * test: 增加单元测试 * test: 增加单元测试
1 parent c543845 commit 90d11d3

File tree

7 files changed

+530
-1
lines changed

7 files changed

+530
-1
lines changed

src/BootstrapBlazor/Components/BaseComponents/BootstrapBlazorRoot.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<Download></Download>
1515
<Mask></Mask>
1616
<ConnectionHub></ConnectionHub>
17-
17+
<BootstrapBlazorRootOutlet></BootstrapBlazorRootOutlet>
1818
@foreach (var com in Generators)
1919
{
2020
@com.Generator()
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License
3+
// See the LICENSE file in the project root for more information.
4+
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone
5+
6+
namespace BootstrapBlazor.Components;
7+
8+
/// <summary>
9+
/// BootstrapBlazorRootContent Component
10+
/// </summary>
11+
public class BootstrapBlazorRootContent : IComponent, IDisposable
12+
{
13+
private object? _registeredIdentifier;
14+
15+
/// <summary>
16+
/// Gets or sets the <see cref="string"/> ID that determines which <see cref="BootstrapBlazorRootOutlet"/> instance will render
17+
/// the content of this instance.
18+
/// </summary>
19+
[Parameter] public string? RootName { get; set; }
20+
21+
/// <summary>
22+
/// Gets or sets the <see cref="object"/> ID that determines which <see cref="BootstrapBlazorRootOutlet"/> instance will render
23+
/// the content of this instance.
24+
/// </summary>
25+
[Parameter] public object? RootId { get; set; }
26+
27+
/// <summary>
28+
/// Gets or sets the content.
29+
/// </summary>
30+
[Parameter]
31+
public RenderFragment? ChildContent { get; set; }
32+
33+
[Inject]
34+
private BootstrapBlazorRootRegisterService RootRegisterService { get; set; } = default!;
35+
36+
/// <summary>
37+
/// <inheritdoc/>
38+
/// </summary>
39+
/// <param name="renderHandle"></param>
40+
void IComponent.Attach(RenderHandle renderHandle)
41+
{
42+
43+
}
44+
45+
/// <summary>
46+
/// <inheritdoc/>
47+
/// </summary>
48+
/// <param name="parameters"></param>
49+
/// <returns></returns>
50+
Task IComponent.SetParametersAsync(ParameterView parameters)
51+
{
52+
parameters.SetParameterProperties(this);
53+
54+
object? identifier = null;
55+
56+
if (RootName is not null && RootId is not null)
57+
{
58+
throw new InvalidOperationException($"{nameof(BootstrapBlazorRootContent)} requires that '{nameof(RootName)}' and '{nameof(RootId)}' cannot both have non-null values.");
59+
}
60+
else if (RootName is not null)
61+
{
62+
identifier = RootName;
63+
}
64+
else if (RootId is not null)
65+
{
66+
identifier = RootId;
67+
}
68+
identifier ??= BootstrapBlazorRootOutlet.DefaultIdentifier;
69+
70+
if (!Equals(identifier, _registeredIdentifier))
71+
{
72+
if (_registeredIdentifier is not null)
73+
{
74+
RootRegisterService.RemoveProvider(_registeredIdentifier, this);
75+
}
76+
77+
RootRegisterService.AddProvider(identifier, this);
78+
_registeredIdentifier = identifier;
79+
}
80+
81+
RootRegisterService.NotifyContentProviderChanged(identifier, this);
82+
return Task.CompletedTask;
83+
}
84+
85+
/// <summary>
86+
/// <inheritdoc/>
87+
/// </summary>
88+
public void Dispose()
89+
{
90+
if (_registeredIdentifier is not null)
91+
{
92+
RootRegisterService.RemoveProvider(_registeredIdentifier, this);
93+
}
94+
GC.SuppressFinalize(this);
95+
}
96+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License
3+
// See the LICENSE file in the project root for more information.
4+
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone
5+
6+
using Microsoft.AspNetCore.Components.Rendering;
7+
8+
namespace BootstrapBlazor.Components;
9+
10+
/// <summary>
11+
/// BootstrapBlazorRootOutlet Component
12+
/// </summary>
13+
public class BootstrapBlazorRootOutlet : IComponent, IDisposable
14+
{
15+
private static readonly RenderFragment _emptyRenderFragment = _ => { };
16+
private object? _subscribedIdentifier;
17+
private RenderHandle _renderHandle;
18+
19+
/// <summary>
20+
/// Gets the default identifier that can be used to subscribe to all <see cref="BootstrapBlazorRootContent"/> instances.
21+
/// </summary>
22+
public static readonly object DefaultIdentifier = new();
23+
24+
[Inject]
25+
private BootstrapBlazorRootRegisterService RootRegisterService { get; set; } = default!;
26+
27+
/// <summary>
28+
/// Gets or sets the <see cref="string"/> ID that determines which <see cref="BootstrapBlazorRootContent"/> instances will provide
29+
/// content to this instance.
30+
/// </summary>
31+
[Parameter]
32+
public string? RootName { get; set; }
33+
34+
/// <summary>
35+
/// Gets or sets the <see cref="object"/> ID that determines which <see cref="BootstrapBlazorRootContent"/> instances will provide
36+
/// content to this instance.
37+
/// </summary>
38+
[Parameter]
39+
public object? RootId { get; set; }
40+
41+
void IComponent.Attach(RenderHandle renderHandle)
42+
{
43+
_renderHandle = renderHandle;
44+
}
45+
46+
/// <summary>
47+
/// <inheritdoc/>
48+
/// </summary>
49+
/// <param name="parameters"></param>
50+
/// <returns></returns>
51+
Task IComponent.SetParametersAsync(ParameterView parameters)
52+
{
53+
parameters.SetParameterProperties(this);
54+
55+
object? identifier = null;
56+
57+
if (RootName is not null && RootId is not null)
58+
{
59+
throw new InvalidOperationException($"{nameof(BootstrapBlazorRootOutlet)} requires that '{nameof(RootName)}' and '{nameof(RootId)}' cannot both have non-null values.");
60+
}
61+
else if (RootName is not null)
62+
{
63+
identifier = RootName;
64+
}
65+
else if (RootId is not null)
66+
{
67+
identifier = RootId;
68+
}
69+
identifier ??= DefaultIdentifier;
70+
71+
if (!Equals(identifier, _subscribedIdentifier))
72+
{
73+
if (_subscribedIdentifier is not null)
74+
{
75+
RootRegisterService.Unsubscribe(_subscribedIdentifier);
76+
}
77+
78+
RootRegisterService.Subscribe(identifier, this);
79+
_subscribedIdentifier = identifier;
80+
}
81+
82+
RenderContent();
83+
return Task.CompletedTask;
84+
}
85+
86+
internal void ContentUpdated(BootstrapBlazorRootContent? provider)
87+
{
88+
RenderContent();
89+
}
90+
91+
private void RenderContent()
92+
{
93+
_renderHandle.Render(BuildRenderTree);
94+
}
95+
96+
/// <summary>
97+
/// <inheritdoc/>
98+
/// </summary>
99+
/// <param name="builder"></param>
100+
private void BuildRenderTree(RenderTreeBuilder builder)
101+
{
102+
if (_subscribedIdentifier is not null)
103+
{
104+
foreach (var content in RootRegisterService.GetProviders(_subscribedIdentifier))
105+
{
106+
builder.OpenComponent<BootstrapBlazorRootOutletContentRenderer>(0);
107+
builder.SetKey(content);
108+
builder.AddAttribute(1, BootstrapBlazorRootOutletContentRenderer.ContentParameterName, content.ChildContent ?? _emptyRenderFragment);
109+
builder.CloseComponent();
110+
}
111+
}
112+
}
113+
114+
/// <summary>
115+
/// <inheritdoc/>
116+
/// </summary>
117+
public void Dispose()
118+
{
119+
if (_subscribedIdentifier is not null)
120+
{
121+
RootRegisterService.Unsubscribe(_subscribedIdentifier);
122+
}
123+
GC.SuppressFinalize(this);
124+
}
125+
126+
internal sealed class BootstrapBlazorRootOutletContentRenderer : 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+
}
144+
}

src/BootstrapBlazor/Extensions/BootstrapBlazorServiceCollectionExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ public static IServiceCollection AddBootstrapBlazor(this IServiceCollection serv
3636
services.TryAddSingleton<IZipArchiveService, DefaultZipArchiveService>();
3737
services.TryAddSingleton(typeof(IDispatchService<>), typeof(DefaultDispatchService<>));
3838

39+
// BootstrapBlazorRootRegisterService 服务
40+
services.AddScoped<BootstrapBlazorRootRegisterService>();
41+
3942
// Html2Pdf 服务
4043
services.TryAddSingleton<IHtml2Pdf, DefaultHtml2PdfService>();
4144

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License
3+
// See the LICENSE file in the project root for more information.
4+
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone
5+
6+
namespace BootstrapBlazor.Components;
7+
8+
/// <summary>
9+
/// BootstrapBlazorRootRegisterService
10+
/// </summary>
11+
public class BootstrapBlazorRootRegisterService
12+
{
13+
private readonly Dictionary<object, BootstrapBlazorRootOutlet> _subscribersByIdentifier = [];
14+
private readonly Dictionary<object, List<BootstrapBlazorRootContent>> _providersByIdentifier = [];
15+
16+
/// <summary>
17+
/// add provider
18+
/// </summary>
19+
/// <param name="identifier"></param>
20+
/// <param name="provider"></param>
21+
public void AddProvider(object identifier, BootstrapBlazorRootContent provider)
22+
{
23+
if (!_providersByIdentifier.TryGetValue(identifier, out var providers))
24+
{
25+
providers = [];
26+
_providersByIdentifier.Add(identifier, providers);
27+
}
28+
29+
providers.Add(provider);
30+
}
31+
32+
/// <summary>
33+
/// remove provider
34+
/// </summary>
35+
/// <param name="identifier"></param>
36+
/// <param name="provider"></param>
37+
public void RemoveProvider(object identifier, BootstrapBlazorRootContent provider)
38+
{
39+
if (!_providersByIdentifier.TryGetValue(identifier, out var providers))
40+
{
41+
throw new InvalidOperationException($"There are no content providers with the given root ID '{identifier}'.");
42+
}
43+
44+
var index = providers.LastIndexOf(provider);
45+
if (index < 0)
46+
{
47+
throw new InvalidOperationException($"The provider was not found in the providers list of the given root ID '{identifier}'.");
48+
}
49+
50+
providers.RemoveAt(index);
51+
if (index == providers.Count)
52+
{
53+
// We just removed the most recently added provider, meaning we need to change
54+
// the current content to that of second most recently added provider.
55+
var contentProvider = GetCurrentProviderContentOrDefault(providers);
56+
NotifyContentChangedForSubscriber(identifier, contentProvider);
57+
}
58+
}
59+
60+
/// <summary>
61+
/// get all providers by identifier
62+
/// </summary>
63+
/// <param name="identifier"></param>
64+
/// <returns></returns>
65+
public List<BootstrapBlazorRootContent> GetProviders(object identifier)
66+
{
67+
_providersByIdentifier.TryGetValue(identifier, out var providers);
68+
return providers ?? [];
69+
}
70+
71+
/// <summary>
72+
/// subscribe
73+
/// </summary>
74+
/// <param name="identifier"></param>
75+
/// <param name="subscriber"></param>
76+
public void Subscribe(object identifier, BootstrapBlazorRootOutlet subscriber)
77+
{
78+
if (_subscribersByIdentifier.ContainsKey(identifier))
79+
{
80+
throw new InvalidOperationException($"There is already a subscriber to the content with the given root ID '{identifier}'.");
81+
}
82+
83+
_subscribersByIdentifier.Add(identifier, subscriber);
84+
}
85+
86+
/// <summary>
87+
/// 取消订阅
88+
/// </summary>
89+
/// <param name="identifier"></param>
90+
public void Unsubscribe(object identifier)
91+
{
92+
if (!_subscribersByIdentifier.Remove(identifier))
93+
{
94+
throw new InvalidOperationException($"The subscriber with the given root ID '{identifier}' is already unsubscribed.");
95+
}
96+
}
97+
98+
/// <summary>
99+
/// Notify content provider changed
100+
/// </summary>
101+
/// <param name="identifier"></param>
102+
/// <param name="provider"></param>
103+
public void NotifyContentProviderChanged(object identifier, BootstrapBlazorRootContent provider)
104+
{
105+
if (!_providersByIdentifier.TryGetValue(identifier, out var providers))
106+
{
107+
throw new InvalidOperationException($"There are no content providers with the given root ID '{identifier}'.");
108+
}
109+
110+
// We only notify content changed for subscribers when the content of the
111+
// most recently added provider changes.
112+
if (providers.Count != 0 && providers[^1] == provider)
113+
{
114+
NotifyContentChangedForSubscriber(identifier, provider);
115+
}
116+
}
117+
118+
private static BootstrapBlazorRootContent? GetCurrentProviderContentOrDefault(List<BootstrapBlazorRootContent> providers)
119+
=> providers.Count != 0
120+
? providers[^1]
121+
: null;
122+
123+
private void NotifyContentChangedForSubscriber(object identifier, BootstrapBlazorRootContent? provider)
124+
{
125+
if (_subscribersByIdentifier.TryGetValue(identifier, out var subscriber))
126+
{
127+
subscriber.ContentUpdated(provider);
128+
}
129+
}
130+
}

0 commit comments

Comments
 (0)