1.この記事の目的
- BlazorでLight/Dark Mode切替です
- LocalStorageにモードを保管し、ブラウザを閉じても状態を維持します
LocalStorageはブラウザーを再起動しても保持されます
2.前提条件
- Visual studio 2022 Version 17.12.2
- .Net 8
- UI:Blazor Web App
- Blazor rendermode: InteractiveServerRenderMode
3.ソリューションのソース
4.構成
4.1 CustomBasePage.cs
- 全てのrazorページの継承元
- OnAfterRenderAsyncで現在のテーマを取得し反映
using BlazorTheme.Components.Services;
using Microsoft.AspNetCore.Components;
namespace BlazorTheme.Components
{
public class CustomBasePage : ComponentBase
{
[Inject]
protected ThemeService ThemeService { get; set; } = default!;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// StreamRenderingの場合、OnAfterRenderは複数回実行される。
// 初回だけ実行するようにしてしまうと、<table>タグが生成されるまえにだけ
// Applyする可能性があるので、冗長だがAfterRenderのたびに実行する。
await ThemeService.GetCurrentTheme();
await ThemeService.ApplyCurrentTheme();
}
}
}
Home.razorがCustomBasePageを継承する例
@inherits CustomBasePage
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
4.2 ToggleTheme.razor
- テーマの切り替えボタン
@inject ThemeService ThemeService
<button @onclick="ToggleThemeOnClick" class="theme-toggle @ThemeService.CurrentTheme">
<span class="theme-icon light">☀️</span>
<span class="theme-icon dark">🌙</span>
</button>
@code {
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await ThemeService.GetCurrentTheme();
await ThemeService.ApplyCurrentTheme();
}
private async Task ToggleThemeOnClick()
{
await ThemeService.ToggleTheme();
await ThemeService.ApplyCurrentTheme();
}
}
4.3 ThemeService.cs
- テーマ取得、変更の実行サービス
- 現在のテーマをjavascriptを呼び出しページへ反映
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using Microsoft.JSInterop;
using System.Security.Cryptography;
namespace BlazorTheme.Components.Services
{
public class ThemeService
{
private readonly ProtectedLocalStorage _localStorage;
private readonly IJSRuntime _jsRuntime;
private const string ThemeKey = "theme";
private const string DefaultTheme = "light";
public string CurrentTheme { get; private set; } = DefaultTheme;
public ThemeService(ProtectedLocalStorage localStorage, IJSRuntime jsRuntime)
{
_localStorage = localStorage;
_jsRuntime = jsRuntime;
}
public async Task<string> GetCurrentTheme()
{
try
{
var result = await _localStorage.GetAsync<string>(ThemeKey);
CurrentTheme = result.Success ? result.Value! : DefaultTheme;
}
catch (CryptographicException)
{
CurrentTheme = "light";
}
return CurrentTheme;
}
public async Task<string> ToggleTheme()
{
CurrentTheme = CurrentTheme == DefaultTheme ? "dark" : DefaultTheme;
await _localStorage.SetAsync(ThemeKey, CurrentTheme);
return CurrentTheme;
}
public async Task ApplyCurrentTheme()
{
await _jsRuntime.InvokeVoidAsync("setTheme", CurrentTheme);
}
}
}
4.4 site.js
- javascriptでテーマを反映
window.setTheme = (theme) => {
document.documentElement.setAttribute("data-theme", theme);
const tables = document.querySelectorAll(".table");
tables.forEach((table) => {
if (theme === "dark") {
table.classList.add("table-dark");
} else {
table.classList.remove("table-dark");
}
});
};
4.5 app.css
- テーマ色の指定など
/* Light Mode */
:root {
--background-color: #ffffff;
--text-color: #000000;
--top-color: #f7f7f7;
--theme-color1: rgb(5, 39, 103);
--theme-color2: #3a0647;
}
/* Dark Mode */
:root[data-theme='dark'] {
--background-color: #000000;
--text-color: #4cff00;
--top-color: #1a1a1a;
--theme-color1: #1a1a1a;
--theme-color2: #3b2e2e;
}
/* Mode */
body {
background-color: var(--background-color);
color: var(--text-color);
}
4.6 MainLayout.razor.css
- サイドバーとトップローにテーマ色を反映
.sidebar {
background-image: linear-gradient(180deg, var(--theme-color1) 0%, var(--theme-color2) 70%);
}
.top-row {
background-color: var(--top-color);
color: var(--text-color);
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
5.動作確認
ローカルストレージへ暗号化されたテーマが記録されていることが分かります