Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blazor <head> modification using a section-based approach (preview7) #34518

Merged
merged 1 commit into from
Jul 20, 2021
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
4 changes: 3 additions & 1 deletion src/Components/Components.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"src\\Components\\test\\testassets\\TestServer\\Components.TestServer.csproj",
"src\\DataProtection\\Abstractions\\src\\Microsoft.AspNetCore.DataProtection.Abstractions.csproj",
"src\\DataProtection\\Cryptography.Internal\\src\\Microsoft.AspNetCore.Cryptography.Internal.csproj",
"src\\DataProtection\\Cryptography.KeyDerivation\\src\\Microsoft.AspNetCore.Cryptography.KeyDerivation.csproj",
"src\\DataProtection\\DataProtection\\src\\Microsoft.AspNetCore.DataProtection.csproj",
"src\\DefaultBuilder\\src\\Microsoft.AspNetCore.csproj",
"src\\Features\\JsonPatch\\src\\Microsoft.AspNetCore.JsonPatch.csproj",
Expand All @@ -72,9 +73,9 @@
"src\\Identity\\ApiAuthorization.IdentityServer\\src\\Microsoft.AspNetCore.ApiAuthorization.IdentityServer.csproj",
"src\\Identity\\Core\\src\\Microsoft.AspNetCore.Identity.csproj",
"src\\Identity\\EntityFrameworkCore\\src\\Microsoft.AspNetCore.Identity.EntityFrameworkCore.csproj",
"src\\Identity\\UI\\src\\Microsoft.AspNetCore.Identity.UI.csproj",
"src\\Identity\\Extensions.Core\\src\\Microsoft.Extensions.Identity.Core.csproj",
"src\\Identity\\Extensions.Stores\\src\\Microsoft.Extensions.Identity.Stores.csproj",
"src\\Identity\\UI\\src\\Microsoft.AspNetCore.Identity.UI.csproj",
"src\\JSInterop\\Microsoft.JSInterop\\src\\Microsoft.JSInterop.csproj",
"src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj",
"src\\Middleware\\Diagnostics.Abstractions\\src\\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj",
Expand Down Expand Up @@ -103,6 +104,7 @@
"src\\Razor\\Razor.Runtime\\src\\Microsoft.AspNetCore.Razor.Runtime.csproj",
"src\\Razor\\Razor\\src\\Microsoft.AspNetCore.Razor.csproj",
"src\\Security\\Authentication\\Cookies\\src\\Microsoft.AspNetCore.Authentication.Cookies.csproj",
"src\\Security\\Authentication\\Core\\src\\Microsoft.AspNetCore.Authentication.csproj",
"src\\Security\\Authorization\\Core\\src\\Microsoft.AspNetCore.Authorization.csproj",
"src\\Security\\Authorization\\Policy\\src\\Microsoft.AspNetCore.Authorization.Policy.csproj",
"src\\Security\\CookiePolicy\\src\\Microsoft.AspNetCore.CookiePolicy.csproj",
Expand Down
8 changes: 8 additions & 0 deletions src/Components/Components/src/Dispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Sections;

