[Introduction to Blazor] Blazor beginners tried to deploy with login and chat functions-Part 2-
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
.
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.
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
.)
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.
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.
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
- Start the server side with debug
- Specify the same URL and start chat in another tab
- Finish debugging
- 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.
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.