New to Kendo UI for Angular? Start a free 30-day trial

Consuming SignalR Events in the Grid

Environment

ProductProgress® 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

You can use ASP.NET Core SignalR for pushing events from the server into the client application.

This sample application features a client-side Todo project with Angular and a Kendo UI for Angular Grid which consumes events from an ASP.NET Core SignalR backend. When the user adds a new item or updates an existing one, the data updates across multiple clients simultaneously.

Setting Up the Server

  1. Create the Hub and the Controller.

        using System.Threading.Tasks;
        using Microsoft.AspNetCore.SignalR;
        using server.Controllers;
    
        namespace server.Hubs
        {
            public class TodoHub : Hub
            {
    
            }
        }
        using 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; }
            }
        }
  2. Configure the pipeline for the HTTP request.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.HttpsPolicy;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Options;
    using server.Hubs;
    
    namespace server
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            public IConfiguration Configuration { get; }
    
            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
                services.AddCors(options => options.AddPolicy("CorsPolicy",
                    builder =>
                    {
                        builder.AllowAnyMethod().AllowAnyHeader()
                            .AllowAnyOrigin()
                            .AllowCredentials();
                    }));
                services.AddSignalR();
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                else
                {
                    app.UseHsts();
                }
    
                app.UseCors("CorsPolicy");
                app.UseHttpsRedirection();
                app.UseSignalR(routes =>
                {
                    routes.MapHub<TodoHub>("/todohub");
                });
                app.UseMvc();
            }
        }
    }

Setting Up the Client

  1. Create an Angular service that will handle the communication with the server.

    import { Injectable } from '@angular/core';
    import { HubConnection, HubConnectionBuilder } from '@aspnet/signalr';
    import { Subject } from 'rxjs';
    import { Todo } from '../models/todo.model';
    
    @Injectable({
    providedIn: 'root'
    })
    export class SignalRService {
    private connection: HubConnection;
    itemUpdated: Subject<Todo> = new Subject<Todo>();
    itemAdded: Subject<Todo> = new Subject<Todo>();
    
    constructor() {
        this.connection = new HubConnectionBuilder()
        .withUrl('https://localhost:5001/todohub')
        .build();
        this.registerOnEvents();
        this.connection.start().catch(err => console.log(err.toString()));
    }
    
    registerOnEvents() {
        this.connection.on('itemAdded', item => {
        console.log('itemAdded');
        this.itemAdded.next(item);
        });
    
        this.connection.on('itemUpdated', item => {
        console.log('itemUpdated');
        this.itemUpdated.next(item);
        });
    }
    }
  2. Hook the host Grid component so that it consumes the service and the SignalR backend.

    constructor(
        private readonly signalrService: SignalRService,
        private readonly http: HttpClient
    ) {
        signalrService.itemAdded.subscribe(item => {
        this.items = [item, ...this.items];
        });
        signalrService.itemUpdated.subscribe(item => {
        this.items = this.items.filter(x => x.id !== item.id);
        this.items = [item, ...this.items];
        });
    }
    ngOnInit() {
        this.http
        .get<Todo[]>('https://localhost:5001/api/todos/')
        .subscribe(items => {
            this.items = items;
        });
    
        this.form = new FormGroup({
        todoValue: new FormControl('', Validators.required)
        });
    }
    
    addTodo() {
        const toSend = { value: this.form.value.todoValue };
    
        this.http
        .post('https://localhost:5001/api/todos/', toSend)
        .subscribe(() => console.log('added'));
    
        this.form.reset();
    }
    
    markAsDone(item: Todo) {
        item.done = true;
        this.http
        .put('https://localhost:5001/api/todos/' + item.id, item)
        .subscribe(() => console.log('updated'));
    }
  3. Bind the Grid.

    <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>