.NET Blazor を始めてみた

# はじめに

自分はウェブアプリを開発するのに、Onsen UI＋Vue.js でアプリ開発してきましたが、.NET Blazor を使い始めました。

[.NET MAUI＋Blazor を始めてみた - Qiita](https://qiita.com/tinymouse/items/69ae9a2c7761a83c0529)

# .NET Blazor を始めてみた

## 「空の」プロジェクトで始める

Visual Studio で Blazor アプリ開発するプロジェクトを新規作成するとき、「空の」プロジェクトで始めたいものです。

参考：[.NET 7 で使えるようになった "空の" Blazor プロジェクトテンプレート vs. "最小の" テンプレート - Qiita](https://qiita.com/jsakamoto/items/679fbfa541b1236dd9b3) 

### Pages/_Host.cshtml または wwwroot/index.html

自分のアプリに必要のない CSS や JavaScript ライブラリがリンクされていれば削除します。

### App.razor

.NET MAUI＋Blazor のプロジェクトの場合は Main.razor になります。

```<Router>``` コンポーネントが配置されています。ページ遷移の機能を使うために残します。

### MainLayout.razor

```html:MainLayout.razor
@inherits LayoutComponentBase

<main>
    @Body
</main>
```

どのページに遷移してもアプリ全体で同じレイアウトにするときは、ここにコンポーネントを配置します。

### Pages/Index.razor

```html:Index.razor
@page "/"

<h1>Hello, world!</h1>
```

このファイルを編集します。

## コンポーネントを埋込する

参考：[BlazorでSPAするぞ！(2) - Component - ryuichi111stdの技術日記](https://ryuichi111std.hatenablog.com/entry/2019/05/01/224315)

コンポーネントの定義を新規作成します。拡張子 ```razor``` のファイルです。ファイル名がコンポーネント名になります。

```html:Hello.razor
<h1>Hello, world!</h1>
```

作成したコンポーネントをページに埋込してみます。HTML の独自のタグとして使えます。

```html:Index.razor
@page "/"

<Hello />
```

```Index.razor``` も実はコンポーネントの定義で、```MainLayout.razor``` で定義された ```@Body``` に埋込されています。

## コンポーネント間で状態を共有する①

コンポーネントは独自のクラスです。あるコンポーネントで定義された変数やメソッドは、他のコンポーネントで使用できません。
親のコンポーネントで定義された変数を、埋込された子のコンポーネントで参照して使用したいことがあります。

子のコンポーネントの定義で変数に ```[Parameter]``` を指定します。↓

```html:Child.razor
<p>Child</p>

<p>Count: @count<p>

@code {
    [Parameter]
    public int count { get; set; }
}
```

子コンポーネントでパラメータ指定した変数 ```count``` が、親コンポーネントに埋込した子コンポーネントのタグの属性 ```count``` として値を指定できます。↓

```html:Parent.razor
<p>count: @count</p>

<button @onclick="addCount">＋</button>

<Child count=@count />

@code {
    int count = 0;

    void addCount()
    {
        count += 1;
    }
}
```

親コンポーネントで値を変えると子コンポーネントの値も変わります。

## コンポーネント間で状態を共有する②

上記の例は、子のコンポーネントで値を変えても、親のコンポーネントの値は変わりません。
親コンポーネントで定義された変数を、子コンポーネントで参照して変更して、それを親コンポーネントで使用したいことがあります。

子コンポーネントで ```EventCallback``` オブジェクトを用意ます。親から受取した変数を変更したとき、このオブジェクトを ```Invoke()``` します。↓

```csharp:Child.razor
<p>Child</p>

<p>Count: @count</p>

<button @onclick="addCount">＋</button>

@code {
    [Parameter]
    public int count { get; set; }

    [Parameter]
    public EventCallback<int> CountChanged { get; set; }

    async void addCount()
    {
        count += 1;

        await CountChanged.InvokeAsync(count);
    }
}
```

親コンポーネントから子コンポーネントに値を受渡するのに ```@bind-変数名``` を使います。↓

```html:Parent.razor
<p>count: @count</p>

<button @onclick="addCount">＋</button>

<Child @bind-count=@count />

以下略
```

```@bind-``` を省略した書き方できるようです。↓

```html:Parent.razor
<Child count=@count />
```

子コンポーネントで値を変えると親コンポーネントの値も変わります。

## コンポーネント間で状態を共有する③

子コンポーネントで変更したことを親コンポーネントに伝える、他の方法があります。

子コンポーネントの ```EventCallback``` オブジェクトの名前に「On」をつけておきます（つけなくても構いません）。

```csharp:Child.razor
<p>Child</p>

<p>Count: @count</p>

<button @onclick="addCount">＋</button>

@code {
    [Parameter]
    public int count { get; set; }

    [Parameter]
    public EventCallback<int> OnCountChanged { get; set; }

    async void addCount()
    {
        count += 1;

        await OnCountChanged.InvokeAsync(count);
    }
}
```

親コンポーネントから子コンポーネントにパラメータで値を渡します。子コンポーネントで用意した ```EventCallback``` オブジェクト ```OnCountChanged``` に対して、親コンポーネントでメソッド ```CountChanged``` を用意して、紐づけるようにします。このメソッドは、子コンポーネントから値を引数で受取できます。↓

```csharp:Parent.razor
<p>count: @count</p>

<button @onclick="addCount">＋</button>

<Child count=@count OnCountChanged="CountChanged" />

@code {
    int count = 0;

    void addCount()
    {
        count += 1;
    }

    void CountChanged(int count)
    {
        this.count = count;
    }
}
```

## コンポーネント間で状態を共有する④

コンポーネント間で状態を共有する、別の方法です。

参考：[ASP.NET Core Blazor 状態管理 | Microsoft Learn](https://learn.microsoft.com/ja-jp/aspnet/core/blazor/state-management)

コンテナのクラスを用意して、```Program.cs``` に追記します。

```csharp:Container.cs
public class Container
{
    private int _count = 0;

    public int Count
    {
        get {
            return _count;
        }
        set {
            _count = value;
        }
    }
}
```
```csharp:Program.cs
    中略
    builder.Services.AddSingleton<Container>();

    await builder.Build().RunAsync();
```

コンポーネントで ```@inject``` します。

```html:Parent.razor
<p>Count: @container.Count</p>

<button @onclick="addCount">＋</button>

<Child />

@inject Container container

@code {
    void addCount()
    {
        container.Count += 1;
    }
}
```
```html:Child.razor
<p>Child</p>

<p>Count: @container.Count</p>

<button @onclick="addCount">＋</button>

@inject Container container

@code {
    void addCount()
    {
        container.Count += 1;
    }
}
````

```container``` がアプリ全体で参照できる大域変数のように使えます。
ただし、```container.count``` に代入しても、画面の表示が変わりません。↑

コンテナで ```Action``` オブジェクトを用意して、参照した変数を変更したとき、このオブジェクトを ```Invoke()``` します。↓

```csharp:Container.cs
public class Container
{
    中略
    public event Action? OnChange;
    中略
    public int Count
    {
        中略
        set
        {
            _count = value;

            OnChange?.Invoke();
        }
    }
```

コンテナを ```@inject``` しているコンポーネントは、自分の ```StateHasChanged``` メソッドをコンテナの ```Action``` オブジェクトにセットします。これで、コンテナの値が変更されたとき、コンポーネントの ```StateHasChanged``` メソッドが実行されて、画面が再描画されます。↓

```csharp:Parent.razor
中略
@implements IDisposable

@code {
    中略
    protected override void OnInitialized()
    {
        container.OnChange += this.StateHasChanged;
    }
    public void Dispose()
    {
        container.OnChange -= this.StateHasChanged;
    }
```

```csharp:Child.razor
中略
@implements IDisposable

@code {
    中略
    protected override void OnInitialized()
    {
        container.OnChange += this.StateHasChanged;
    }
    public void Dispose()
    {
        container.OnChange -= this.StateHasChanged;
    }
```

コンテナの ```OnChanged``` に ```StageHasChanged``` をセットするのはコンポーネントの ```OnInitialized``` で、逆に ```Dispose``` のタイミングで ```OnChanged``` から ```StageHasChanged``` を外さないといけません。↑

## ページ遷移する

参考：[BlazorでSPAするぞ！(6) - Routing - ryuichi111stdの技術日記](https://ryuichi111std.hatenablog.com/entry/2019/05/02/151624 
)

```App.razor``` で ```<Router>``` が初期化されています。```MainLayout.razor``` の ```@Body``` に埋込されるページを切替します。

ページの定義を新規作成します。```@page``` ディレクティブで URL を指定します。

```html:Next.razor
@page "/next"

<h1>Next Page</h1>
```

このページに遷移できるようにします。

```html:Index.razor
@page "/"

<a href="next">Next</a>
```
```html:
<NavLink href="next">Next</NavLink>
```
```html:
<button @onclick="GoToNext">Next</button>

@inject NavigationManager nm

@code {
	private void GoToNext()
	{
		nm.NavigateTo("next");
	}
}
```