Reputation: 13
I'm following this tutorial creating a chat app.
My chat app has multiple pages with different chatrooms. Every chatroom has the same code, I use this @page "/chat/{roomId:int}"
to denote each of the chatrooms.
My problem is that I don't want to create a new SignalR connection every time the user goes to another chat (I'm using a proxy, which introduces delays that i want to minimize).
But I don't really know how to go about separating the hubConnection from the page, especially how to handle this part of the code _hubConnection.On<string, string>("Broadcast", BroadcastMessage);
because it references a method inside this page and I'm not sure how to set this if the hubConnection was in a different file.
I was thinking of either creating a separate service for SignalR or putting the connection into App.razor, but I dont know how to handle hubConnection.On... I was thinking that this should be possible, given no refresh happens when i go to another chatroom. I would appreciate any advice. Thank you.
Upvotes: 1
Views: 27
Reputation: 22082
This requirement is very interesting. We usually don't do this because a new connectionId will be created each time _hubConnection is created. This can identify how many pages/clients the user has opened. After reconnecting, the new connectionId will be used, and the message may be lost.
Of course, you can implement this function. We can use AddSingleton to register ChatService. The following is my test code. (It is not a perfect code, you need to further improve it later)
I copy the ChatRoom.razor and create a new ChatRoom.razor, we can see they are using same connectionId in my test result.
ChatService.cs
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.AspNetCore.Components;
using System.Collections.Concurrent;
using _79472916;
public class ChatService
{
private static readonly ConcurrentDictionary<string, HubConnection> ConnectedUsers = new();
public event Action<string, string> OnMessageReceived;
public async Task ConnectAsync(string username)
{
if (string.IsNullOrWhiteSpace(username))
{
throw new ArgumentException("Username cannot be empty.", nameof(username));
}
if (!ConnectedUsers.ContainsKey(username))
{
string hubUrl = "https://localhost:7228/chat";
var hubConnection = new HubConnectionBuilder()
.WithUrl(hubUrl)
.Build();
hubConnection.On<string, string>("Broadcast", (user, message) =>
{
OnMessageReceived?.Invoke(user, message);
});
await hubConnection.StartAsync();
ConnectedUsers.TryAdd(username, hubConnection);
await SendMessageAsync(username, $"[Notice] {username} joined the chat room.");
}
}
public async Task SendMessageAsync(string username, string message)
{
if (ConnectedUsers.TryGetValue(username, out var hubConnection) && hubConnection.State == HubConnectionState.Connected)
{
await hubConnection.SendAsync("Broadcast", username, message);
}
}
public async Task DisconnectAsync(string username)
{
if (ConnectedUsers.TryRemove(username, out var hubConnection))
{
await SendMessageAsync(username, $"[Notice] {username} left the chat room.");
await hubConnection.StopAsync();
await hubConnection.DisposeAsync();
}
}
}
ChatRoom.razor
@page "/chatroom"
@inject ChatService ChatService
@rendermode InteractiveServer
<h1>Blazor SignalR Chat Sample</h1>
<hr />
@if (!_isChatting)
{
<p>Enter your name to start chatting:</p>
<input type="text" maxlength="32" @bind="@_username" />
<button type="button" @onclick="@Chat">Chat!</button>
@if (_message != null)
{
<div class="invalid-feedback">@_message</div>
<small class="form-text text-muted">@_message</small>
}
}
else
{
<div class="alert alert-secondary mt-4">
<span>You are connected as <b>@_username</b></span>
<button class="btn btn-sm btn-warning ml-md-auto" @onclick="@DisconnectAsync">Disconnect</button>
</div>
<div id="scrollbox">
@foreach (var item in _messages)
{
@if (item.IsNotice)
{
<div class="alert alert-info">@item.Body</div>
}
else
{
<div class="@item.CSS">
<div class="user">@item.Username</div>
<div class="msg">@item.Body</div>
</div>
}
}
<hr />
<textarea class="input-lg" placeholder="Enter your message" @bind="@_newMessage"></textarea>
<button class="btn btn-default" @onclick="@SendAsync">Send</button>
</div>
}
@code {
private bool _isChatting = false;
private string _username;
private string _message;
private string _newMessage;
private List<Message> _messages = new();
protected override async Task OnInitializedAsync()
{
ChatService.OnMessageReceived += BroadcastMessage;
}
public async Task Chat()
{
if (string.IsNullOrWhiteSpace(_username))
{
_message = "Please enter a name";
return;
}
await ChatService.ConnectAsync(_username);
_isChatting = true;
_messages.Clear();
}
private void BroadcastMessage(string name, string message)
{
bool isMine = name.Equals(_username, StringComparison.OrdinalIgnoreCase);
_messages.Add(new Message(name, message, isMine));
InvokeAsync(StateHasChanged);
}
private async Task SendAsync()
{
if (!string.IsNullOrWhiteSpace(_newMessage))
{
await ChatService.SendMessageAsync(_username, _newMessage);
_newMessage = string.Empty;
}
}
private async Task DisconnectAsync()
{
if (_isChatting)
{
await ChatService.DisconnectAsync(_username);
_isChatting = false;
}
}
private class Message
{
public Message(string username, string body, bool mine)
{
Username = username;
Body = body;
Mine = mine;
}
public string Username { get; set; }
public string Body { get; set; }
public bool Mine { get; set; }
public bool IsNotice => Body.StartsWith("[Notice]");
public string CSS => Mine ? "sent" : "received";
}
}
Register it
using _79472916;
using _79472916.Components;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyHeader()
.AllowAnyMethod()
.SetIsOriginAllowed(_ => true)
.AllowCredentials();
});
});
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddSingleton<ChatService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
//app.MapHub<BlazorChatSampleHub>("/chat");
app.UseCors("AllowAll");
app.UseWebSockets();
app.MapHub<BlazorChatSampleHub>("/chat");
app.Run();
Upvotes: 0