Efficient Sequential List Update with SignalR in Angular and .NET
Table of Contents
Introduction
When working with task management or planning applications, ensuring that users can reorder lists efficiently and in real-time is critical. A user may need to adjust the sequence of tasks, and such updates must be immediately reflected across all active users.
In this post, we’ll walk through how to manage and broadcast sequential list updates in real-time using SignalR for broadcasting updates and Angular for frontend notification handling. The goal is to provide users with a seamless experience as they reorder a list.
Create Server-Side Broadcasting Mechanism in .NET
The backend needs to handle broadcasting updates to all clients whenever the order of items in a list is changed. Here’s how you can set up SignalR in the backend with .NET:
1. Create a Broadcast Hub
First, create a SignalR hub on the server side that extends Hub
class and notifies all connected clients.
using Microsoft.AspNetCore.SignalR;
public class CollaborationHub : Hub
{
public async Task PlanOrderUpdated(string username)
{
Log.Information($"{username} updated the plan order");
await Clients.All.SendAsync("PlanOrderUpdated", username);
}
}
The PlanOrderUpdated
method sends a notification with the username of the person who made the change to all connected clients.
2. Register SignalR
Once the hub is created, configure it in Program.cs
. Add SignalR to your services and map the hub route, naming the route (e.g., /CollaborationHub
), so that the frontend can connect to it.
// TODO1: Add SignalR
builder.Services.AddSignalR();
app.MapControllers();
// TODO2: Map and name the hub route
app.MapHub<CollaborationHub>("/CollaborationHub");
app.Run();
Client-Side Notification Mechanism in Angular
On the Angular frontend, we need to connect to the server hub and listen for updates from the SignalR hub to reflect changes in the UI, ensuring that the updated order is displayed in real-time for all users.
1. Install SignalR and Create Collaboration Service
First, install the SignalR client package for Angular and create a service to manage the real-time connection.
npm i @microsoft/signalr
ng g s collaboration
In the service, set up a connection to the SignalR hub and create methods:
- start(): Starts the hub connection.
- stop(): Stops the hub connection.
- on(): Listens for the event. The event name (e.g.,
PlanOrderUpdated
) should match the one defined on the server. - invoke(): Calls the method to trigger an event. The method name (e.g.,
PlanOrderUpdated
) should match the one defined on the server.
import * as signalR from '@microsoft/signalr';
export class CollaborationService {
private hubConnection = new signalR.HubConnectionBuilder()
.withUrl('http://localhost:5000/CollaborationHub', { withCredentials: false })
.build();
startConnection() {
return this.hubConnection.start();
}
stopConnection() {
return this.hubConnection.stop();
}
orderIndexUpdatedListener(handler: (username: string) => void) {
this.hubConnection.on('PlanOrderUpdated', (username: string) => handler(username));
}
orderIndexUpdated(username: string) {
this.hubConnection.invoke('PlanOrderUpdated', username);
}
}
2. Connect and Listen for Updates
In your Angular component, start the hub connection when the component loads in ngOnInit()
and register an event listener to receive updates on changes made by others.
export class PlanComponent implements OnInit {
constructor(private _coService: CollaborationService) {}
ngOnInit(): void {
this._coService.startConnection().then(() => {
this._coService.orderIndexUpdatedListener((username: string) => {
this.getExecList();
notify({message: `${username} just updated the plan order.`, type: "success", displayTime: 3000}, {position: "bottom center"});
});
});
}
ngOnDestroy() {
this._coService.stopConnection();
}
}
When the user leaves the component, ngOnDestroy()
is triggered to stop the hub connection.
Efficient Update Handling with Debouncing
To ensure that multiple updates aren’t processed in rapid succession (which can be overwhelming for the system), we can debounce updates to trigger only after a short delay. This technique improves performance and reduces unnecessary requests to the server.
1. Implement Debouncing for Order Updates
Using RxJS debounceTime()
, we can delay the sending of updates to the backend until the user has finished making changes. This ensures that updates are batched together, optimizing both the user experience and server load.
export class PlanComponent implements OnInit {
constructor(private _service: TaskService) {}
ngOnInit(): void {
this.onUpdateOrderIndex();
}
onUpdateOrderIndex() {
this._service.updateOrderIndex$.pipe(
debounceTime(2000),
map((tasks) => {
const updatedTasks = new Array<Task>();
tasks.forEach((task, i) => {
if(task.orderIndex != i) {
task.orderIndex = i;
updatedTasks.push(task);
}
});
return updatedTasks;
}),
filter((updatedTasks) => updatedTasks.length > 0),
switchMap((tasks) => this._service.updateOrderIndex(tasks))
).subscribe(() => {
this._coService.orderIndexUpdated(this.loginInfo.username);
});
}
}
2. Service to Handle Update Requests
Handle the HTTP request in the service to update the task order in the backend.
export class TaskService {
updateOrderIndex$ = new Subject<Task[]>();
constructor(private _http: HttpClient, private _env: EnvService) { }
updateOrderIndex(tasks: Task[]) {
return this._http.patch(`${this._env.APIOption.TaskEndpoint}/OrderIndex`, tasks);
}
}
Conclusion
With SignalR, updates are broadcast instantly to all connected users, ensuring that everyone is on the same page. By incorporating debouncing for handling rapid changes, you reduce server load and improve performance. This combination of real-time communication and efficient backend handling makes the task list update process seamless and user-friendly.