There was a need for using a Kendo Grid in an Angular 5 website where the backing store for the data was Elastic Search. Utilizing the filtering on local data was simple enough but for the needs of filtering there needed to be server side integration. The server was running ASP.NET Core.
To get started create a view and view model for Angular to expose the grid.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Component } from '@angular/core'; | |
import { process, State } from '@progress/kendo-data-query'; | |
import { sampleProducts } from './products'; | |
import { | |
GridComponent, | |
GridDataResult, | |
DataStateChangeEvent | |
} from '@progress/kendo-angular-grid'; | |
@Component({ | |
selector: 'my-app', | |
template: ` | |
<kendo-grid | |
[data]="gridData" | |
[pageSize]="state.take" | |
[skip]="state.skip" | |
[sort]="state.sort" | |
[filter]="state.filter" | |
[sortable]="true" | |
[pageable]="true" | |
[filterable]="true" | |
(dataStateChange)="dataStateChange($event)"> | |
<kendo-grid-column field="ProductID" title="ID" width="40" [filterable]="false"> | |
</kendo-grid-column> | |
<kendo-grid-column field="ProductName" title="Product Name"> | |
</kendo-grid-column> | |
<kendo-grid-column field="FirstOrderedOn" title="First Ordered On" width="240" filter="date" format="{0:d}"> | |
</kendo-grid-column> | |
<kendo-grid-column field="UnitPrice" title="Unit Price" width="180" filter="numeric" format="{0:c}"> | |
</kendo-grid-column> | |
<kendo-grid-column field="Discontinued" width="120" filter="boolean"> | |
<ng-template kendoGridCellTemplate let-dataItem> | |
<input type="checkbox" [checked]="dataItem.Discontinued" disabled/> | |
</ng-template> | |
</kendo-grid-column> | |
</kendo-grid> | |
` | |
}) | |
export class AppComponent { | |
public state: State = { | |
skip: 0, | |
take: 5, | |
// Initial filter descriptor | |
filter: { | |
logic: 'and', | |
filters: [{ field: 'ProductName', operator: 'contains', value: 'Chef' }] | |
} | |
}; | |
public gridData: GridDataResult = process(sampleProducts, this.state); | |
public dataStateChange(state: DataStateChangeEvent): void { | |
this.state = state; | |
this.gridData = process(sampleProducts, this.state); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { NgModule } from '@angular/core'; | |
import { BrowserModule } from '@angular/platform-browser'; | |
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; | |
import { HttpClientModule } from '@angular/common/http'; | |
import { GridModule } from '@progress/kendo-angular-grid'; | |
import { AppComponent } from './app.component'; | |
@NgModule({ | |
bootstrap: [ | |
AppComponent | |
], | |
declarations: [ | |
AppComponent | |
], | |
imports: [ | |
BrowserModule, | |
BrowserAnimationsModule, | |
HttpClientModule, | |
GridModule | |
] | |
}) | |
export class AppModule { } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { AppModule } from './app.module'; | |
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; | |
const platform = platformBrowserDynamic(); | |
platform.bootstrapModule(AppModule); |
To wire the view and view model to the server side data, there needs to be an Angular HTTP service and an ASP.NET Core Controller. The controller needs to be able to accept the filter and paging options of the grid as the user changes them and react to them server side. To accomplish this, some objects need to be created to handle the request:
First, the filter object, which is changed whenever a new filter is selected or is cleared; must be mapped to a C# object that can be serialized. The structure of the Kendo Grid filter is as such:
filter: {
logic: 'and',
filters: [{ field: 'ProductName', operator: 'contains', value: 'Chef' }]
}
To make that object transportable to C#, lets create a POCO:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class CompositeFilterDescriptor | |
{ | |
[JsonProperty("logic")] | |
public string Logic { get; set; } | |
[JsonProperty("filters")] | |
public ICollection<CompositeFilterDescriptor> Filters { get; set; } | |
[JsonProperty("field")] | |
public string Field { get; set; } | |
[JsonProperty("operator")] | |
public string Operator { get; set; } | |
[JsonProperty("value")] | |
public object Value { get; set; } | |
[JsonProperty("ignoreCase")] | |
public bool? IgnoreCase { get; set; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class Page | |
{ | |
public int Skip { get; set; } | |
public int Take { get; set; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class RequestDto | |
{ | |
public CompositeFilterDescriptor Filters { get; set; } | |
public Page Page { get; set; } | |
} |
Now lets create an ASP.NET Core controller endpoint for our Filter.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using System.Linq; | |
using System.Text; | |
using System.Threading.Tasks; | |
using Microsoft.AspNetCore.Authorization; | |
using Microsoft.AspNetCore.Mvc; | |
using Microsoft.EntityFrameworkCore; | |
using Nest; | |
public class GridController : Controller | |
{ | |
private readonly ElasticClient _elasticClient; | |
public GridController() | |
{ | |
//Initalize ElasticClient | |
} | |
[HttpPost] | |
public async Task<IActionResult> PostRequest([FromBody]RequestDto request) | |
{ | |
CompositeFilterMapper compositeFilterMapper = new CompositeFilterMapper(); | |
//Grab a page from elastic | |
selector = search => | |
search.Index("your-index") | |
.From(request.Page.Skip) | |
.Size(request.Page.Take) | |
.Query(q => | |
compositeFilterMapper.GetQueryContainer(q)); | |
var searchResponse = _elasticClient.Search(selector); | |
return Ok(new ResponseDto | |
{ | |
Results = searchResponse.Documents, | |
Count = searchResponse.Total | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class ResponseDto<T> | |
{ | |
public IEnumerable<T> Results { get; set; } | |
public long Count { get; set; } | |
} |
The only thing missing now is the query to work is the CompositeFilterMapper.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class CompositeFilterMapper<T> | |
{ | |
private readonly CompositeFilterDescriptor _compositeFilterDescriptor;= | |
public CompositeFilterMapper(CompositeFilterDescriptor compositeFilterDescriptor) | |
{ | |
_compositeFilterDescriptor = compositeFilterDescriptor; | |
} | |
public QueryContainer GetQueryContainer(QueryContainerDescriptor<ElasticDataUpload<T>> query) | |
{ | |
return ApplyQuery(new[] {_compositeFilterDescriptor}, _compositeFilterDescriptor.Logic, query); | |
} | |
private QueryContainerDescriptor<T> ApplyQuery( | |
IEnumerable<CompositeFilterDescriptor> filterFilters, string filterOperator, | |
QueryContainerDescriptor<T> query) | |
{ | |
List<QueryContainerDescriptor<T>> innerQueries = | |
new List<QueryContainerDescriptor<T>>(); | |
foreach (var filter in filterFilters) | |
{ | |
if (filter.Filters != null && filter.Filters.Any()) | |
{ | |
innerQueries.Add(ApplyQuery(filter.Filters, filter.Logic, query)); | |
} | |
else | |
{ | |
innerQueries.Add((QueryContainerDescriptor<T>)CreateQuery(filter.Field, filter.Operator, filter.IgnoreCase, filter.Logic, filter.Value, query)); | |
} | |
} | |
if (filterOperator == "and") | |
{ | |
return (QueryContainerDescriptor<T>)query.Bool(b => b.Must(innerQueries.Cast<QueryContainer>().ToArray())); | |
} | |
else if (filterOperator == "or") | |
{ | |
return (QueryContainerDescriptor<T>) query.Bool(b => b.Should(innerQueries.Cast<QueryContainer>().ToArray())); | |
} | |
else | |
{ | |
throw new ArgumentOutOfRangeException(); | |
} | |
} | |
private QueryContainer CreateQuery(string filterField, | |
string filterOperator,bool? filterIgnoreCase, string filterLogic, object value, | |
QueryContainerDescriptor<T> query) | |
{ | |
var type = MapToType(filterField); | |
var propertyExpression = MapToProperty(filterField); | |
return CreateQuery(query,type, propertyExpression, filterOperator, value); | |
} | |
private QueryContainer CreateQuery(QueryContainerDescriptor<T> query, | |
Type fieldType, Expression<Func<T, object>> propertyExpression, | |
string filterOperator, object value) | |
{ | |
if (fieldType == typeof(String)) | |
{ | |
return CreateStringFilter(query,propertyExpression, filterOperator, value); | |
} | |
else if (fieldType == typeof(DateTime)) | |
{ | |
return CreateDateTimeFilter(query, propertyExpression, filterOperator, value); | |
} | |
else | |
{ | |
throw new ArgumentOutOfRangeException(); | |
} | |
} | |
private QueryContainer CreateDateTimeFilter(QueryContainerDescriptor<ElasticDataUpload<SystemLog>> query, | |
Expression<Func<ElasticDataUpload<SystemLog>, object>> propertyExpression, string filterOperator, | |
object value) | |
{ | |
switch (filterOperator) | |
{ | |
case "eq": | |
return query.DateRange(d => d.Field(propertyExpression) | |
.GreaterThanOrEquals(DateMath.Anchored((DateTime)value)) | |
.LessThanOrEquals(DateMath.Anchored((DateTime)value))); | |
case "neq": | |
return query.DateRange(d => d.Field(propertyExpression) | |
.GreaterThanOrEquals(DateMath.Anchored((DateTime)value)) | |
.LessThanOrEquals(DateMath.Anchored((DateTime)value))); | |
case "isnull": | |
throw new ArgumentException("Unable to apply less than to date and time queries"); | |
case "isnotnull": | |
throw new ArgumentException("Unable to apply less than to date and time queries"); | |
case "lt": | |
return query.DateRange(d => d.Field(propertyExpression).LessThan(DateMath.Anchored((DateTime)value))); | |
case "lte": | |
return query.DateRange(d => d.Field(propertyExpression).LessThanOrEquals(DateMath.Anchored((DateTime)value))); | |
case "gt": | |
return query.DateRange(d => d.Field(propertyExpression).GreaterThan(DateMath.Anchored((DateTime)value))); | |
case "gte": | |
return query.DateRange(d => d.Field(propertyExpression).GreaterThanOrEquals(DateMath.Anchored((DateTime)value))); | |
case "startswith": | |
throw new ArgumentException("Unable to apply less than to date and time queries"); | |
case "endswith": | |
throw new ArgumentException("Unable to apply less than to date and time queries"); | |
case "contains": | |
throw new ArgumentException("Unable to apply less than to date and time queries"); | |
case "doesnotcontain": | |
throw new ArgumentException("Unable to apply less than to date and time queries"); | |
case "isempty": | |
throw new ArgumentException("Unable to apply less than to date and time queries"); | |
case "isnotempty": | |
throw new ArgumentException("Unable to apply less than to date and time queries"); | |
default: | |
throw new ArgumentOutOfRangeException(); | |
} | |
} | |
private QueryContainer CreateStringFilter(QueryContainerDescriptor<T> query, | |
Expression<Func<T, object>> propertyExpression, | |
string filterOperator, object value) | |
{ | |
switch (filterOperator) | |
{ | |
case "eq": | |
return query.QueryString(qs => qs.Fields(fs => fs.Field(propertyExpression)).Query((string)value)); | |
case "neq": | |
return query.Bool(b => b.MustNot(x => x.QueryString(qs => qs.Fields(fs => fs.Field(propertyExpression)).Query((string)value)))); | |
case "isnull": | |
return query.QueryString(qs => qs.Fields(fs => fs.Field(propertyExpression)).Query(null)); | |
case "isnotnull": | |
return query.Bool(b => b.MustNot(x => x.QueryString(qs => qs.Fields(fs => fs.Field(propertyExpression)).Query(null)))); | |
case "lt": | |
throw new ArgumentException("Unable to apply less than to string queries"); | |
case "lte": | |
throw new ArgumentException("Unable to apply less than equal to string queries"); | |
case "gt": | |
throw new ArgumentException("Unable to apply greater than to string queries"); | |
case "gte": | |
throw new ArgumentException("Unable to apply greater than equal to string queries"); | |
case "startswith": | |
return query.Bool(b => b.Should(x => x.Term(propertyExpression, value))); | |
case "endswith": | |
return query.Bool(b => b.Should(x => x.Term(propertyExpression, value))); | |
case "contains": | |
return query.Match(qs => qs.Field(propertyExpression).Query((string)value).Operator(Operator.And)); | |
case "doesnotcontain": | |
return query.Bool(b => b.MustNot(x => x.Match(qs => qs.Field(propertyExpression).Query((string)value).Operator(Operator.And)))); | |
case "isempty": | |
return query.QueryString(qs => qs.Fields(fs => fs.Field(propertyExpression)).Query("")); | |
case "isnotempty": | |
return query.Bool(b => b.MustNot(x => x.QueryString(qs => qs.Fields(fs => fs.Field(propertyExpression)).Query("")))); | |
default: | |
throw new ArgumentOutOfRangeException(); | |
} | |
} | |
private System.Linq.Expressions.Expression<Func<T, object>> MapToProperty(string fieldName) | |
{ | |
switch (fieldName) | |
{ | |
//return expressions from property names | |
default: | |
throw new ArgumentOutOfRangeException(); | |
} | |
} | |
private Type MapToType(string fieldName) | |
{ | |
switch (fieldName) | |
{ | |
// Map field name to type | |
// return typeof(String); | |
default: | |
throw new ArgumentOutOfRangeException(); | |
} | |
} | |
} |
You will need to build in your own express and type mapping for properties, but otherwise this is built for Strings and DateTimes. From this base you should be able to implement different types and queries you would need for the Kendo Grid to work with ElasticSearch.