C#の実験的なBlazor（WebAssembly＋Razor）とSignalRでチャットを作ってみた３実用っぽいの編

#実用品っぽく変更
今回は自分が妄想しているそれっぽいページができるように変更を加えていきます。
##シリーズ
C#の実験的なBlazor（WebAssembly＋Razor）とSignalRでチャットを作ってみた
* [１準備編](https://qiita.com/lensouko/items/3aff7311887470ad0905)
* [２実装編](https://qiita.com/lensouko/items/f28dbbaf4dc8cedeff26)
* ３実用っぽいの編（ココ
##大まかな方針
このシリーズはBlazorを主体としてついでにSignalRと考えているためチャットのログの保存は考えていません。
ここからは実用的なものに近づけるべくチャットはホーム（Index）以外で表示するようにします。
###補足
実はローカルで動いたものの、Azureへ発行すると動かなくなっていたのですが、原因は言語サービスの更新をし忘れてMyGetのパッケージのみ更新してしまっていたことでした。タイミングが悪くて言語サービスの更新通知が来ていなかっただけかもしれませんが、日々の作業の準備としてBlazor言語サービスを最新にし、MyGetのパッケージも最新にしておきます。これは毎日しておいた方が良いかもしれません。
なお、サーバープロジェクトのターゲットフレームワークは「.NET Core 2.1」にしておきます（Blazorのターゲットが現時点で2.1となっていますので、実際に作成する際はその時の最新のBlazorが対応するターゲットのバージョンにしてください）。
###現時点での最新ソースと動作サンプル
[ソース(GitHub)](https://github.com/lensouko/SignalBlazorR)
[動作サンプル](http://signalblazorr2.azurewebsites.net/)
１・２で作成したソースを別ブランチとして残しておき、変更を加えていっています。
#変更内容
##チャットページ埋め込み化
１・２回目ではチャットは単独ページでしたが、これを各ページ（ソリューション作成時のテンプレで用意されたIndex以外のページです）へ埋め込むように変更します。
###Chatページのコンポーネント化
####Chatの移動
ファイルをPagesディレクトリからShareディレクトリへ移動します。
![Chat移動前後.png](https://qiita-image-store.s3.amazonaws.com/0/244471/b00dc3b4-f5b3-d091-fbd8-e3de8d328908.png)
次に、ChatをURLから判断して移動するページではなく埋め込みで使用するためのサブページなのでソースの@pageを削除します。また、それに伴いNaviMenuから"/chat"を削除してください。これでChat.schtmlがページからコンポーネント扱いに変わりましたので、Indexを除く他のページに埋め込みます。
####埋め込みの記述
![Chat埋め込み作業.png](https://qiita-image-store.s3.amazonaws.com/0/244471/9b2a3f23-80fc-9ad4-40d8-80f07a725448.png)
図のようにChatのクラス名をタグのように入力すると、途中で入力候補に出てきますので選んでからタグを閉じて"<Chat />"としてください。
今回はパラメータがありませんので指定してませんが、埋め込み元ページからしか受け取れない情報が必要な場合は埋め込みページの方へpublicプロパティを追加してください、そうすればタグにクラス名を入力後、スペースを空けると選択候補にプロパティが表示されますので"プロパティ名=渡したい値"と入力すればできます。今回は不要なのでやりませんが、以下に試したソースを提示します。

```C#:ChatData.schtml
<tr>
    <td>@Message.name</td>
    <td>@Message.message</td>
</tr>

@functions {
    public SimpleMessage Message { get; set; }
}
```

```C#:Chat.schtml
    @foreach (var Message in MessageList)
    {
        <ChatData Message=Message />
    }
```

と、文字列だけでなくオブジェクトも指定できます。
####試しの実行とエラー
さて、現段階で一度動かしてみるとエラーが起きてしまいます。これはChatクラスがPagesからSharedに移動したことにより、JavaScriptからC#メソッド呼び出し時のnamespaceの指定も変える必要があるためです。試しでC#内でthisをポイントすると～.Pages.Chatから～.Shared.Chatに変わっているのが確認できるかと思います。

```javascript:Chat.schtml
let AddMessageSMethod = Blazor.platform.findMethod(
	"SignalBlazorR.Client",
	"SignalBlazorR.Client.Shared",
	"Chat",
	"jusin"
);
```

C#側から渡せばいいのですが、今はこのままとしておきます。
IndexからCounterへ移動し、入力して発言ボタンをクリックするとこれで一見正常に動いたように見えますが、Fetch dataへ移動するとエラーが発生しています。
![Letエラー.png](https://qiita-image-store.s3.amazonaws.com/0/244471/96ccd647-31da-9fb0-91d2-149b20443b9b.png)
これはJavaScriptでSignalRのconnectionを格納する変数が既にあるのにまた宣言してるよってな意味のエラーになります。今までSignalRを普通に触っていただけなら起きなかったと思われますが、SPAとしたため、メニューからURLが切り替わってもブラウザ上でのページとしては切り替わっていない（サーバーへ読みに行って表示しているわけではないという意味です）ページのインスタンスは変更されておらず、JavaScriptのインスタンスもそのままなのでLetで同じ変数を複数回宣言してるよと怒られたことになります。これがvarならインスタンスが破棄されているのか問題なさそうに動くのですが、connectionが別物に切り替わっているため望んでいた動きとは異なってしまいますのでletでもエラーとならないように修正する必要があります。
####JavaScriptの外部ファイル化
前述のとおり、ページの切り替えではリロードが起きているわけではないことから、Chat部分の読み込みのたびに初期化が走るのが問題となりますので、外部ファイル化し、index.htmlで１度だけロードする様にに変更します。なお、JavaScriptファイルはwwwrootの下に配置します。すでにsignalrがありますので、自作ファイルとライブラリ／フレームワークとの違いが分かるようにsignalrはjs/libへ、chat.jsはjs直下へ置いています。index.htmlの修正も忘れないようにしてください。

```javascript:chat.js
document.addEventListener('DOMContentLoaded', function () {
    // 初期化処理が一度しか走っていないことを確認するためなので、
    // 後で削除してください
    console.log("DOMContentLoadedいべんとだー");
    // ここにChat.schtmlのscriptタグ内のソースを移動してきてください
});
```

上記の通りにしてDOM構築完了に合わせてSignalRのコネクションを開始するようにします。実際にページを何度切り替えても初期ロードのメッセージは１度しか出力されなくなっています。また、切り替え後でも問題なくチャットのはつげんボタンでチャットログが追加されていることが確認できます。ただし、ページを切り替えるとログが消えてしまいますのでちょっと寂しい気がします。
####画面切り替えでもチャットログをクリアしない
ではどうすればログが消えないのかということですが、一つはサーバーでログを保存してそれを読み込むというのがありますが、ページ切り替えのたびにログの読み直しは正直面倒です。特に先ほどのJavaScriptの件でブラウザ上でのページインスタンス（今どきのブラウザはタブ・ウィンドウ毎にプロセスを立ち上げるため、同一プロセスと読み替えた方が良いかもしれません）は同じでBlazorのChatインスタンスは別々ということが分かりましたので、これを利用しますので、別インスタンスでも共通となるとstaticさんの出番です。というわけでとりあえず面倒なので入力と出力の両方をstaticに変更してみます。

```c#:Chat.schtml
    // ここと
    public static SimpleMessage Msg = new SimpleMessage();
    public static List<SimpleMessage> MessageList = new List<SimpleMessage>();

    // ここのようなthis.～からthis.を外します
    // public void AddMessage内の処理ですが、
    // これをpublic static bool jusin内へ移動します
        MessageList.Add(msg);
```

正直AddMessageが要らない子のようになってしまいましたが、this.StateHasChanged();で再描画の指示を出せるのはインスタンスのみですので、ここだけを利用する形になってしまいました。protectedなのでmyIns.StateHasChanged();で呼び出せませんので、更新メソッドを用意します。

```c#:Chat.schtml
    // AddMessageメソッドを引数なしのRePaintメソッドに変更して、
    // jusinメソッドの呼び出しも修正します
    myIns.RePaint();

    // また、RePaintメソッドは以下のようにシンプルにします
    public void RePaint()
    {
        // 手動での更新通知
        this.StateHasChanged();
    }
```

これでメニューからページを切り替えてもチャットログも入力欄も消えなくなりました。あといい加減わかりにくいので、入力欄にラベルを張り付けておきます。上が名前で下が発言内容なので分かりやすいようにラベルをつけておいてください。
##Group機能
SignalRにはグループ機能があります。で、各ページの下にチャットを埋め込むようにしたので折角だからページごとのグループを作って同じページを閲覧している人同士でチャットできるようにしてみようと思います。
###ページURLの取得
ページごとのグループにするならページのURLをグループ名にすれば確実かと思いましたのでURLを取得することにします。サンプルにはなかったのでBlazorのソースを適当にそれっぽいところを重点的にみているとMicrosoft.AspNetCore.Blazor.Browser.Services.BrowserUriHelperクラスにGetAbsoluteUriというメソッドで取得できることが分かりました。取得したURLを元に"/counter"のようなメニューで指定するURLへ変換するためにUriクラスを使用します。なお、"/"の場合は後ろにIndexをつける処理を追加しています。また、Groupをサーバーへ送信するためにSimpleMessageクラスへプロパティを追加します。後はグループ追加と離脱の処理をServerとJavaScriptに加えるとひとまずはそれっぽくグループ機能が動くかと思います。
ただ、問題なのがページの表示タイミングはOnInitで拾えますが、ページから離脱するイベントは取得できません。JavaScriptにならあるのですが仕方がないのでOnInit内でstaticにしたMsg.roomから初期表示なのかページ切り替えなのかを判断してグループの離脱・追加を行うようにします。

```c#:Chat.cshtml
// usingの追加（一回だけなので書かなくてもおｋです）
@using Microsoft.AspNetCore.Blazor.Browser.Services;
// OnInit内に記述
var url = new BrowserUriHelper().GetAbsoluteUri();
url = $"{url}{(url.EndsWith("/") ? "index" : string.Empty)}";
var uri = new Uri(url);
if (Msg.room != uri.AbsolutePath)
{
    if (!string.IsNullOrWhiteSpace(Msg.room))
    {
        RegisteredFunction.Invoke<bool>("グループ離脱", Msg);
    }
    Msg.room = uri.AbsolutePath;
    RegisteredFunction.Invoke<bool>("グループ追加", Msg);
}
```

JavaScript側の方はBlazor.registerFunctionで登録している以外は通常のSignalRと変わりませんので省略します。また、サーバーサイドも同様に省略します。
これでページごとのグループでのチャットができるようになりました。グループ関係なしで発信できるようにサンプルではSharedの方へ定数を定義し、C#クライアントで送信メッセージのインスタンスを複製し、roomの値に定数を使用し、サーバーサイドで同じ定数で判断するようにしています。これも設定した定数を変更する必要が出た際にはSharedの値を変更するだけでクライアントサイドもサーバーサイドも同時に変更できるのがBlazorのメリットと言えますね。

#総括
チャット部分に関しては実用品とするにはまだまだ手を入れる部分がありますが、だいぶんそれっぽい形になったと思います。これ以上のところはもうBlazor要素がなくなりそうですし、そもそも設計方針やどんな機能を実装したいかにもよる部分がありますのでご自身で色々試すなりASP.NET CoreのSiganlRでググるなりすれば解決するかと思います。
今回僕自身が思ったことは、VSの通知に出ていなくてもソースをいじる前にツールの「拡張機能と更新プログラム」でBlazorの言語サービスの更新を行っておくことと、MyGet(NuGetのパッケージ管理)も最新にしておくこと、それとオープンソースなのでこんな機能があると思うけどどうすればいいのか？と思ったらとりあえずソースを落としてVSで開いてみることが重要かと思いました。ちなみにVSで開く理由は自分で使用する際にnamespaceが分からないと困るのでVSで開いてポイントすれば表示してくれるという理由です。またそのクラスの定義へ移動できるのもメリットです。
それではこの後は肝心なコンテンツ部分がテンプレそのままでしたのでしばらくは主に画面サイドの機能をいろいろと調べていこうと思いますので、それなりに記事に起こせるような情報がたまったらその４を書こうかと思います。


##更新履歴
* 2018/04/02 23:50頃　とりあえずソースと動作サンプルを公開するために説明なしで投稿。
* 2018/04/06 23:40頃　前回から結構大きく変更してしまいましたので、徐々に記述する方針から変更した内容とBlazorというかSPA特有の振る舞いが理由の変更点などを書いていこうと思い、書き直し。
* 2018/04/07 21:30頃　Blazorにかかわらない部分は省略しつついったんチャットを実用的な感じでできるところまで記述