Skip to content

Commit

Permalink
fix #948 [doc] PersistentComponentState while prerendering: There is …
Browse files Browse the repository at this point in the history
…already a persisted object under the same key 'ColorMode'
  • Loading branch information
hakenr committed Nov 22, 2024
1 parent 588de71 commit 47bfbb5
Show file tree
Hide file tree
Showing 15 changed files with 172 additions and 105 deletions.
6 changes: 4 additions & 2 deletions Havit.Blazor.Documentation.Server/App.razor
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<!doctype html>
<html lang="en" data-bs-theme="@_docColorModeResolver.GetColorMode().ToString("g").ToLowerInvariant()">
@using Havit.Blazor.Documentation.Shared.Components.DocColorMode
@inject IDocColorModeProvider DocColorModeProvider
<!doctype html>
<html lang="en" data-bs-theme="@DocColorModeProvider.GetColorMode().ToString("g").ToLowerInvariant()">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
Expand Down
8 changes: 0 additions & 8 deletions Havit.Blazor.Documentation.Server/App.razor.cs

This file was deleted.

27 changes: 0 additions & 27 deletions Havit.Blazor.Documentation.Server/DocColorModeServerResolver.cs

This file was deleted.

10 changes: 9 additions & 1 deletion Havit.Blazor.Documentation.Server/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
using Havit.Blazor.Documentation.Server.Services;
using Havit.Blazor.Documentation.Services;
using Havit.Blazor.Documentation.Shared.Components.DocColorMode;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using SmartComponents.Inference.OpenAI;
using SmartComponents.LocalEmbeddings;
Expand Down Expand Up @@ -44,7 +46,13 @@ public static void Main(string[] args)
builder.Services.AddTransient<IComponentApiDocModelBuilder, ComponentApiDocModelBuilder>();
builder.Services.AddSingleton<IDocXmlProvider, DocXmlProvider>();
builder.Services.AddSingleton<IDocPageNavigationItemsTracker, DocPageNavigationItemsTracker>();
builder.Services.AddTransient<IDocColorModeResolver, DocColorModeServerResolver>();

builder.Services.AddScoped<IDocColorModeProvider, DocColorModeProvider>();
builder.Services.AddCascadingValue<ColorMode>(services =>
{
var docColorModeStateProvider = services.GetRequiredService<IDocColorModeProvider>();
return new DocColorModeCascadingValueSource(docColorModeStateProvider);
});

builder.Services.AddTransient<IDemoDataService, DemoDataService>();

Expand Down
32 changes: 17 additions & 15 deletions Havit.Blazor.Documentation.Server/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,22 @@
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Havit.Blazor.Documentation.Server": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Havit.Blazor.Documentation.Server": {
"commandName": "Project",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public string GetCookieValue(string key)
{
return _httpContextAccessor.HttpContext.Request.Cookies[key];
}

public bool IsSupported() => true;
}
8 changes: 7 additions & 1 deletion Havit.Blazor.Documentation/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,15 @@ public static async Task Main(string[] args)
builder.Services.AddTransient<IComponentApiDocModelBuilder, ComponentApiDocModelBuilder>();
builder.Services.AddSingleton<IDocXmlProvider, DocXmlProvider>();
builder.Services.AddSingleton<IDocPageNavigationItemsTracker, DocPageNavigationItemsTracker>();
builder.Services.AddSingleton<IDocColorModeResolver, DocColorModeClientResolver>();
builder.Services.AddSingleton<IHttpContextProxy, WebAssemblyHttpContextProxy>();

builder.Services.AddScoped<IDocColorModeProvider, DocColorModeProvider>();
builder.Services.AddCascadingValue<ColorMode>(services =>
{
var docColorModeStateProvider = services.GetRequiredService<IDocColorModeProvider>();
return new DocColorModeCascadingValueSource(docColorModeStateProvider);
});

builder.Services.AddTransient<IDemoDataService, DemoDataService>();

