LoginSignup
6
8

More than 3 years have passed since last update.

【Blazor入門】Blazor初心者がログインからチャット機能まで付けてデプロイしてみた ~その2~

Posted at

前回

【Blazor入門】Blazor初心者がログインからチャット機能まで付けてデプロイしてみた ~その1~
の続きです。

前回までは、ログイン画面をそれとなく作って、チャットページに遷移するまで作りました。
今回は実際にチャットができる所までやってみたいと思います。

チャット画面の作成

そこまで難しくないので頑張っていきましょー

Chat.razor Chat.razor.csの作成

新たに、Chat.razorファイルChar.razor.csファイルを作りましょう。
image.png

Chat.razor
@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>
Chat.razor.cs
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が未実装だからです。
Counter.gif

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ではなく。)

image.png

あとは、Chat.razor.csを以下のように書き換えてください。

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を追加。
image.png

メッセージを受け取ったら、全クライアントにメッセージを流す処理を追加する。

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に対応させる記述を書きます。

Startup.cs
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の実装ができました。実際に動作を見てみましょう。
Counter.gif

SignalRの動作について

どういう順序で処理が発生しているのかを追ってみましょう。

①まず最初に、[送信]ボタンを押す所から処理がスタートするので、Chat.razorを見ます。

Chat.razor
<button class="btn btn-primary" @onclick="SendAsync">送信</button>

②送信ボタン押下時に発火するのはSendAsyncメソッドであり、この処理が紐づいているのはコードビハインドとなります。

Chat.razor.cs
private async Task SendAsync()
{
    await _hubConnection.SendAsync("SendMessageClientsAll", _messageInput);
    _messageInput = string.Empty;
}

SendAsyncメソッドでは定義したChatHubSendMessageClientsAllメソッドを呼んでいます。

ChatHub.cs
public async Task SendMessageClientsAll(string message)
{
    await Clients.All.SendAsync("ReceiveMessage", message);
}

SendMessageClientsAllメソッド内でReceiveMessageが呼ばれます。これはChat.razor.csOnInitializedAsyncメソッドでハブを定義した際に作った処理です。

Chat.razor.cs
_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の処理を通って画面に反映されます。

Chat.razor
<div align="left">
    @foreach (var message in _messages)
    {
        @*HTMLのタグをそのまま出力する為にMarkUpString型にキャストする*@
        @((MarkupString)message)<br>
    }
</div>

Enter押下時にメッセージを送信する方法

今の記述だと、メッセージを送信する為に毎回ボタンを押す必要があり、少し手間です。
そこでEnter押下時にもメッセージが送信されるように修正してみましょう。

Chat.razor
<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メソッドを呼び出すようにしてあげれば完了です。

Chat.razor.cs
private async Task KeyDownAsync(KeyboardEventArgs e)
{
    if (e.Key == "Enter")
    {
        await SendAsync();
    }
}

分からなかった点

もし分かる方がいらっしゃれば教えて頂きたいです。
SignalRの自動再接続についてです。

以下の様にWithAutomaticReconnectメソッドを噛ましており、RandomRetryPolicyクラスを引数として渡しているのでサーバーが落ちた場合に2~5秒おきに無限回の再接続要求がされると考えていました。

次のような想定
1. デバッグでサーバー側を起動
2. 同じURLを指定し、別のタブでチャットを起動
3. デバッグを終了
4. デバッグを再開
5. 2で開いたタブが2~5秒おきに再接続を試みており復帰

Chat.razor.cs
_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));
    }
}

しかし実際には、サーバー側が復帰してもクライアント側は自動接続されず、リロードが求められてしまいます。
image.png

ReconnectedイベントStartAsyncメソッドを実装しているのでリロードを必要とせず復帰するかと思いきやそんなことはなかった。何か思い違いをしているのでしょうか。。。

まとめ

SignalRを使った場合でもBlazorであればフルC#で書けることが分かりました。

Enter押下時の処理などもJavaScriptif(keycode == '13'){}などのような記述をしていたりしましたが、@onkeydownと書くことにより処理をC#に任せられるというのはかなりよいですね。

次回は正しいユーザーでログインした場合にログインを許可するような実装を行います。

参考にさせて頂いたページ

6
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
8