BlazorでLight／Dark Mode切替＋ローカルストレージにモードを保存

## １．この記事の目的
- BlazorでLight／Dark Mode切替です
- LocalStorageにモードを保管し、ブラウザを閉じても状態を維持します

::: note
LocalStorageはブラウザーを再起動しても保持されます
:::
https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.aspnetcore.components.server.protectedbrowserstorage.protectedlocalstorage?view=aspnetcore-8.0

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/e1cfc641-f4f3-7c5a-b196-81a5fc0a48e3.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/549257f2-6b6e-dc03-9a6e-898241333cea.png)



## ２．前提条件
- Visual studio 2022 Version 17.12.2
- .Net 8
- UI：Blazor Web App
- Blazor rendermode： InteractiveServerRenderMode


## ３．ソリューションのソース
https://github.com/masayahak/BlazorTheme




## ４．構成



### ４．１ CustomBasePage.cs
- 全てのrazorページの継承元
- OnAfterRenderAsyncで現在のテーマを取得し反映

```C#
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を継承する例
```html
@inherits CustomBasePage
@page "/"

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.
```




### ４．２ ToggleTheme.razor
- テーマの切り替えボタン
```html
@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();
    }
}
```


### ４．３ ThemeService.cs
- テーマ取得、変更の実行サービス
- 現在のテーマをjavascriptを呼び出しページへ反映

```C#
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);
        }
    }
}
```

### ４．４ site.js
- javascriptでテーマを反映
```js
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");
        }
    });
};
```




### ４．５ app.css
- テーマ色の指定など
```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);
}
```

### ４．６ MainLayout.razor.css
- サイドバーとトップローにテーマ色を反映
```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;
}
```

## ５．動作確認
ローカルストレージへ暗号化されたテーマが記録されていることが分かります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/14a2bea4-3d6e-7a15-3f16-5545fd762cbb.png)

ローカルストレージを削除してからGETし直すとデフォルト（ライトモード）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/a130abf2-908a-b9ca-c4d6-add6f80abfd8.png)
  
もう一度モードを変更するとローカルストレージにキーが追加されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/62427465-4780-f6b4-526e-b283e1bdec42.png)