await builder.Build().RunAsync();
Expand Down
1 change: 1 addition & 0 deletions Havit.Blazor.Documentation/Services/IHttpContextProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
/// </summary>
public interface IHttpContextProxy
{
bool IsSupported();
string GetCookieValue(string key);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

public class WebAssemblyHttpContextProxy : IHttpContextProxy
{
public bool IsSupported() => false;
public string GetCookieValue(string key) => throw new NotSupportedException();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace Havit.Blazor.Documentation.Shared.Components.DocColorMode;

// Reference implementation in AuthenticationStateCascadingValueSource
// https://github.com/dotnet/aspnetcore/blob/79d06db8a4be29165e24eb841054a337161bd09a/src/Components/Authorization/src/CascadingAuthenticationStateServiceCollectionExtensions.cs#L29-L56
public class DocColorModeCascadingValueSource : CascadingValueSource<ColorMode>, IDisposable
{
private readonly IDocColorModeProvider _docColorModeStateProvider;

public DocColorModeCascadingValueSource(IDocColorModeProvider docColorModeStateProvider)
: base(docColorModeStateProvider.GetColorMode, isFixed: false)
{
_docColorModeStateProvider = docColorModeStateProvider;
_docColorModeStateProvider.ColorModeChanged += HandleColorModeChanged;
}

private void HandleColorModeChanged(ColorMode colorMode)
{
// It's OK to discard the task because this only represents the duration of the dispatch to sync context.
// It handles any exceptions internally by dispatching them to the renderer within the context of whichever
// component threw when receiving the update. This is the same as how a CascadingValue doesn't get notified
// about exceptions that happen inside the recipients of value notifications.
_ = NotifyChangedAsync(colorMode);
}

public void Dispose()
{
_docColorModeStateProvider.ColorModeChanged -= HandleColorModeChanged;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using Havit.Blazor.Documentation.Services;

namespace Havit.Blazor.Documentation.Shared.Components.DocColorMode;

public class DocColorModeProvider : IDisposable, IDocColorModeProvider
{
private readonly IHttpContextProxy _httpContextProxy;
private readonly PersistentComponentState _persistentComponentState;
private PersistingComponentStateSubscription _persistingComponentStateSubscription;
private ColorMode? _colorMode;

public DocColorModeProvider(
IHttpContextProxy httpContextProxy,
PersistentComponentState persistentComponentState)
{
_httpContextProxy = httpContextProxy;
_persistentComponentState = persistentComponentState;
_persistingComponentStateSubscription = _persistentComponentState.RegisterOnPersisting(PersistMode);
}

public event ColorModeChangedHandler ColorModeChanged;

/// <summary>
/// Raises the <see cref="AuthenticationStateChanged"/> event.
/// </summary>
/// <param name="task">A <see cref="Task"/> that supplies the updated <see cref="AuthenticationState"/>.</param>
public void SetColorMode(ColorMode colorMode)
{
_colorMode = colorMode;
ColorModeChanged?.Invoke(colorMode);
}

public ColorMode GetColorMode()
{
if (_colorMode == null)
{
ResolveInitialColorMode();
}
return _colorMode.Value;
}

private void ResolveInitialColorMode()
{
// prerendering
if (_httpContextProxy.IsSupported()
&& _httpContextProxy.GetCookieValue("ColorMode") is string cookie
&& Enum.TryParse<ColorMode>(cookie, ignoreCase: true, out var mode))
{
_colorMode = mode;
}
else if (_persistentComponentState.TryTakeFromJson<ColorMode>("ColorMode", out var restored))
{
_colorMode = restored;
}
else
{
_colorMode = ColorMode.Auto;
}
}

private Task PersistMode()
{
_persistentComponentState.PersistAsJson("ColorMode", GetColorMode());
return Task.CompletedTask;
}

void IDisposable.Dispose()
{
_persistingComponentStateSubscription.Dispose();
}
}

/// <summary>
/// A handler for the <see cref="AuthenticationStateProvider.AuthenticationStateChanged"/> event.
/// </summary>
/// <param name="task">A <see cref="Task"/> that supplies the updated <see cref="AuthenticationState"/>.</param>
public delegate void ColorModeChangedHandler(ColorMode colorMode);

Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,20 @@ namespace Havit.Blazor.Documentation.Shared.Components.DocColorMode;
/// The client-side component uses JS to switch the color mode and save the new value to the cookie.
/// The auto mode is resolved by color-mode-auto.js startup script (see _Layout.cshtml).
/// </remarks>
public partial class DocColorModeSwitcher : IDisposable
public partial class DocColorModeSwitcher(
IDocColorModeProvider docColorModeProvider,
IJSRuntime jsRuntime)
{
[Inject] protected IDocColorModeResolver DocColorModeResolver { get; set; }
[Inject] protected PersistentComponentState PersistentComponentState { get; set; }
[Inject] protected IJSRuntime JSRuntime { get; set; }
[CascadingParameter] protected ColorMode ColorMode { get; set; }

private PersistingComponentStateSubscription _persistingSubscription;
private IJSObjectReference _jsModule;
private ColorMode _mode = ColorMode.Auto;

protected override void OnInitialized()
{
_persistingSubscription = PersistentComponentState.RegisterOnPersisting(PersistMode);
private readonly IDocColorModeProvider _docColorModeProvider = docColorModeProvider;
private readonly IJSRuntime _jsRuntime = jsRuntime;

if (PersistentComponentState.TryTakeFromJson<ColorMode>("ColorMode", out var restored))
{
_mode = restored;
}
else
{
_mode = DocColorModeResolver.GetColorMode();
}
}
private Task PersistMode()
{
PersistentComponentState.PersistAsJson("ColorMode", _mode);
return Task.CompletedTask;
}
private IJSObjectReference _jsModule;

private async Task HandleClick()
{
_mode = _mode switch
ColorMode = ColorMode switch
{
ColorMode.Auto => ColorMode.Dark,
ColorMode.Dark => ColorMode.Light,
Expand All @@ -52,38 +34,35 @@ private async Task HandleClick()
};

await EnsureJsModule();
await _jsModule.InvokeVoidAsync("setColorMode", _mode.ToString("g").ToLowerInvariant());
await _jsModule.InvokeVoidAsync("setColorMode", ColorMode.ToString("g").ToLowerInvariant());

_docColorModeProvider.SetColorMode(ColorMode);
}

private async Task EnsureJsModule()
{
_jsModule ??= await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./Shared/Components/DocColorMode/DocColorModeSwitcher.razor.js");
_jsModule ??= await _jsRuntime.InvokeAsync<IJSObjectReference>("import", "./Shared/Components/DocColorMode/DocColorModeSwitcher.razor.js");
}

private IconBase GetIcon()
{
return _mode switch
return ColorMode switch
{
ColorMode.Auto => BootstrapIcon.CircleHalf,
ColorMode.Light => BootstrapIcon.Sun,
ColorMode.Dark => BootstrapIcon.Moon,
_ => throw new InvalidOperationException($"Unknown color mode '{_mode}'.")
_ => throw new InvalidOperationException($"Unknown color mode '{ColorMode}'.")
};
}

private string GetTooltip()
{
return _mode switch
return ColorMode switch
{
ColorMode.Auto => "Auto color mode (theme). Click to switch to Dark.",
ColorMode.Dark => "Dark color mode (theme). Click to switch to Light.",
ColorMode.Light => "Light color mode (theme). Click to switch to Auto.",
_ => "Click to switch color mode (theme) to Auto." // fallback
};
}

void IDisposable.Dispose()
{
_persistingSubscription.Dispose();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Havit.Blazor.Documentation.Shared.Components.DocColorMode;

public interface IDocColorModeProvider
{
event ColorModeChangedHandler ColorModeChanged;

ColorMode GetColorMode();
void SetColorMode(ColorMode colorMode);
}

This file was deleted.

0 comments on commit 47bfbb5

Please sign in to comment.