#前書き
こちらはタイトル通りのチャットの基本部分をとりあえず作ってみたのでサンプルとして投稿しております。当投稿は実装編となり、開発準備などを行った1準備編の続きです。
あくまでも実用レベルではありませんので見た目は適当で送受信する内容もチャットとして使うには足りていないものになりますが、以下の内容を実装できればとりあえずの基礎部分はできるため十分かと思います。
- 送信ボタンのクリックでC#のイベントハンドラを呼び出す
- C#からJavaScriptを呼び出し、画面の入力値をクラスのインスタンスを引数として渡す
- SignalRをJavaScriptから使用してServerのhubへ送信し、全クライアントで受信する(こちらはただのAsp.Net CoreによるSiganlRなので特に問題はないはずでした)
- JavaScriptからC#のメソッドを呼び出してhubより受信したオブジェクトを引数として渡す
- JavaScriptから呼び出されたC#のメソッドで渡された引数を元に画面へ反映させる
##この投稿のシリーズ
##更新履歴
- 2018年03月22日 ChatHubのusingを変更し忘れてたので修正。動いてるソースからコピペをしながら記事を書いていたけど、ビルドの確認もしていなかったため漏れてました。
- 2018年03月28日 SignalR受信時のC#サイドでJSONからのデシリアライズが動かなくなっていたため、Newtonsoft.JsonからMicrosoft.AspNetCore.Blazor.JsonUtilを使用するように変更。
- 2018年04月02日 アレンジ版のページを仮で作成したのでシリーズのリンクを用意
#SignalRを使用する準備
##~.Sharedプロジェクト
ファイルを追加してSignalRのhubとブラウザとでやり取りデータのクラスを定義します。
横着してサーバーへの送信とクライアントへの送信を同じクラスで行っていますが、実際に使い物になるものを作る場合はサーバーへの送信では画面入力項目をすべて格納できる定義で、クライアントへの送信ではそれに加えて時間だったり書き込み者の情報だったり必要な内容を足したものになるかと思います。また、特定のユーザーやグループへの送信となると誰宛なのかといった情報もサーバーへ送信する情報に追加する必要があります。
また、jsonが全部小文字になってしまったので多分属性で頭文字が大文字になるようにした方が良いのですが、そういうのは今回は重要ではなかったので後回しにしました。
public class SimpleMessage
{
public string name { get; set; } = string.Empty;
public string message { get; set; } = string.Empty;
}
横着しているのでSharedではこれだけです。
##~.Serverプロジェクト
###Hubの作成
「ChatHub」の名前でHubを作成します。受信した内容を無加工で配信します。
本来ならログへ保存するなり送信元情報を付加するなりしますが、主にBlazorの今回は技術サンプルなので手抜きします。
通常、「:Hub」とするだけで十分なのですが、ファイルを置くディレクトリ名を「Hub」にしてしまったため、そりゃネームスペースやん!と突っ込まれてしまいました。自動で付けられるnamespaceから最後の「.Hub」を削るか別の名前のディレクトリにするかした方が良いのかもしれませんが今回は継承するクラス名をフルで書くようにしています。
また、α版とプレビュー版でメソッド名が変わっていますので要注意です。
using SignalBlazorR.Shared;
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalBlazorR.Server.Hub
{
public class ChatHub : Microsoft.AspNetCore.SignalR.Hub
{
public Task PostMessage(SimpleMessage msg)
{
// プレビュー1の場合
return Clients.All.SendAsync("AddMessage", msg);
// α2の場合
//return Clients.All.InvokeAsync("AddMessage", msg);
}
}
}
###SignalRの使用とHubのマッピングを登録
Startupを編集してSignalRを使用できるようにします。
ConfigureServicesメソッドでSignalRの仕様を登録し、Configureメソッドでは作成したChatHubとパスを(多分)紐づけます。
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver = new DefaultContractResolver();
});
// この1行を追加します
services.AddSignalR();
services.AddResponseCompression(options =>
{
options.MimeTypes = ResponseCompressionDefaults.MimeTypes
.Concat(new[] { MediaTypeNames.Application.Octet });
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseResponseCompression();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// UseMvcの後でもいいかどうか不明ですが
app.UseSignalR(routes =>
{
// 使用するClassを登録しているようです
routes.MapHub<ChatHub>("/chathub");
});
app.UseMvc(routes =>
{
routes.MapRoute(name: "default", template: "{controller}/{action}/{id?}");
});
app.UseBlazor<Client.Program>();
}
}
以上でServerプロジェクトは終わりです。
##~.Clientプロジェクト
###チャットページ以外の準備
signalr.jsは「index.html」内で設定します。
また、作成するページは「Chat.cshtml」としますので、メニューにも追加します。
<head><!--他の内容は省略しています-->
<script type="text/javascript" src="js/signalr.js"></script>
</head>
<!--メニューが並んでいる途中に以下の内容を追加します-->
<!--コピペして編集したのでhome用のアイコンですが今は気にしません-->
<li>
<NavLink href="/chat">
<span class='glyphicon glyphicon-home'></span> Chat
</NavLink>
</li>
###using等とUI部分
そしてついに肝心要のチャットページ作成です。
「Pages」の下に「Chat.schtml」を追加して編集します。
先頭にはusingでSharedやブラウザとの連携機能やJSONを使用するためのものを追加し、@pageでURLのパスのどれに該当するページなのかを宣言します。
@pageは触り始めた時のテンプレにはなかったのですが、昨日更新したテンプレには含まれており、この追加と「App.cshtml」のRouterにあるnamespaceの削除を行わないとページが真っ白になるという悲劇が発生してしまいました。
また、入力部分と出力部分は単純なhtmlにrazor構文で記述となります。ただ、ボタンのイベント記述はJavaScriptではなくC#を呼び出すもので、入力の方もC#側から削除できるようにするために少し通常のrazor構文とは異なります。
@using SignalBlazorR.Shared;
@using Microsoft.AspNetCore.Blazor;
@using Microsoft.AspNetCore.Blazor.Browser.Interop;
@page "/chat"
<p>
<input type="text" @bind(Msg.name)><br />
<input type="text" @bind(Msg.message)><br />
<button @onclick(発言)>はつげん</button><br />
</p>
<table class='table'>
<thead>
<tr>
<th>名前</th>
<th>発言</th>
</tr>
</thead>
<tbody>
@foreach (var Message in MessageList)
{
<tr>
<td>@Message.name</td>
<td>@Message.message</td>
</tr>
}
</tbody>
</table>
###JavaScript実装
JavaScriptは通常のSignalRのように記述しますが、送信のトリガーがボタンのクリックイベントではなく、ボタンのクリックイベントをハンドルするC#メソッドからの呼び出しになる点と、Hubから受信した際のイベントハンドラは処理を行うC#のメソッドを呼ぶ必要があり、かつその際の引数の使用に制限があるため、一度JSONへ戻してから.netの文字列型へ変換する処理をかます必要があります。
C#からJavaScriptのメソッドを呼び出す場合はJavaScript側で「Blazor.registerFunction」を使用してメソッドの登録を行うことで呼び出せるようになります。
逆にJavaScriptからC#のメソッドを呼び出す場合は面倒で、assembly名(ビルドして出来上がるdllの名前でプロジェクトのプロパティで確認できます)とnamespace(プロジェクト名.Pagesになります)とtype(クラス)名も必要となります。C#側で取得して@assemblyNameとかやっても現段階では改行が入るのか動きませんでしたので、リテラルで記述しています。上記の名称と呼び出すメソッド名で「Blazor.platform.findMethod」を使用して関数ポインタを取得するようで、取得した関数ポインタから「Blazor.platform.callMethod」で呼び出しを行います。
この時、引数に使えるのは文字列型4個までという制限があるため、渡したいオブジェクトを「JSON.stringify」でJSONにし、更にJavaScriptの文字列では渡せないらしく、「Blazor.platform.toDotNetString」で使用可能なデータへ変換します。
<script>
// コネクション作成
let connection = new signalR.HubConnection('/chathub');
// 受信処理
connection.on('AddMessage', Msg => {
console.log("受信");
let AddMessageSMethod = Blazor.platform.findMethod(
"SignalBlazorR.Client",
"SignalBlazorR.Client.Pages",
"Chat",
"jusin"
);
var ts = Blazor.platform.toDotNetString(JSON.stringify(Msg));
Blazor.platform.callMethod(AddMessageSMethod, null, [ts]);
});
// 送信処理
Blazor.registerFunction("迷信", Msg => {
connection.invoke("PostMessage", Msg)
.catch(e => console.log(e));
return true;
});
// ログ出力
Blazor.registerFunction("log", Msg => {
console.log(Msg);
return true;
});
// 接続開始
connection.start().catch(e => console.log(e));
</script>
###Blazor実装
C#では画面の入力を元にJavaScriptへオブジェクトを渡してSignalRの送信メソッドを呼び出します。また、受信のイベントハンドラはJavaScript側にあるため、JavaScriptから呼び出し可能なメソッドを用意してJSONで受け取ります。受け取ったJSON文字列はMicrosoft.AspNetCore.Blazor.JsonUtilクラスのDeserializeメソッドでインスタンスに戻します。また、JavaScriptからはリフレクションを利用して関数ポインタのようなものを取得してから呼び出すため、staticなメソッドしか呼び出してもらえません。そのうえ、チャットログ用のListを更新してもthis.StateHasChanged()を呼び出さないと変更が通知されなくて画面が更新されませんので、c#側のインスタンスが必要となります。
そのため、OnInitメソッドでstatic変数にthisを格納してインスタンスメソッドを呼ぶ必要があります。なので、JavaScriptへ公開するためのstaticメソッドとStateHasChangedで変更を通知するためのインスタンスメソッドのセットが必要になります。
@functions {
SimpleMessage Msg = new SimpleMessage();
List<SimpleMessage> MessageList = new List<SimpleMessage>();
public static Chat myIns;
protected override void OnInit()
{
myIns = this;
}
// ここはC#内で完結するため日本語メソッド名が利用できました
protected void 発言()
{
RegisteredFunction.Invoke<bool>("迷信", Msg);
}
public static bool jusin(string smsg)
{
RegisteredFunction.Invoke<bool>("log", "「jusin」よばれたー");
var msg = JsonConvert.DeserializeObject<SimpleMessage>(smsg);
myIns.AddMessage(msg);
return true;
}
public void AddMessage(SimpleMessage smsg)
{
// JavaScriptでログ出力
RegisteredFunction.Invoke<bool>("log", "「AddMessage」よばれたー");
this.MessageList.Add(smsg);
// C#でのログ出力を行うと
// WASM: 「AddMessage」よばれたー
// と出力されます
Console.Out.WriteLine("「AddMessage」よばれたー");
// 手動での更新通知
this.StateHasChanged();
}
}
#まとめ
まだ作成中の実験的なBlazorなので、実際に自分が作ってる途中でも変更が入って画面が真っ白になるという悲劇も起きましたしまだ本格利用は無理というところです。とはいえ面白い技術なので、SignalRやwebsocketなどいくつかの通信手段を.netアセンブリからWebAssemblyへのコンパイル時にブラウザのAPIを使用するようになってくれればJavaScriptを排除して完全にC#オンリーでできるようになっていろいろ遊べるようになるかもしれません。
なんといってもJavaScriptからC#への呼び出しがすごく面倒なので何とかしてほしいものですが、自分が詰まったところを一通り書いておきましたので、多分同じようなことをしようとしたら参考になるかもしれません。もっとも、Blazorの内部処理が大幅に変わって記述も変わったらお役目御免となるかもしれませんが。
2018年03月28日に気づいたのですが、ClientのC#でJSONのデシリアライズが動かなくなっており、悲しくなってBlazorのソースから絶対にJSON使ってそうな「Microsoft.AspNetCore.Blazor.Browser.Http」のBrowserHttpMessageHandlerを漁っていたところ、JsonUtilを発見しましたのでそちらを使うように変更しました。なお、JsonUtilの内部では「SimpleJson」というものを使っているようでした。