Integration with UI for ASP.NET Core

The UI for ASP.NET Core and UI for ASP.NET MVC suites provide a DataSourceRequest model binder and a ToDataSourceResult() method which process bulk data operations.

This article demonstrates how to set up a Grid to perform server-side CRUD operations by using these features in Angular applications and applying the Data Query helper functions.

Using with .NET Core

For a working example refer to the aspnetcore-data project on GitHub.

Using with .NET Web Api

For a working example refer to the aspnetwebapi-data project on GitHub.

Sample Integration Project

To implement the sample project:

  1. Install the prerequisites
  2. Follow the setup process

Prerequisites

Setup

  1. Create an ASP.NET Core web application that is integrated with Angular.

    As a starting point, the sample projects use the Angular project template with ASP.NET Core, which is recommended in the official Microsoft documentation.

  2. Install Telerik UI for ASP.NET Core. For more information, refer to the article on getting started.

    You can skip the UI-related steps because the sample project uses only DataSourceRequest and ToDataSourceResult() from the Kendo.Mvc.UI and Kendo.Mvc.Extensions namespaces.

  3. Create a local Blogging database as demonstrated in step 5 of the sample project.

  4. Based on your existing database, create the EF model by using the dotnet ef command-line tool. Once dotnet ef is installed, run the dotnet ef dbcontext scaffold "Server=(localdb)\mssqllocaldb;Database=Blogging;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -o Models command within the project root folder.
  5. Register the context with a dependency injection by using the database connection string in startup.cs.

    public void ConfigureServices(IServiceCollection services)
    {
       services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    
       // In production, the Angular files will be served from this directory.
       services.AddSpaStaticFiles(configuration =>
       {
           configuration.RootPath = "ClientApp/dist";
       });
    
       var connection = @"Data Source=(localdb)\mssqllocaldb;Initial Catalog=Blogging;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False";
       services.AddDbContext<BloggingContext>(options => options.UseSqlServer(connection));
    
    }
  6. If using a .Net Core controller, implement the CRUD endpoints in the BlogsController which will be called by the Grid on performing CRUD operations.

    The Read action utilizes the DataSourceRequest model binder and performs the data operations by using the ToDataSourceResult() method.

    using System.Linq;
    using Microsoft.AspNetCore.Mvc;
    using Kendo.Mvc.UI;
    using Kendo.Mvc.Extensions;
    using aspnetcore_data.Models;

    namespace aspnetcore_data.Controllers
    {
    public class BlogsController : Controller
    {
    private readonly BloggingContext _context;

       public BlogsController(BloggingContext context)
       {
    
           _context = context;
       }
    
       [Route(&quot;api/Blogs&quot;)]
       [HttpGet]
       public JsonResult GetProducts([DataSourceRequest] DataSourceRequest request)
       {
           var result = Json(this._context.Blog.ToDataSourceResult(request));
           return result;
       }
    
       [Route(&quot;api/Blogs&quot;)]
       [HttpPost]
       public JsonResult AddBlog([FromBody] Blog request)
       {
    
           this._context.Add(request);
           this._context.SaveChanges();
           var result = Json(this._context.Blog.Local);
           return result;
    
       }
    
       [Route(&quot;api/Blogs/{id}&quot;)]
       [HttpDelete]
       public JsonResult DeleteBlog([FromBody] Blog request)
       {
           this._context.Remove(request);
           this._context.SaveChanges();
           var result = Json(this._context.Blog.Local);
           return result;
       }
    
       [Route(&quot;api/Blogs/{id}&quot;)]
       [HttpPut]
    
       public JsonResult Editblog([FromBody] Blog request)
       {
    
           var existingblog = this._context.Blog.Where(b =&gt; b.BlogId == request.BlogId).FirstOrDefault&lt;Blog&gt;();
    
           if (existingblog != null)
           {
               existingblog.Url = request.Url;
           }
           this._context.SaveChanges();
    
           var result = Json(this._context.Blog.Local);
           return result;
       }
    

    }
    }

    In case .Net Web Api is used, the controller is automatically scaffolded by Visual Studio following the guideline on this article

    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.EntityFrameworkCore;
    using Kendo.Mvc.UI;
    using Kendo.Mvc.Extensions;
    
    namespace aspnetwebapi_data.Controllers
    {
       [Route("api/[controller]")]
       [ApiController]
       public class BlogsController : ControllerBase
       {
           private readonly BloggingContext _context;
    
           public BlogsController(BloggingContext context)
           {
               _context = context;
           }
    
           // GET: api/Blogs
           [HttpGet]
           public DataSourceResult GetProducts([DataSourceRequest] DataSourceRequest request)
           {
               var result = this._context.Blog.ToDataSourceResult(request);
               return result;
           }
    
           // GET: api/Blogs/5
           [HttpGet("{id}")]
           public async Task<ActionResult<Blog>> GetBlog(int id)
           {
               var blog = await _context.Blog.FindAsync(id);
    
               if (blog == null)
               {
                   return NotFound();
               }
    
               return blog;
           }
    
           // PUT: api/Blogs/5
           // To protect from overposting attacks, please enable the specific properties you want to bind to, for
           // more details see https://aka.ms/RazorPagesCRUD.
           [HttpPut("{id}")]
           public async Task<IActionResult> PutBlog(int id, Blog blog)
           {
               if (id != blog.BlogId)
               {
                   return BadRequest();
               }
    
               _context.Entry(blog).State = EntityState.Modified;
    
               try
               {
                   await _context.SaveChangesAsync();
               }
               catch (DbUpdateConcurrencyException)
               {
                   if (!BlogExists(id))
                   {
                       return NotFound();
                   }
                   else
                   {
                       throw;
                   }
               }
    
               return NoContent();
           }
    
           // POST: api/Blogs
           // To protect from overposting attacks, please enable the specific properties you want to bind to, for
           // more details see https://aka.ms/RazorPagesCRUD.
           [HttpPost]
           public async Task<ActionResult<Blog>> PostBlog(Blog blog)
           {
               _context.Blog.Add(blog);
               await _context.SaveChangesAsync();
    
               return CreatedAtAction("GetBlog", new { id = blog.BlogId }, blog);
           }
    
           // DELETE: api/Blogs/5
           [HttpDelete("{id}")]
           public async Task<ActionResult<Blog>> DeleteBlog(int id)
           {
               var blog = await _context.Blog.FindAsync(id);
               if (blog == null)
               {
                   return NotFound();
               }
    
               _context.Blog.Remove(blog);
               await _context.SaveChangesAsync();
    
               return blog;
           }
    
           private bool BlogExists(int id)
           {
               return _context.Blog.Any(e => e.BlogId == id);
           }
       }
    }
  7. Install the Data Query and Grid packages.

  8. Create an Angular service which will process the requests and parse the response by using the toDataSourceRequestString and translateDataSourceResultGroups Data Query methods.

    import { Injectable } from '@angular/core';
       import { HttpClient } from '@angular/common/http';
    
       import { Observable, BehaviorSubject } from 'rxjs';
       import { tap, map } from 'rxjs/operators';
    
       import { GridDataResult } from '@progress/kendo-angular-grid';
       import {
           State,
           toDataSourceRequestString,
           translateDataSourceResultGroups,
       } from '@progress/kendo-data-query';
    
       @Injectable()
       export class DataService extends BehaviorSubject<any[]> {
    
           constructor(private http: HttpClient) {
               super([]);
           }
    
           private BASE_URL = 'api/Blogs';
           private data: any[] = [];
           public state: State = {
               skip: 0,
               take: 5,
               filter: { filters: [], logic: 'or' },
               group: [],
               sort: []
           };
    
           public read() {
               if (this.data.length) {
                   return super.next(this.data);
               }
    
               this.fetch()
                   .pipe(
                       tap(data => {
                           this.data = data;
                       })
                   )
                   .subscribe(data => {
                       super.next(data);
                   });
           }
    
           public fetch(dataItem?: any, action: string = ''): Observable<any> {
    
               switch (action) {
                   case '': {
                       const queryStr = `${toDataSourceRequestString(this.state)}`;
                       const hasGroups = this.state.group && this.state.group.length;
    
                       return this.http.get(`${this.BASE_URL}?${queryStr}`).pipe(
                           // Process the response.
                           map(({ data, total }: GridDataResult): GridDataResult => {
                               return {
                                   data: hasGroups ? translateDataSourceResultGroups(data) : data,
                                   total: total
                               };
                           }
                           ));
                   }
                   case 'create': {
                       return this.http.post(`${this.BASE_URL}`, dataItem);
                   }
                   case 'edit': {
                       return this.http.put(`${this.BASE_URL}/${dataItem.blogId}`, dataItem);
                   }
                   case 'delete': {
                       const options = {
                           headers: {},
                           body: dataItem,
                       };
    
                       return this.http.delete(`${this.BASE_URL}/${dataItem.blogId}`, options);
                   }
               }
           }
    
           public save(dataItem: any, isNew?: boolean) {
               if (isNew) {
                   const newBlog = { Url: dataItem.url };
                   this.fetch(newBlog, 'create').subscribe(() => this.read(), () => this.read());
               } else {
                   this.fetch(dataItem, 'edit').subscribe(() => this.read(), () => this.read());
               }
           }
    
           public delete(dataItem: any) {
               this.fetch(dataItem, 'delete').subscribe(() => this.read(), () => this.read());
           }
       }
  9. Configure the Grid for editing, bind its data to the remote data source, and send the proper request on each (save) and (remove) event.

    <kendo-grid
               [data]="view | async"
               [pageSize]="state.take"
               [skip]="state.skip"
               [sort]="state.sort"
               [filter]="state.filter"
               [group]="state.group"
               [groupable]="true"
               [filterable]="true"
               [sortable]="true"
               [pageable]="true"
               [scrollable]="'scrollable'"
               (dataStateChange)="dataStateChange($event)"
               [height]="400"
               (edit)="editHandler($event)" (cancel)="cancelHandler($event)"
               (save)="saveHandler($event)" (remove)="removeHandler($event)"
               (add)="addHandler($event)">
    <ng-template kendoGridToolbarTemplate>
       <button kendoGridAddCommand>Add new</button>
    </ng-template>
    <kendo-grid-column field="blogId" title="Blog Id" [editable]="false" [filter]="'numeric'"></kendo-grid-column>
    <kendo-grid-column field="url" title="Blog URL"></kendo-grid-column>
    <kendo-grid-command-column title="command" width="220">
       <ng-template kendoGridCellTemplate let-isNew="isNew">
       <button kendoGridEditCommand [primary]="true">Edit</button>
       <button kendoGridRemoveCommand>Remove</button>
       <button kendoGridSaveCommand [disabled]="formGroup?.invalid">{{ isNew ? 'Add' : 'Update' }}</button>
       <button kendoGridCancelCommand>{{ isNew ? 'Discard changes' : 'Cancel' }}</button>
       </ng-template>
    </kendo-grid-command-column>
    </kendo-grid>
    
    ...
    
    import { Component, OnInit } from '@angular/core';
    import { FormGroup, FormControl } from '@angular/forms';
    import { Observable } from 'rxjs';
    
    import { DataStateChangeEvent } from '@progress/kendo-angular-grid';
    import { State } from '@progress/kendo-data-query';
    
    import { DataService } from './data.service';
    
    @Component({
       selector: 'app-fetch-data',
       templateUrl: './fetch-data.component.html'
    })
    export class FetchDataComponent implements OnInit {
       public view: Observable<any>;
       public formGroup: FormGroup;
       public state: State = {
           skip: 0,
           take: 5,
           filter: { filters: [], logic: 'or' },
           group: [],
           sort: []
       };
       private editedRowIndex: number;
    
       constructor(private dataService: DataService) {
    
       }
       public ngOnInit(): void {
           this.view = this.dataService;
           this.dataService.read();
       }
    
       public dataStateChange(state: DataStateChangeEvent): void {
           this.dataService.state = state;
           this.state = state;
           this.dataService.read();
       }
    
       public addHandler({ sender }) {
           this.closeEditor(sender);
    
           this.formGroup = new FormGroup({
               'blogId': new FormControl(),
               'url': new FormControl('')
           });
    
           sender.addRow(this.formGroup);
       }
    
       public editHandler({ sender, rowIndex, dataItem }) {
           this.closeEditor(sender);
    
           this.formGroup = new FormGroup({
               'blogId': new FormControl(dataItem.blogId),
               'url': new FormControl(dataItem.url)
           });
    
           this.editedRowIndex = rowIndex;
    
           sender.editRow(rowIndex, this.formGroup);
       }
    
       public cancelHandler({ sender, rowIndex }) {
           this.closeEditor(sender, rowIndex);
       }
    
       public saveHandler({ sender, rowIndex, formGroup, isNew }) {
           const blog: any = formGroup.value;
           this.dataService.save(blog, isNew);
           sender.closeRow(rowIndex);
       }
    
       public removeHandler({ dataItem }) {
           this.dataService.delete(dataItem);
       }
    
       private closeEditor(grid, rowIndex = this.editedRowIndex) {
           grid.closeRow(rowIndex);
           this.editedRowIndex = undefined;
           this.formGroup = undefined;
       }
    }

In this article