2024: Microsoft のチュートリアルの Blazor Todo リスト アプリを Fluent UI に変換

## テンプレートの FluentUI Blazor プロジェクトの新規作成
1. Visual Studio 2022を開いて
2. プロジェクトの新規作成を押下
![project_create.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/ddb0a302-c56c-e48b-e5e2-17b37d34583d.png)
3. Fluent Blazorのウエブアプリテンプレートを選択
4. 「次」を押下
![fluent_create_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/1847cf85-8303-465b-ce2c-fad7657e3b90.png)
5. プロジェクト名を入力
6. 「次」を押下
![ffb_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/efc54feb-70a0-b152-91f9-623a4364e915.png)
7. 「作成」を押下
![fluent_create_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/7d64dc7d-4cf3-f569-ba6c-6b7dcd47a26c.png)

## デフォルトのアプリの作り直し
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/a23dda4f-c0e7-8a6f-af6b-52db637778aa.png)

デフォルトのアプリは [Fluent UI](https://www.fluentui-blazor.net/) を使用していますが、アプリケーションの基本的のレイアウトを定義するために [Layout](https://www.fluentui-blazor.net/Layout) の代わりに [MainLayout](https://www.fluentui-blazor.net/MainLayout) を使用したいと考えました。
### MainLayout.razor
まず `Components/Layout/MainLayout.razor` の内容を書き換えましょう。
#### ビフォアー
```
@inherits LayoutComponentBase
<FluentLayout>
    <FluentHeader>
        FluentBlazorTodoList
    </FluentHeader>
    <FluentStack Class="main" Orientation="Orientation.Horizontal" Width="100%">
        <NavMenu />
        <FluentBodyContent>
            <div class="content">
                @Body
            </div>
        </FluentBodyContent>
    </FluentStack>
    <FluentFooter>
        <div class="link1">
            <a href="https://www.fluentui-blazor.net" target="_blank">Documentation and demos</a>
        </div>
        <div class="link2">
            <a href="https://learn.microsoft.com/en-us/aspnet/core/blazor" target="_blank">About Blazor</a>
        </div>
    </FluentFooter>
</FluentLayout>

<div id="blazor-error-ui">
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>
```
#### アフター
```
@inherits LayoutComponentBase

<FluentMainLayout>
    <Header>
        FluentBlazorTodoList
    </Header>
    <Body>
        @Body
    </Body>
    <NavMenuContent>
        <NavMenu />
    </NavMenuContent>
</FluentMainLayout>

<div id="blazor-error-ui">
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>
```
#### まとめ
1. [`FluentLayout`](https://www.fluentui-blazor.net/Layout) を [`FluentMainLayout`](https://www.fluentui-blazor.net/MainLayout) に置き換え
2. [`FluentHeader`](https://www.fluentui-blazor.net/Header) を `Header` に置き換え
3. 後で不要になるため、[`FluentFooter`](https://www.fluentui-blazor.net/Footer) を削除
4. 「main」のスタック（[`FluentStack`](https://www.fluentui-blazor.net/Stack)）を 2 つに分割:
    4.1. [`FluentBodyContent`](https://www.fluentui-blazor.net/BodyContent) タグとそのコンテンツを **@Body** ディレクティブを含む単純な `Body` タグに変換
    4.2. `NavMenu` コンポーネントを `NavMenuContent` タグに配置します
### NavMenu.razor
次に、変更に合わせて `Components/Layout/NavMenu.razor` の内容を書き換えましょう。
#### ビフォアー
```
@rendermode InteractiveServer

<div class="navmenu">
    <input type="checkbox" title="Menu expand/collapse toggle" id="navmenu-toggle" class="navmenu-icon" />
    <label for="navmenu-toggle" class="navmenu-icon"><FluentIcon Value="@(new Icons.Regular.Size20.Navigation())" Color="Color.Fill" /></label>
    <nav class="sitenav" aria-labelledby="main-menu" onclick="document.getElementById('navmenu-toggle').click();">
        <FluentNavMenu Id="main-menu" Collapsible="true" Width="250" Title="Navigation menu" @bind-Expanded="expanded">
            <FluentNavLink Href="/" Match="NavLinkMatch.All" Icon="@(new Icons.Regular.Size20.Home())" IconColor="Color.Accent">Home</FluentNavLink>
            <FluentNavLink Href="counter" Icon="@(new Icons.Regular.Size20.NumberSymbolSquare())" IconColor="Color.Accent">Counter</FluentNavLink>
            <FluentNavLink Href="weather" Icon="@(new Icons.Regular.Size20.WeatherPartlyCloudyDay())" IconColor="Color.Accent">Weather</FluentNavLink>
        </FluentNavMenu>
    </nav>
</div>

@code {
    private bool expanded = true;

}
```
#### アフター
```
<FluentNavMenu Id="main-menu" Collapsible="true" Width="250" Title="Navigation menu">
    <FluentNavLink Href="/" Match="NavLinkMatch.All" Icon="@(new Icons.Regular.Size20.Home())" IconColor="Color.Accent">Home</FluentNavLink>
    <FluentNavLink Href="counter" Icon="@(new Icons.Regular.Size20.NumberSymbolSquare())" IconColor="Color.Accent">Counter</FluentNavLink>
    <FluentNavLink Href="weather" Icon="@(new Icons.Regular.Size20.WeatherPartlyCloudyDay())" IconColor="Color.Accent">Weather</FluentNavLink>
</FluentNavMenu>
```
#### まとめ
1. サイド・メニューの折りたたみはメイン・レイアウトによって管理されるため、[`FluentNavMenu`](https://www.fluentui-blazor.net/NavMenu) タグとそのコンテンツだけ必要です。
2. 不要になった `Collapsible` 属性を削除。
3. `Width` 属性も不要で残すと `Body` の内容のレイアウトが崩れる。
### 結果
ご覧のとおり、マークアップは簡素化されたのに、書き換えの外観はデフォルトのアプリケーションの外観とほぼ同じです
![todo_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/459fd71b-3a80-08e8-930e-a00cb8c0af25.png)

## Todo リスト アプリを構築する
このアプリを、概要ページを備えた ToDo リスト アプリに変換しましょう。
### 不要機能の削除
まず、「Counter（カウンター）」と「Weather（天気）」のページを削除しましょう。
#### ファイル削除
1. `Components/Pages/Counter.razor`と`Components/Pages/Weather.razor`のファイルを選択
2. 右クリックすると表示されるコンテキストメニューで Delete を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/a2dfe9fe-6e06-9529-8ce4-69d65154b707.png)
#### リンク処理
`Components/Layout/NavMenu.razor`ファイルの中で：
1. 1番目の「Home（ホーム）」のリンクの名前を「Todo リスト」に変更
2. 1番目のリンクのアイコンをチェックされたチェックボックスに変更
    ```
    <FluentNavMenu Id="main-menu" Title="Navigation menu">
        <FluentNavLink Href="/" Match="NavLinkMatch.All" Icon="@(new Icons.Regular.Size20.CheckboxChecked())" IconColor="Color.Accent">Todo リスト</FluentNavLink>
        <FluentNavLink Href="weather" Icon="@(new Icons.Regular.Size20.WeatherPartlyCloudyDay())" IconColor="Color.Accent">Weather</FluentNavLink>
        <FluentNavLink Href="counter" Icon="@(new Icons.Regular.Size20.NumberSymbolSquare())" IconColor="Color.Accent">Counter</FluentNavLink>
    </FluentNavMenu>
    ```
3. 2番目と3番目のリンクを削除
    ```
    <FluentNavMenu Id="main-menu" Title="Navigation menu">
        <FluentNavLink Href="/" Match="NavLinkMatch.All" Icon="@(new Icons.Regular.Size20.CheckboxChecked())" IconColor="Color.Accent">Todo リスト</FluentNavLink>
    </FluentNavMenu>
    ```
4. 「About」ページへのリンクを追加する
    ```
    <FluentNavMenu Id="main-menu" Title="Navigation menu">
        <FluentNavLink Href="/" Match="NavLinkMatch.All" Icon="@(new Icons.Regular.Size20.CheckboxChecked())" IconColor="Color.Accent">Todo リスト</FluentNavLink>
        <FluentNavLink Href="about" Icon="@(new Icons.Regular.Size20.Info())" IconColor="Color.Accent">About</FluentNavLink>
    </FluentNavMenu>
    ```
### Todo リスト追加
1. `Components/Pages/Home.razor`を開いて
2. 相対 URL `/` とともに @page ディレクティブを追加。
3. そのまま静的にレンダリングされないように、ページの対話機能を有効にします。 対話型サーバー レンダリング モードにすると、コンポーネントでサーバーからの UI イベントを処理できます。
4. ページに HTML <title> 要素を追加することを可能にする PageTitle コンポーネントを使用してページ タイトルを追加します。
    ```
    @page "/"
    @rendermode InteractiveServer
    
    <PageTitle>Todo</PageTitle>
    
    <h3>Todo</h3>
    
    @code {
    
    }
    ```
5. プロジェクト（`FluentBlazorTodoList`）フォルダーを右クリックすると表示されるコンテキストメニューで Add ＞ New Folder を選択
![todo_9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/a0e8e71a-d620-6d07-e059-5580ed6b907d.png)
6. モデル クラスの保持に使用するため、新しいフォルダーを「Models」に名付け
![todo_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/c4d15e51-38e0-36b2-97a5-4166953c466a.png)
7. `Models`フォルダーを右クリックすると表示されるコンテキストメニューで Add ＞ New Item を選択
![todo_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/e3b8bc86-cb03-bf88-153c-1cc994046d7b.png)
8.  Todo アイテムを表すクラスを保持するために、プロジェクト `Models`フォルダーに `TodoItem.cs` ファイルを追加
![todo_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/7bffb93b-52a4-0899-a9f4-4f1473b1ad86.png)
9. 次の C# コードを挿入して `TodoItem` クラスを肉付け
    ```
    namespace Models;

    public class TodoItem
    {
        public string? Title { get; set; }
        public DateTime? DueDate { get; set; }
        public bool IsDone { get; set; }
    }
    ```
10. Todo コンポーネントに戻り
11. @namespace でモデルの `TodoItem` クラスを使用可能にする
12. Todo アイテム用のフィールドを @code ブロックに追加（Todo コンポーネントでは、このフィールドを使って ToDo リストの状態を維持）
13. リストを表す縦のスタック（[`FluentStack`](https://www.fluentui-blazor.net/Stack)）を追加
14. スタック（[`FluentStack`](https://www.fluentui-blazor.net/Stack)）のなか、各 Todo アイテムを横のスタック（[`FluentStack`](https://www.fluentui-blazor.net/Stack)）を含むカード（[`FluentCard`](https://www.fluentui-blazor.net/Card)）としてレンダリングするために、 **foreach** ループを追加
    ```
    @page "/"
    @rendermode InteractiveServer
    @namespace FluentBlazorTodoList.Models
    
    <PageTitle>Todo</PageTitle>
    
    <h3>Todo</h3>
    
    <FluentStack Orientation="Orientation.Vertical"
                 HorizontalAlignment="HorizontalAlignment.Center"
                 VerticalAlignment="VerticalAlignment.Top"
                 VerticalGap="4">
        @foreach (var todo in todos)
        {
        <FluentCard Height="64px">
                <FluentStack Orientation="Orientation.Horizontal"
                             HorizontalAlignment="HorizontalAlignment.Start"
                             VerticalAlignment="VerticalAlignment.Center"
                             HorizontalGap="4">
                    <FluentCheckbox @bind-Value="@todo.IsDone" Label="Is Done?" />
                    <FluentTextField @bind-Value="@todo.Title" Label="Title" Placeholder="title"></FluentTextField>
                    <FluentDatePicker Label="Due" AriaLabel="To" @bind-Value="@todo.DueDate" />
                </FluentStack>
         </FluentCard>
        }
    </FluentStack>

    @code {
        private List<TodoItem> todos = new();
    }
    ```
15. リストのスタック（[`FluentStack`](https://www.fluentui-blazor.net/Stack)）を別の縦のスタック（[`FluentStack`](https://www.fluentui-blazor.net/Stack)）で囲み、その下のリストに新しい Todo 項目を追加する UI 要素を追加
16. 追加 UI 要素をリストから視覚的に分離するため、 [`FluentDivider`](https://www.fluentui-blazor.net/Divider) を追加
17. 追加 UI のコントロールを含む横のスタック（[`FluentStack`](https://www.fluentui-blazor.net/Stack)）を追加
18. 新しい Todo 項目のタイトル用に [`FluentTextField`](https://www.fluentui-blazor.net/TextField) を追加
19. 新しい Todo 項目の期限用の [`FluentDatePicker`](https://www.fluentui-blazor.net/DateTime#fluentdatepicker-class) を追加
20. 追加アクションを実行するための「Add Todo」ボタン（[`FluentButton`](https://www.fluentui-blazor.net/Button)）を追加
    ```
    @page "/"
    @rendermode InteractiveServer
    @namespace FluentBlazorTodoList.Models
    
    <PageTitle>Todo</PageTitle>
    
    <h3>Todo</h3>
    
    <FluentStack Orientation="Orientation.Vertical"
                 HorizontalAlignment="HorizontalAlignment.Left"
                 VerticalAlignment="VerticalAlignment.Top"
                 VerticalGap="24">
        <FluentStack Orientation="Orientation.Vertical"
                     HorizontalAlignment="HorizontalAlignment.Center"
                     VerticalAlignment="VerticalAlignment.Top"
                     VerticalGap="4">
            @foreach (var todo in todos)
            {
            <FluentCard Height="64px">
                    <FluentStack Orientation="Orientation.Horizontal"
                                 HorizontalAlignment="HorizontalAlignment.Start"
                                 VerticalAlignment="VerticalAlignment.Center"
                                 HorizontalGap="4">
                        <FluentCheckbox @bind-Value="@todo.IsDone" Label="Is Done?" />
                        <FluentTextField @bind-Value="@todo.Title" Label="Title" Placeholder="title"></FluentTextField>
                        <FluentDatePicker Label="Due" AriaLabel="To" @bind-Value="@todo.DueDate" />
                    </FluentStack>
             </FluentCard>
            }
        </FluentStack>
        <FluentDivider Style="width: 100%;" Orientation=Orientation.Horizontal Role="DividerRole.Separator"></FluentDivider>
        <FluentStack Orientation="Orientation.Horizontal"
                        HorizontalAlignment="HorizontalAlignment.Start"
                        VerticalAlignment="VerticalAlignment.Center"
                        HorizontalGap="4">
    <FluentTextField Label="New Todo" Placeholder="Something todo" ></FluentTextField>
            <FluentDatePicker Label="Due" AriaLabel="To" />
    <FluentButton Appearance="Appearance.Accent" aria-label="add-a-todo-item-to-the-list" Autofocus="true" IconStart="@(new Icons.Regular.Size20.AddCircle())" >Add todo</FluentButton>
        </FluentStack>
    </FluentStack>
    
    @code {
        private List<Models.TodoItem> todos = new();
    }
    ```
現時点では、ボタンにイベント・ハンドラーがアタッチされていないため、「Add Todo」ボタン（[`FluentButton`](https://www.fluentui-blazor.net/Button)）を押しても何も起こりません。
21. Todo アイテムの追加アクションを処理するには、**@code** ブロックに AddTodo メソッドを追加

    @code {
        private List<Models.TodoItem> todos = new();
        private void AddTodo()
        {
            // // Todo: todo アイテムを追加
        }
    }
22 . 次に、@onclick 属性を使用して、AddTodo メソッドを「Add Todo」ボタンのクリック・イベント ハンドラーとして登録

    <FluentButton Appearance="Appearance.Accent" aria-label="add-a-todo-item-to-the-list" Autofocus="true" IconStart="@(new Icons.Regular.Size20.AddCircle())" OnClick="@AddTodo">Add todo</FluentButton>
23 . 新しい Todo アイテムのタイトルを取得するため、**@code** ブロックの先頭に `newTodoTitle` **String** フィールドを追加
24 . 新しい Todo アイテムの期限を取得するため、`newTodoDueDate` **DateTime** フィールドを **@code** ブロックの先頭に追加
25 . `AddTodo` メソッドを更新して、指定されたタイトルと期限を持つ `TodoItem` をリストに追加
26 . `newTodoTitle` を空の文字列に設定し、`newTodoDueDate` を **null** に設定して、  [`FluentTextField`](https://www.fluentui-blazor.net/TextField) と [`FluentDatePicker`](https://www.fluentui-blazor.net/DateTime#fluentdatepicker-class) の値をクリア

    @code {
        string? newTodoTitle;
        DateTime? newTodoDueDate;
        private List<Models.TodoItem> todos = new();
        private void AddTodo()
        {
            if (!string.IsNullOrWhiteSpace(newTodoTitle))
            {
                todos.Add(new TodoItem { Title = newTodoTitle, DueDate = newTodoDueDate });
                newTodoTitle = string.Empty;
                newTodoDueDate = null;
            }
        }
    }
27 . **@bind-Value** 属性を使用して、`newTodoTime` を [`FluentTextField`](https://www.fluentui-blazor.net/TextField) 要素にバインド
28 .   **@bind-Value** 属性を使用して、`newTodoDueDate` を [`FluentDatePicker`](https://www.fluentui-blazor.net/DateTime#fluentdatepicker-class) 要素にバインド

    <FluentTextField @bind-Value=newTodoTitle Label="New Todo" Placeholder="Something todo"></FluentTextField>
    <FluentDatePicker Label="Due" AriaLabel="To" @bind-Value=newTodoDueDate />
29 . Todo リストの前にカウンター・バッジ（[`FluentCounterBadge`](https://www.fluentui-blazor.net/CounterBadge)）を追加して、完了していない Todo 項目の数を表示する (IsDone は false)。 Blazor がコンポーネントを再レンダリングするたびにその Razor 式が評価される

        <FluentCounterBadge Count="@todos.Count(todo => !todo.IsDone)" Appearance="Appearance.Neutral">
            <FluentButton Appearance="Appearance.Outline">
                <FluentIcon Value="@(new Icons.Regular.Size24.CheckboxPerson())" Color="@Color.FillInverse" />
            </FluentButton>
        </FluentCounterBadge>
    
        <FluentDivider Style="width: 100%;" Orientation=Orientation.Horizontal Role="DividerRole.Separator"></FluentDivider>

#### 完成
    ```
    @page "/"
    @rendermode InteractiveServer
    @namespace FluentBlazorTodoList.Models
    
    <PageTitle>Todo</PageTitle>
    
    <h3>Todo</h3>

    <FluentStack Orientation="Orientation.Vertical"
                 HorizontalAlignment="HorizontalAlignment.Left"
                 VerticalAlignment="VerticalAlignment.Top"
                 VerticalGap="24">
        <FluentCounterBadge Count="@todos.Count(todo => !todo.IsDone)" Appearance="Appearance.Neutral">
            <FluentButton Appearance="Appearance.Outline">
                <FluentIcon Value="@(new Icons.Regular.Size24.CheckboxPerson())" Color="@Color.FillInverse" />
            </FluentButton>
        </FluentCounterBadge>
    
        <FluentDivider Style="width: 100%;" Orientation=Orientation.Horizontal Role="DividerRole.Separator"></FluentDivider>
    
        <FluentStack Orientation="Orientation.Vertical"
                     HorizontalAlignment="HorizontalAlignment.Center"
                     VerticalAlignment="VerticalAlignment.Top"
                     VerticalGap="4">
            @foreach (var todo in todos)
            {
            <FluentCard Height="64px">
                    <FluentStack Orientation="Orientation.Horizontal"
                                 HorizontalAlignment="HorizontalAlignment.Start"
                                 VerticalAlignment="VerticalAlignment.Center"
                                 HorizontalGap="4">
                        <FluentCheckbox @bind-Value="@todo.IsDone" Label="Is Done?" />
                        <FluentTextField @bind-Value="@todo.Title" Label="Title" Placeholder="title"></FluentTextField>
                        <FluentDatePicker Label="Due" AriaLabel="To" @bind-Value="@todo.DueDate" />
                    </FluentStack>
             </FluentCard>
            }
        </FluentStack>
        <FluentDivider Style="width: 100%;" Orientation=Orientation.Horizontal Role="DividerRole.Separator"></FluentDivider>
        <FluentStack Orientation="Orientation.Horizontal"
                        HorizontalAlignment="HorizontalAlignment.Start"
                        VerticalAlignment="VerticalAlignment.Center"
                        HorizontalGap="4">
            <FluentTextField @bind-Value=newTodoTitle Label="New Todo" Placeholder="Something todo"></FluentTextField>
            <FluentDatePicker Label="Due" AriaLabel="To" @bind-Value=newTodoDueDate />
    <FluentButton Appearance="Appearance.Accent" aria-label="add-a-todo-item-to-the-list" Autofocus="true" IconStart="@(new Icons.Regular.Size20.AddCircle())" OnClick="@AddTodo">Add todo</FluentButton>
        </FluentStack>
    </FluentStack>
    
    @code {
        string? newTodoTitle;
        DateTime? newTodoDueDate;
        private List<Models.TodoItem> todos = new();
        private void AddTodo()
        {
            if (!string.IsNullOrWhiteSpace(newTodoTitle))
            {
                todos.Add(new TodoItem { Title = newTodoTitle, DueDate = newTodoDueDate });
                newTodoTitle = string.Empty;
                newTodoDueDate = null;
            }
        }
    }
    ```
![todo_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/aeb59d40-1f13-fa84-dce1-02d553613291.png)

### About ページ追加
1. `Components/Pages`フォルダーを右クリックすると表示されるコンテキストメニューで Add ＞ New Component を選択
![todo_7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/74214f69-cc23-2a1e-000f-602fa03b0d31.png)
2. 「About.razor」を名付けて
3. 「追加」を押下
![todo_8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/e6ca61c5-c60b-0c37-1266-520b6501454f.png)
4. `Components/Pages/About.razor`を開いて
5. 次のコードを挿入して「About」のページを実装してください
    ```
    @page "/about"
    @rendermode InteractiveServer
    
    <PageTitle>About</PageTitle>
    
    <FluentBodyContent>
        <h3>About</h3>
    
        <FluentPersona Name="Meister 619"
                       Status="PresenceStatus.Available"
                       StatusSize="PresenceBadgeSize.Large"
                       StatusTitle="He is available"
                       Image="https://avatars.githubusercontent.com/u/3477572?v=4"
                       ImageSize="64px">
        </FluentPersona>
    
        <p> ... blah blah blah ...</p>
    </FluentBodyContent>
    @code {
    
    }
    ```
#### 完成
![todo_15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/002a3166-744f-c835-bcaa-4cd80f48e91c.png)

## 参考
* MicrosoftのLearn/.NET/ASP.NET Core/[Blazor Todo リスト アプリ](https://learn.microsoft.com/ja-jp/aspnet/core/blazor/tutorials/build-a-blazor-app?view=aspnetcore-8.0)のチュートリアル
* [Fluent UI Blazor コンポネント ライブラリー](https://www.fluentui-blazor.net/)
* [Fluent UI Blazor](https://github.com/microsoft/fluentui-blazor) の GitHub レポジトリー