Consuming SignalR Events in the Grid
Environment
| Product | Progress® Kendo UI Grid |
Description
How can I bind the Kendo UI for Angular Grid to a SignalR hub to consume the events which are pushed from the backend?
Solution
To consume SignalR events in the Grid, you need to set up a SignalR hub on the server that will push events to the clients. Then, on the client side, you can create an Angular service that connects to the SignalR hub and listens for events. Finally, you can bind the Grid to the data received from the SignalR events. We suggest visiting the official Microsoft documentation on ASP.NET Core SignalR for detailed information on setting up the SignalR hub and client.
Setting Up the Server
-
Create the
Huband theController.csusing System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; using server.Controllers; namespace server.Hubs { public class TodoHub : Hub { } }csusing System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using server.Hubs; namespace server.Controllers { [Route("api/[controller]")] [ApiController] public class TodosController : ControllerBase { private ConcurrentDictionary<Guid, TodoItem> _store = new ConcurrentDictionary<Guid, TodoItem>(); private readonly IHubContext<TodoHub> _todoHubContext; public TodosController(IHubContext<TodoHub> todoHubContext) { _todoHubContext = todoHubContext; } [HttpGet] public ActionResult<IEnumerable<TodoItem>> Get() { return Ok(_store.Values); } [HttpPost] public ActionResult<TodoItem> Post([FromBody] TodoItem todoItem) { Guid key = Guid.NewGuid(); todoItem.Id = key; _store.TryAdd(key, todoItem); _todoHubContext.Clients.All.SendAsync("itemAdded", todoItem); return Ok(todoItem); } [HttpPut("{id}")] public ActionResult<TodoItem> Put(Guid id, [FromBody] TodoItem todoItem) { _store.TryGetValue(todoItem.Id, out TodoItem oldValue); _store.TryUpdate(todoItem.Id, todoItem, oldValue); _todoHubContext.Clients.All.SendAsync("itemUpdated", todoItem); return Ok(todoItem); } } public class TodoItem { public Guid Id { get; set; } public string Value { get; set; } public bool Done { get; set; } } } -
Configure the pipeline for the HTTP request.
csusing server.Hubs; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddSignalR(); builder.Services.AddCors(options => { options.AddPolicy("CorsPolicy", policy => { policy.WithOrigins("http://localhost:4200") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); }); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseHsts(); } app.UseHttpsRedirection(); app.UseRouting(); // UseCors must be called before MapHub app.UseCors("CorsPolicy"); app.UseAuthorization(); app.MapControllers(); app.MapHub<TodoHub>("/todohub"); app.Run();
Setting Up the Client
-
Install the SignalR client package by running
npm install @microsoft/signalr. -
Create an Angular service that will handle the communication with the server.
tsimport { Injectable, OnDestroy } from '@angular/core'; import { HubConnection, HubConnectionBuilder, HubConnectionState } from '@microsoft/signalr'; import { Subject } from 'rxjs'; import { Todo } from '../models/todo.model'; @Injectable({ providedIn: 'root' }) export class SignalRService implements OnDestroy { private connection: HubConnection; public itemUpdated: Subject<Todo> = new Subject<Todo>(); public itemAdded: Subject<Todo> = new Subject<Todo>(); constructor() { this.connection = new HubConnectionBuilder() .withUrl('https://localhost:5001/todohub') .withAutomaticReconnect() .build(); this.registerOnEvents(); this.startConnection(); } private async startConnection(): Promise<void> { try { await this.connection.start(); console.log('SignalR connection established'); } catch (err) { console.error('Error establishing SignalR connection:', err); // Retry logic is handled by withAutomaticReconnect() } } private registerOnEvents(): void { this.connection.on('itemAdded', (item: Todo) => { console.log('itemAdded'); this.itemAdded.next(item); }); this.connection.on('itemUpdated', (item: Todo) => { console.log('itemUpdated'); this.itemUpdated.next(item); }); this.connection.onclose((error) => { console.log('SignalR connection closed', error); }); this.connection.onreconnecting((error) => { console.log('SignalR reconnecting', error); }); this.connection.onreconnected((connectionId) => { console.log('SignalR reconnected', connectionId); }); } public ngOnDestroy(): void { if (this.connection.state === HubConnectionState.Connected) { this.connection.stop(); } } } -
Hook the host Grid component so that it consumes the service and the SignalR backend.
tsimport { Component, OnInit, OnDestroy } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { FormGroup, FormControl, Validators } from '@angular/forms'; import { Subscription } from 'rxjs'; import { SignalRService } from './signalr.service'; import { Todo } from './models/todo.model'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit, OnDestroy { public items: Todo[] = []; public form: FormGroup; private subscriptions = new Subscription(); constructor( private readonly signalrService: SignalRService, private readonly http: HttpClient ) { this.subscriptions.add( this.signalrService.itemAdded.subscribe(item => { this.items = [item, ...this.items]; }) ); this.subscriptions.add( this.signalrService.itemUpdated.subscribe(item => { this.items = this.items.filter(x => x.id !== item.id); this.items = [item, ...this.items]; }) ); } ngOnInit(): void { this.http .get<Todo[]>('https://localhost:5001/api/todos/') .subscribe({ next: (items) => { this.items = items; }, error: (err) => console.error('Error loading todos:', err) }); this.form = new FormGroup({ todoValue: new FormControl('', Validators.required) }); } addTodo(): void { const toSend = { value: this.form.value.todoValue }; this.http .post('https://localhost:5001/api/todos/', toSend) .subscribe({ next: () => console.log('Todo added'), error: (err) => console.error('Error adding todo:', err) }); this.form.reset(); } markAsDone(item: Todo): void { item.done = true; this.http .put('https://localhost:5001/api/todos/' + item.id, item) .subscribe({ next: () => console.log('Todo updated'), error: (err) => console.error('Error updating todo:', err) }); } ngOnDestroy(): void { this.subscriptions.unsubscribe(); } } -
Bind the Grid.
html<form (ngSubmit)="addTodo()" [formGroup]="form"> <kendo-textbox formControlName="todoValue"></kendo-textbox> <button [disabled]="form.invalid">Add Todo</button> </form> <kendo-grid [kendoGridBinding]="items" [pageable]="true" [pageSize]="5" [filterable]="true" [sortable]="true"> <kendo-grid-column field="id" [width]="350" title="ID"></kendo-grid-column> <kendo-grid-column field="value" title="Task"></kendo-grid-column> <kendo-grid-column field="done" filter="boolean" title="Status" [width]="200"> <ng-template kendoGridCellTemplate let-dataItem> <input type="checkbox" kendoCheckBox disabled="disabled" checked="{{ dataItem.done ? 'checked': undefined }}" /> </ng-template> </kendo-grid-column> <kendo-grid-column> <ng-template kendoGridCellTemplate let-dataItem> <button class="k-button k-primary" [disabled]="dataItem.done" (click)="markAsDone(dataItem)">Mark as done</button> </ng-template> </kendo-grid-column> </kendo-grid>