Blazor Server-Side アプリで作る 簡単チャットアプリ

Blazor Server-SideアプリケーションでBlazorだけで、チャットアプリを作ります。
驚くほど簡単にできます:open_mouth:

![jytc9-9qsu4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110304/05b01ed0-027f-b55c-9cae-6364f2db111a.gif)


### 開発環境
Windows10
Visual Studio 2019
.Net Core 3.0

# メッセージ管理クラスの作成

`Sharedフォルダ`に、`List<string>型`のチャット情報を保持するだけの`ChatClass`を作ります。

```c#:Shared/ChatClass.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace BlazorApp30.Shared
{
    public class ChatClass
    {
        /// <summary>
        /// チャットメッセージを保持する
        /// </summary>
        private List<string> _messages { get; set; } = new List<string>();

        /// <summary>
        /// 任意のイベントを実行するイベントハンドラ
        /// </summary>
        public event EventHandler StateChanged;

        /// <summary>
        /// チャットメッセージを取得する
        /// </summary>
        /// <returns></returns>
        public List<string> GetMessages()
        {
            return _messages;
        }

        /// <summary>
        /// チャットメッセージを追加する
        /// </summary>
        /// <param name="str"></param>
        public void AddMessage(string str)
        {
            _messages.Add(str);

            // チャットメッセージが追加されたら、イベントを実行する
            this.StateHasChanged();
        }

        /// <summary>
        /// チャットメッセージをクリアする
        /// </summary>
        public void ClearMessages()
        {
            _messages.Clear();

            // チャットメッセージがクリアされたら、イベントを実行する
            this.StateHasChanged();
        }

        /// <summary>
        /// イベントを実行する
        /// </summary>
        private void StateHasChanged()
        {
            this.StateChanged?.Invoke(this, EventArgs.Empty);
        }
    }
}
```
`ChatClass`は、チャットメッセージを保持するクラスです。メッセージの取得と追加、クリアには、それぞれ関数を用意し、更に`EventHandler `を用意してあげることにより、使用する側でチャットメッセージが追加された事を契機に、任意のイベントを実行できるようにしておきます。

# ChatClassをサービスに登録する

ASP.Net Coreでは、アプリケーション起動時に、任意のサービスのインスタンスを生成してくれる仕組みがあります。チャットアプリケーションでは、参加するユーザーに同期的にメッセージを表示させますので、`ChatClass`を**シングルトン**で起動します。Blazor Server-Sideだからできることですが、これで `ChatClass`は、アプリケーション全体で唯一無二の存在になりました。

```C#:Startup.cs
        using BlazorApp30.Shared;
        ～
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddServerSideBlazor();
            services.AddSingleton<WeatherForecastService>();

            // ChatClassをシングルトンで生成する
            services.AddSingleton<ChatClass>();
        }
```

# クライアント画面
今回は、`Pages/Index.razor`に入力ページ、`Pages/FetchData.razor`に履歴ページを作ります。
`ChatClass`インスタンスを複数の画面で使用する場合、それぞれの画面で `inject`で宣言するよりも、`MainLayout.razorにだけ`記述し、`CascadingParameter`でインスタンスを渡す方が効率的です。

```javascript:Shared/MainLayout.razor
@inherits LayoutComponentBase
@inject BlazorApp30.Shared.ChatClass chatClass
@implements IDisposable

<div class="sidebar">
    <NavMenu />
</div>

@*カスケードパラメータで子にインスタンスを渡す*@
<CascadingValue Value="@chatClass" Name="Chat">
    <div class="main">
        <div class="top-row px-4">
            <a href="https://docs.microsoft.com/en-us/aspnet/" target="_blank">About</a>
        </div>

        <div class="content px-4">
            @Body
        </div>
    </div>
</CascadingValue>

@code{
    protected override void OnInitialized()
    {
        // 初回時にイベントハンドラに、イベントの紐づけ
        chatClass.StateChanged += OnChatStateChanged;
    }

    void OnChatStateChanged(
        object sender, EventArgs e)
    {
        this.InvokeAsync(StateHasChanged);
    }

    void IDisposable.Dispose()
    {
        // 閉じられるとき、イベントのリリース
        chatClass.StateChanged -= OnChatStateChanged;
    }
}
```

少しイメージしにくいのですが、**CascadingValueタグ で囲う ことにより、その配下の`ページ(子)`は、`MainLayout(親)`と同じインスタンスを使えるようになります。** つまり、親でやったことは、子でやる必要はなくなります。

`MainLayout`内では、`ChatClass`の`StateChangedイベントハンドラ`に、Componentクラスの `StateHasChangedメソッド`を実行するようにセットしているので、チャットメッセージが追加されるとComponentクラスの `StateHasChangedメソッド`により、画面が更新されるようになります。尚、画面更新時に`this.InvokeAsync`を使用するのは、別スレッドからコントロールを操作しようとして怒られるためです。

## メッセージ入力画面

`Pages/Index.razor`にチャットメッセージの入力画面を作ります。

```javascript:Pages/Index.razor
@page "/"

<input type="text" @bind-value="InputMessage" />
<button class="btn btn-primary" @onclick="_=> chat.AddMessage(InputMessage)">送信</button>

@foreach (var m in chat.GetMessages())
{
    <div style="padding: 10px; margin-bottom: 10px; border: 1px solid #333333; border-radius: 10px;">
        @m
    </div>
}

@code
{
    [CascadingParameter(Name="Chat") ] protected ChatClass chat { get; set; }

    string InputMessage = "";
}
```

## チャットメッセージ履歴画面
`Pages/FetchData.razor`にチャットメッセージの履歴画面を作ります。

```javascript:Pages/FetchData.razor
@page "/fetchdata"

@using BlazorApp30.Data

<h3>チャットの履歴</h3>
<button class="btn btn-primary" @onclick="_=>chat.ClearMessages()">履歴クリア</button>

@foreach (var m in chat.GetMessages())
{
    <div style="padding: 10px; margin-bottom: 10px; border: 1px solid #333333; border-radius: 10px;">
        @m
    </div>
}

@code
{
    [CascadingParameter(Name = "Chat")] protected ChatClass chat { get; set; }
}
```

チャットメッセージを使う画面では、メッセージの送受信に関わるコードがなく、非常にスッキリ！
Server-Sideで画面をレンダリングする為、Server-Sideだからこそ成せるコードですが、非常に面白いですね。

# 参考
[blazorhelpwebsite.com  Implementing State Management In Blazor] (http://blazorhelpwebsite.com/Blog/tabid/61/EntryId/4338/Implementing-State-Management-In-Blazor.aspx)