[Introduction to Blazor] Blazor beginners tried to deploy with login and chat functions-Part 2-

7 minute read

Last time

[Introduction to Blazor] Blazor beginners tried to deploy with login to chat function ~ Part 1 ~
It is a continuation of.

Until the last time, I made a login screen implicitly and made it until it transitioned to the chat page.
This time I would like to go to the place where I can actually chat.

Creating a chat screen

It’s not that difficult, so let’s do our best!

Chat.razor Creating Chat.razor.cs

Let’s create a new Chat.razor file and Char.razor.cs file.
image.png

Chat.razor


@page "/Chat"

<h1>chat</h1>

<div align="center">
    <div class="form-group">
        <input @bind="_messageInput" size="50"/>
        <button class="btn btn-primary" @onclick="SendAsync">Send</button>
    </div>

    <hr>

    <div align="left">
        @foreach (var message in _messages)
        {
            @*Cast to MarkUpString type to output HTML tag as it is*@
            @((MarkupString)message)<br>
        }
    </div>
</div>

C#:Chat.razor.cs


using System.Collections.Generic;

namespace LoginTest.Pages
{
    public partial class Chat
    {
        #region field
        private List<string> _messages = new List<string>();
        private string _messageInput;
        #endregion

        #region method
        /// <summary>
        ///Ignite when the send button is pressed
        /// </summary>
        public void SendAsync()
        {
            _messages.Insert(0, _messageInput);
        }
        #endregion
    }
}

When I try to do this, the communication between browsers does not work.
That should be because SignalR is not implemented yet.
Counter.gif

SignalR
I think the following article will be helpful for what is SignalR.
Knowing ASP.NET SignalR (1/5)

Simply put, it’s a framework that makes it easy to implement asynchronous communication between the server and the client.
Chat is a typical example of using SignalR, so let’s create a chat this time.

Try implementing SignalR

I have implemented SignalR in .NET MVC, but of course (?) At that time, I used JavaScript to draw the screen to the client.

However, with Blazor, client processing can also be written in full C # , so the annoyance is slightly reduced.

Install Microsoft.AspNetCore.SignalR.Client from Nuget.
(Not Microsoft.AspNetCore.SignalR.Client.Core.)

image.png

After that, rewrite Chat.razor.cs as follows.

C#: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 field
        private List<string> _messages = new List<string>();
        private string _messageInput;
        private HubConnection _hubConnection;
        #endregion

        #region property
        [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 method
        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        protected override async Task OnInitializedAsync()
        {
            _hubConnection = new HubConnectionBuilder()
                .WithUrl(Navigation.ToAbsoluteUri("/chathub")) //Specify the URL of the Hub specified in Map Hub of startup.
                .WithAutomaticReconnect(new RandomRetryPolicy()) //Automatic connection
                .Build();

            _hubConnection.On<string>("ReceiveMessage", (message) =>
            {
                if (string.IsNullOrEmpty(message)) return;

                //Only determine if the URL exists in the input string
                var urlPattern = new Regex(@"(https?|ftp)(:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+)");
                var urlPatternMatch = urlPattern.Match(message);

                //Sanitize the input string
                message = HttpUtility.HtmlEncode(message);

                //If the URL exists in the input string, convert it to an anchor tag
                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();
            };

            //Update the screen
            await _hubConnection.StartAsync();
        }
        /// <summary>
        /// 
        /// </summary>
        public void Dispose()
        {
            _ = _hubConnection.DisposeAsync();
        }
        #endregion

        #region class
        public class RandomRetryPolicy : IRetryPolicy
        {
            private readonly Random _random = new Random();

            public TimeSpan? NextRetryDelay(RetryContext retryContext)
            {
                //Randomly try to reconnect between 2 and 5 seconds
                return TimeSpan.FromSeconds(_random.Next(2, 5));
            }
        }
        #endregion
    }
}

I could have written it in the constructor, but it inherits the ComponentBase class to use the ʻOnInitializedAsync method. (How do you write a constructor in a razor file` …)

The addition of hubs is described in detail on the following page, so you can add it according to this procedure.
Use ASP.NET Core SignalR with Blazor WebAssembly

Add Hubs folder and ChatHub.cs to the solution.
image.png

When a message is received, add a process to send the message to all clients.

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);
        }
    }
}

I don’t think it is necessary to list all of them, but write the description that makes Startup.cs correspond to SignalR as follows according to the above ʻURL`.

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");
            });
        }
    }
}