namespace Microsoft.AspNetCore.Components
{
Expand All @@ -13,6 +14,8 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public abstract class Dispatcher
{
private SectionRegistry? _sectionRegistry;

/// <summary>
/// Creates a default instance of <see cref="Dispatcher"/>.
/// </summary>
Expand All @@ -24,6 +27,11 @@ public abstract class Dispatcher
/// </summary>
internal event UnhandledExceptionEventHandler? UnhandledException;

/// <summary>
/// Gets the <see cref="Sections.SectionRegistry"/> associated with the dispatcher.
/// </summary>
internal SectionRegistry SectionRegistry => _sectionRegistry ??= new();

/// <summary>
/// Validates that the currently executing code is running inside the dispatcher.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions src/Components/Components/src/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Web, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Build.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Authorization.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Forms.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
Expand Down
3 changes: 3 additions & 0 deletions src/Components/Components/src/RenderHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ public Dispatcher Dispatcher
/// </summary>
public bool IsRenderingOnMetadataUpdate => TestableMetadataUpdate.IsSupported && (_renderer?.IsRenderingOnMetadataUpdate ?? false);

internal bool IsRendererDisposed => _renderer?.Disposed
?? throw new InvalidOperationException("No renderer has been initialized.");

/// <summary>
/// Notifies the renderer that the component should be rendered.
/// </summary>
Expand Down
14 changes: 9 additions & 5 deletions src/Components/Components/src/RenderTree/Renderer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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.

#nullable disable warnings
Expand Down Expand Up @@ -42,7 +42,6 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable
private bool _isBatchInProgress;
private ulong _lastEventHandlerId;
private List<Task>? _pendingTasks;
private bool _disposed;
private Task? _disposeTask;

/// <summary>
Expand Down Expand Up @@ -126,6 +125,11 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid
/// </summary>
internal bool IsRenderingOnMetadataUpdate { get; private set; }

/// <summary>
/// Gets whether the renderer has been disposed.
/// </summary>
internal bool Disposed { get; private set; }

private async void RenderRootComponentsOnHotReload()
{
// Before re-rendering the root component, also clear any well-known caches in the framework
Expand Down Expand Up @@ -562,7 +566,7 @@ private ComponentState GetOptionalComponentState(int componentId)
/// </summary>
protected virtual void ProcessPendingRender()
{
if (_disposed)
if (Disposed)
{
throw new ObjectDisposedException(nameof(Renderer), "Cannot process pending renders after the renderer has been disposed.");
}
Expand Down Expand Up @@ -954,7 +958,7 @@ private void HandleExceptionViaErrorBoundary(Exception error, ComponentState? er
/// <param name="disposing"><see langword="true"/> if this method is being invoked by <see cref="IDisposable.Dispose"/>, otherwise <see langword="false"/>.</param>
protected virtual void Dispose(bool disposing)
{
_disposed = true;
Disposed = true;

// It's important that we handle all exceptions here before reporting any of them.
// This way we can dispose all components before an error handler kicks in.
Expand Down Expand Up @@ -1059,7 +1063,7 @@ public async ValueTask DisposeAsync()
HotReloadManager.OnDeltaApplied -= RenderRootComponentsOnHotReload;
}

if (_disposed)
if (Disposed)
{
return;
}
Expand Down
10 changes: 10 additions & 0 deletions src/Components/Components/src/Sections/ISectionContentProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// 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.

namespace Microsoft.AspNetCore.Components.Sections
{
internal interface ISectionContentProvider
{
RenderFragment? Content { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// 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.

namespace Microsoft.AspNetCore.Components.Sections
{
internal interface ISectionContentSubscriber
{
void ContentChanged(RenderFragment? content);
}
}
66 changes: 66 additions & 0 deletions src/Components/Components/src/Sections/SectionContent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 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.

namespace Microsoft.AspNetCore.Components.Sections
{
/// <summary>
/// Provides content to <see cref="SectionOutlet"/> components with matching <see cref="Name"/>s.
/// </summary>
internal class SectionContent : ISectionContentProvider, IComponent, IDisposable
{
private string? _registeredName;
private SectionRegistry _registry = default!;

/// <summary>
/// Gets or sets the name that determines which <see cref="SectionOutlet"/> instances will render
/// the content of this instance.
/// </summary>
[Parameter] public string Name { get; set; } = default!;

/// <summary>
/// Gets or sets the content to be rendered in corresponding <see cref="SectionOutlet"/> instances.
/// </summary>
[Parameter] public RenderFragment? ChildContent { get; set; }

RenderFragment? ISectionContentProvider.Content => ChildContent;

void IComponent.Attach(RenderHandle renderHandle)
{
_registry = renderHandle.Dispatcher.SectionRegistry;
}

Task IComponent.SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);

if (string.IsNullOrEmpty(Name))
{
throw new InvalidOperationException($"{GetType()} requires a non-empty string parameter '{nameof(Name)}'.");
}

if (Name != _registeredName)
{
if (_registeredName is not null)
{
_registry.RemoveProvider(_registeredName, this);
}

_registry.AddProvider(Name, this);
_registeredName = Name;
}

_registry.NotifyContentChanged(Name, this);

return Task.CompletedTask;
}

/// <inheritdoc/>
public void Dispose()
{
if (_registeredName is not null)
{
_registry.RemoveProvider(_registeredName, this);
}
}
}
}
83 changes: 83 additions & 0 deletions src/Components/Components/src/Sections/SectionOutlet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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.

namespace Microsoft.AspNetCore.Components.Sections
{
/// <summary>
/// Renders content provided by <see cref="SectionContent"/> components with matching <see cref="Name"/>s.
/// </summary>
internal class SectionOutlet : ISectionContentSubscriber, IComponent, IDisposable
{
private static readonly RenderFragment _emptyRenderFragment = _ => { };

private string? _subscribedName;
private RenderHandle _renderHandle;
private SectionRegistry _registry = default!;
private RenderFragment? _content;

/// <summary>
/// Gets or sets the name that determines which <see cref="SectionContent"/> instances will provide
/// content to this instance.
/// </summary>
[Parameter] public string Name { get; set; } = default!;

void IComponent.Attach(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
_registry = _renderHandle.Dispatcher.SectionRegistry;
}

Task IComponent.SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);

if (string.IsNullOrEmpty(Name))
{
throw new InvalidOperationException($"{GetType()} requires a non-empty string parameter '{nameof(Name)}'.");
}

if (Name != _subscribedName)
{
if (_subscribedName is not null)
{
_registry.Unsubscribe(_subscribedName);
}

_registry.Subscribe(Name, this);
_subscribedName = Name;
}

RenderContent();

return Task.CompletedTask;
}

void ISectionContentSubscriber.ContentChanged(RenderFragment? content)
{
_content = content;
RenderContent();
}

private void RenderContent()
{
// Here, we guard against rendering after the renderer has been disposed.
// This can occur after prerendering or when the page is refreshed.
// In these cases, a no-op is preferred.
if (_renderHandle.IsRendererDisposed)
{
return;
}

_renderHandle.Render(_content ?? _emptyRenderFragment);
}

/// <inheritdoc/>
public void Dispose()
{
if (_subscribedName is not null)
{
_registry.Unsubscribe(_subscribedName);
}
}
}
}
Loading