前回
【Blazor入門】Blazor初心者がログインからチャット機能まで付けてデプロイしてみた ~その1~
の続きです。
前回までは、ログイン画面をそれとなく作って、チャットページに遷移するまで作りました。
今回は実際にチャットができる所までやってみたいと思います。
チャット画面の作成
そこまで難しくないので頑張っていきましょー
Chat.razor Chat.razor.csの作成
新たに、Chat.razorファイル
・Char.razor.csファイル
を作りましょう。
@page "/Chat"
<h1>チャット</h1>
<div align="center">
<div class="form-group">
<input @bind="_messageInput" size="50"/>
<button class="btn btn-primary" @onclick="SendAsync">送信</button>
</div>
<hr>
<div align="left">
@foreach (var message in _messages)
{
@*HTMLのタグをそのまま出力する為にMarkUpString型にキャストする*@
@((MarkupString)message)<br>
}
</div>
</div>
using System.Collections.Generic;
namespace LoginTest.Pages
{
public partial class Chat
{
#region フィールド
private List<string> _messages = new List<string>();
private string _messageInput;
#endregion
#region メソッド
/// <summary>
/// 送信ボタン押下時に発火
/// </summary>
public void SendAsync()
{
_messages.Insert(0, _messageInput);
}
#endregion
}
}
これを実行してみると、ブラウザ間の通信がうまくいきません。
それもそのはず、SignalR
が未実装だからです。
SignalR
SignalR
とは何ぞや、というのは以下の記事が参考になると思います。
ASP.NET SignalRを知る (1/5)
簡単に言えば、サーバーとクライアント間の非同期通信を簡単に実装できるフレームワークです。
SignalR
を使用した典型的な例としてチャットが挙げられるので、今回はチャットを作成します。
SignalRを実装してみる
.NET MVC
でもSignalR
を実装した事がありますが、当然(?)ながら当時はJavaScript
を使用してクライアントへの画面描画を行っていました。
しかしBlazor
ではクライアント処理もフルC#
で書くことが出来る為煩わしさが若干軽減されます。
Nuget
からMicrosoft.AspNetCore.SignalR.Client
をインストールします。
(Microsoft.AspNetCore.SignalR.Client.Core
ではなく。)
あとは、Chat.razor.cs
を以下のように書き換えてください。
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.SignalR.Client;
namespace LoginTest.Pages
{
public partial class Chat : ComponentBase, IDisposable
{
#region フィールド
private List<string> _messages = new List<string>();
private string _messageInput;
private HubConnection _hubConnection;
#endregion
#region プロパティ
[Inject]
public NavigationManager Navigation { get; set; }
public bool IsConnected => _hubConnection.State == HubConnectionState.Connected;
private async Task SendAsync()
{
await _hubConnection.SendAsync("SendMessageClientsAll", _messageInput);
_messageInput = string.Empty;
}
#endregion
#region メソッド
/// <summary>
///
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
_hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/chathub")) //startupのMapHubで指定したHubのURLを指定する.
.WithAutomaticReconnect(new RandomRetryPolicy()) //自動接続
.Build();
_hubConnection.On<string>("ReceiveMessage", (message) =>
{
if (string.IsNullOrEmpty(message)) return;
//入力文字列の中にURLが存在するかどうかを判定だけする
var urlPattern = new Regex(@"(https?|ftp)(:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+)");
var urlPatternMatch = urlPattern.Match(message);
//入力文字列をサニタイズする
message = HttpUtility.HtmlEncode(message);
//入力文字列の中にURLが存在する場合はアンカータグに変換する
if (urlPatternMatch.Success)
{
message = message.Replace(urlPatternMatch.Value, $"<a href=\"{urlPatternMatch}\">{urlPatternMatch}</a>");
}
_messages.Insert(0, message);
StateHasChanged();
});
_hubConnection.Reconnected += async connectionId =>
{
await _hubConnection.StartAsync();
};
//画面の更新を行う
await _hubConnection.StartAsync();
}
/// <summary>
///
/// </summary>
public void Dispose()
{
_ = _hubConnection.DisposeAsync();
}
#endregion
#region クラス
public class RandomRetryPolicy : IRetryPolicy
{
private readonly Random _random = new Random();
public TimeSpan? NextRetryDelay(RetryContext retryContext)
{
//2~5秒の間でランダムに再接続を試みる
return TimeSpan.FromSeconds(_random.Next(2, 5));
}
}
#endregion
}
}
コンストラクタに書いても良かったんですが、OnInitializedAsyncメソッド
を使用する為にComponentBaseクラス
を継承しています。(razorファイル
内でのコンストラクタってどうやって書くんだろう。。。)
ハブの追加は以下のページに詳細に書いてあるので、この手順に沿って追加すればおk。
ASP.NET Core SignalR を Blazor WebAssembly と共に使用する
ソリューションにHubsフォルダ
とChatHub.cs
を追加。
メッセージを受け取ったら、全クライアントにメッセージを流す処理を追加する。
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
namespace LoginTest.Hubs
{
public class ChatHub : Hub
{
public async Task SendMessageClientsAll(string message)
{
await Clients.All.SendAsync("ReceiveMessage", message);
}
}
}
全部載せる必要もないと思いますが、上記のURL
通りにStartup.cs
を以下の様にSignalR
に対応させる記述を書きます。
using System.Linq;
using LoginTest.Hubs;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace LoginTest
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddSignalR();
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseResponseCompression();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapHub<ChatHub>("/chathub");
endpoints.MapFallbackToPage("/_Host");
});
}
}
}
これでSignalR
の実装ができました。実際に動作を見てみましょう。
SignalRの動作について
どういう順序で処理が発生しているのかを追ってみましょう。
①まず最初に、[送信]ボタンを押す所から処理がスタートするので、Chat.razor
を見ます。
<button class="btn btn-primary" @onclick="SendAsync">送信</button>
②送信ボタン押下時に発火するのはSendAsyncメソッド
であり、この処理が紐づいているのはコードビハインドとなります。
private async Task SendAsync()
{
await _hubConnection.SendAsync("SendMessageClientsAll", _messageInput);
_messageInput = string.Empty;
}
③SendAsyncメソッド
では定義したChatHub
のSendMessageClientsAllメソッド
を呼んでいます。
public async Task SendMessageClientsAll(string message)
{
await Clients.All.SendAsync("ReceiveMessage", message);
}
④SendMessageClientsAllメソッド
内でReceiveMessage
が呼ばれます。これはChat.razor.cs
のOnInitializedAsyncメソッド
でハブを定義した際に作った処理です。
_hubConnection.On<string>("ReceiveMessage", (message) =>
{
if (string.IsNullOrEmpty(message)) return;
//入力文字列の中にURLが存在するかどうかを判定だけする
var urlPattern = new Regex(@"(https?|ftp)(:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+)");
var urlPatternMatch = urlPattern.Match(message);
//入力文字列をサニタイズする
message = HttpUtility.HtmlEncode(message);
//入力文字列の中にURLが存在する場合はアンカータグに変換する
if (urlPatternMatch.Success)
{
message = message.Replace(urlPatternMatch.Value, $"<a href=\"{urlPatternMatch}\">{urlPatternMatch}</a>");
}
_messages.Insert(0, message);
StateHasChanged();
});
⑤送信時に入力されているメッセージが_messages
に追加され最後にStateHasChangedメソッド
を呼ぶ事でChat.razor
の処理を通って画面に反映されます。
<div align="left">
@foreach (var message in _messages)
{
@*HTMLのタグをそのまま出力する為にMarkUpString型にキャストする*@
@((MarkupString)message)<br>
}
</div>
Enter押下時にメッセージを送信する方法
今の記述だと、メッセージを送信する為に毎回ボタンを押す必要があり、少し手間です。
そこでEnter押下時
にもメッセージが送信されるように修正してみましょう。
<div class="form-group">
@*<input @bind="_messageInput" size="50"/>*@
<input @bind="_messageInput" @bind:event="oninput" size="50" @onkeydown="KeyDownAsync"/>
<button class="btn btn-primary" @onclick="SendAsync">送信</button>
</div>
修正方法としては簡単で、@bind:event="oninput"
と@onkeydown="KeyDownAsync"
を付けてください。
@onkeydown="KeyDownAsync"
はわかりやすいと思いますが、割り付けている項目に対してキーダウンイベントを発生させるようにします。
@bind:event
にはデフォルトでは@bind:event="onchange"
が割付いており、これはフォーカスロストした場合に発火します。つまり文字を入力して次にエンターを押下しても、項目に対してフォーカスが入ったままなのでプロパティに反映されず_messageInput
にはnull
が入っている事になります。
これを避ける為にも、@bind:event="oninput"
とすることにより入力文字が変化する度にプロパティへの値の反映をさせることができます。
あとはKeyDownAsyncメソッド
を追加して、Enter押下時
にのみSendAsyncメソッド
を呼び出すようにしてあげれば完了です。
private async Task KeyDownAsync(KeyboardEventArgs e)
{
if (e.Key == "Enter")
{
await SendAsync();
}
}
分からなかった点
もし分かる方がいらっしゃれば教えて頂きたいです。
SignalR
の自動再接続についてです。
以下の様にWithAutomaticReconnectメソッド
を噛ましており、RandomRetryPolicyクラス
を引数として渡しているのでサーバーが落ちた場合に2~5秒おきに無限回の再接続要求がされると考えていました。
次のような想定
- デバッグでサーバー側を起動
- 同じURLを指定し、別のタブでチャットを起動
- デバッグを終了
- デバッグを再開
- 2で開いたタブが2~5秒おきに再接続を試みており復帰
_hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/chathub"))
.WithAutomaticReconnect(new RandomRetryPolicy())
.Build();
_hubConnection.Reconnected += async connectionId =>
{
await _hubConnection.StartAsync();
};
public class RandomRetryPolicy : IRetryPolicy
{
private readonly Random _random = new Random();
public TimeSpan? NextRetryDelay(RetryContext retryContext)
{
//2~5秒の間でランダムに再接続を試みる
return TimeSpan.FromSeconds(_random.Next(2, 5));
}
}
しかし実際には、サーバー側が復帰してもクライアント側は自動接続されず、リロードが求められてしまいます。
Reconnectedイベント
にStartAsyncメソッド
を実装しているのでリロードを必要とせず復帰するかと思いきやそんなことはなかった。何か思い違いをしているのでしょうか。。。
まとめ
SignalR
を使った場合でもBlazor
であればフルC#
で書けることが分かりました。
Enter押下時
の処理などもJavaScript
でif(keycode == '13'){}
などのような記述をしていたりしましたが、@onkeydown
と書くことにより処理をC#
に任せられるというのはかなりよいですね。
次回は正しいユーザーでログインした場合にログインを許可するような実装を行います。
参考にさせて頂いたページ