You have now implemented SignalR. Let’s see how it actually works.
Counter.gif

About the operation of SignalR

Let’s follow the order in which the processes occur.

① First of all, the process starts from the place where you press the [Send] button, so look at Chat.razor.

Chat.razor


<button class="btn btn-primary" @onclick="SendAsync">Send</button>

(2) It is the SendAsync method that fires when the send button is pressed, and this process is linked to the code-behind.

C#:Chat.razor.cs


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

③ In the SendAsync method, the SendMessageClientsAll method of the defined ChatHub is called.

ChatHub.cs


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

ReceiveMessage is called in the SendMessageClientsAll method. This is the process created when defining the hub with the ʻOnInitializedAsync method of Chat.razor.cs`.

C#:Chat.razor.cs


_hubConnection.On<string>("ReceiveMessage", (message) =>
{
    if (string.IsNullOrEmpty(message)) return;

    //Only determine if the URL exists in the input string
    var urlPattern = new Regex(@"(https?|ftp)(:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+)");
    var urlPatternMatch = urlPattern.Match(message);

    //Sanitize the input string
    message = HttpUtility.HtmlEncode(message);

    //If the URL exists in the input string, convert it to an anchor tag
    if (urlPatternMatch.Success)
    {
        message = message.Replace(urlPatternMatch.Value, $"<a href=\"{urlPatternMatch}\">{urlPatternMatch}</a>");
    }

    _messages.Insert(0, message);
    StateHasChanged();
});

⑤ The message entered at the time of sending is added to _messages, and by calling the StateHasChanged method at the end, it is reflected on the screen through the processing of Chat.razor.

Chat.razor


<div align="left">
    @foreach (var message in _messages)
    {
        @*Cast to MarkUpString type to output HTML tag as it is*@
        @((MarkupString)message)<br>
    }
</div>

How to send a message when Enter is pressed

With the current description, you have to press the button every time to send a message, which is a little troublesome.
So let’s modify it so that the message is also sent when ʻEnter is pressed`.

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">Send</button>
</div>

It’s easy to fix, just add @ bind: event =" oninput " and @ onkeydown =" KeyDownAsync ".

I think @ onkeydown =" KeyDownAsync " is easy to understand, but I want to raise a keydown event for the assigned item.

By default, @ bind: event is assigned @ bind: event = "onchange" , which fires when focus is lost. In other words, even if you enter a character and press enter next time, the focus remains on the item, so it is not reflected in the property and _messageInput contains null.

To avoid this, you can set @ bind: event =" oninput " to reflect the value in the property every time the input character changes.

After that, add the KeyDownAsync method and call the SendAsync method only when ʻEnter is pressed`, and you’re done.

C#:Chat.razor.cs


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

What I didn’t understand

If anyone knows, please let me know.
About automatic reconnection of SignalR.

Since I am biting the WithAutomaticReconnect method as shown below and passing the RandomRetryPolicy class as an argument, I thought that if the server goes down, an infinite number of reconnection requests will be made every 2 to 5 seconds. ..

The following assumptions

  1. Start the server side with debug
  2. Specify the same URL and start chat in another tab
  3. Finish debugging
  4. Resume debugging
    5.2 The tab opened in 2 is trying to reconnect every 2 to 5 seconds and returns.

C#: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)
    {
        //Randomly try to reconnect between 2 and 5 seconds
        return TimeSpan.FromSeconds(_random.Next(2, 5));
    }
}

However, in reality, even if the server side returns, the client side will not be automatically connected and reloading will be required.
image.png

Since the StartAsync method is implemented in the Reconnected event, I didn’t think that it would return without the need for reloading. Am I doing something wrong? .. ..

Summary

I found that even if I used SignalR, I could write in full C # with Blazor.

The processing of when pressing Enter was also described as ʻif (keycode == ‘13’) {} in JavaScript, but the processing is done by writing @ onkeydown. It's pretty good to leave it to C # .

Next time, we will implement to allow login when logged in as the correct user.

** Reference page **

-Knowing ASP.NET SignalR (1/5)
-Use ASP.NET Core SignalR with Blazor WebAssembly
-ASP.NET Core SignalR .Net Client
-ASP.NET Core Blazor Data Binding
-Output HTML tags in Blazor application
-SignalR automatic reconnection in a little more detail.