We use Kendo-UI in a project we are developing at work and it’s great if you don’t have to customize it a lot. Otherwise I’m pretty sure you will encounter many strange problems, like me. My current tasks connected with UI layer of the application so I’m struggling with many different problems. Today I’ll describe one of them and present my own solution.
The problem I had is related to Kendo Grid control. Grids are present in almost every business application. Needless to say, it’s possible that you will encounter it once if you will have to use Kendo as well. Let’s go to the details then.
Assume we are building ASP.NET MVC application to manage people. Our data context contains a collection of people. Each person (person is our only entity) has two properties: first name and last name. Now we had been given a task to prepare a grid which will present people from data context. One of the requirements is to list them in one column called Full Name in a form: “last name first name”, like this: Kęstowicz Tymoteusz.
Task described above seems to be pretty easy and that is true. Unfortunately, there is a problem under cover but I’ll prepare backend of the application first and point the problem afterwards.
Model and DataContext:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
namespace KendoHelpersSampleApp.Models { public class Person { public string FirstName { get; set; } public string LastName { get; set; } } } using System.Collections.Generic; using System.Web.Script.Serialization; namespace KendoHelpersSampleApp.Models { public class DataContext { private IEnumerable<Person> people = new List<Person>(); public DataContext() { PrepareSamplePeopleData(); } private void PrepareSamplePeopleData() { const string json = @"It's too long and I've removed it from the post"; var serializer = new JavaScriptSerializer(); people = serializer.Deserialize<List<Person>>(json); } public IEnumerable<Person> People { get { return people; } } } } |
ViewModel:
1 2 3 4 5 6 7 8 9 10 11 |
namespace KendoHelpersSampleApp.ViewModels { public class IndexViewModel { public string FirstName { get; set; } public string LastName { get; set; } public string FullName { get { return string.Format("{0} {1}", LastName, FirstName); } } } } |
Query object:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using System.Collections.Generic; using System.Linq; using KendoHelpersSampleApp.Models; using KendoHelpersSampleApp.ViewModels; namespace KendoHelpersSampleApp.QueryObjects { public static class PeopleExtensions { public static IEnumerable<IndexViewModel> FindAllPeople(this DataContext dataContext) { return dataContext.People.Select(person => new IndexViewModel() { FirstName = person.FirstName, LastName = person.LastName }); } } |
People Controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
using System.Web.Mvc; using Kendo.Mvc.Extensions; using Kendo.Mvc.UI; using KendoHelpers; using KendoHelpersSampleApp.Models; using KendoHelpersSampleApp.QueryObjects; namespace KendoHelpersSampleApp.Controllers { public class PeopleDataController : Controller { readonly DataContext dataContext = new DataContext(); public ActionResult ReadPeople([DataSourceRequest] DataSourceRequest request) { return Json(dataContext.FindAllPeople().ToDataSourceResult(request)); } } } |
View:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
using Kendo.Mvc.UI @using KendoHelpersSampleApp.ViewModels @{ ViewBag.Title = "Home Page"; } <div class="row"> <div class="col-md-12"> <h2>People</h2> @(Html.Kendo().Grid<IndexViewModel>() .Name("PeopleGrid") .Columns(columns => { columns.Bound(p => p.FullName).Title("Full name").Width(180); }) .Groupable() .Pageable(pager => pager.PageSizes(new[] { 10, 15, 25 })) .Sortable() .Scrollable(container => container.Height("auto")) .Filterable() .DataSource(dataSource => dataSource .Ajax() .Read("ReadPeople", "PeopleData") .PageSize(15) )) </div> </div> |
I put only the most important parts of the application above. Full example is available on my github account. You can get it from here: https://github.com/tkestowicz/KendoHelpers.
When you run the code you should see the result above. The grid should be populated with data. It looks fine so where is the problem? The problem is with built-in functionalities provided by Kendo when you use LINQ to Entities. The current example uses LINQ to Objects but if you will try to use grouping, sorting or filtering then the following exception will be thrown:
Screenshot is taken from the project I’m working at.
It’s because of the difference between model and view model. Kendo uses the property from ViewModel, FullName in this case, to build specific transformations of the data. However FullName property is not present in the model and Kendo engine can’t figure how to map selected view-model property to the model so it throws an exception.
The problem is recognized, so good so far. Now we need to solve it by changing default mapping generated by Kendo engine. I’ve implemented three generic helpers, one per functionality:
Sorts mapping helper:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
using System.Collections.Generic; using System.Linq; using Kendo.Mvc; using Kendo.Mvc.UI; namespace KendoHelpers { public static class SortAttributesHelper { public static DataSourceRequest SortAttributesMapping(this DataSourceRequest request, IDictionary<string, string[]> mappings) { return request.SelectAttributesToReplace(mappings).ReplaceMappings(request, mappings); } private static void ApplyNewMapping(this SortDescriptor oldMapping, DataSourceRequest request, IDictionary<string, string[]> mappings) { mappings.NewMappings(oldMapping).ToList().ForEach(newMapping => request.Sorts.Add(PrepareDescriptor(newMapping, oldMapping))); } private static DataSourceRequest ReplaceMappings(this IEnumerable<SortDescriptor> attributesToReplace, DataSourceRequest request, IDictionary<string, string[]> mappings) { attributesToReplace.ToList().ForEach(oldMapping => oldMapping.RemoveOldMapping(request).ApplyNewMapping(request, mappings) ); return request; } private static SortDescriptor RemoveOldMapping(this SortDescriptor oldMapping, DataSourceRequest request) { request.Sorts.Remove(oldMapping); return oldMapping; } private static SortDescriptor PrepareDescriptor(string newMapping, SortDescriptor oldMapping) { return new SortDescriptor(newMapping, oldMapping.SortDirection); } private static IEnumerable<SortDescriptor> SelectAttributesToReplace(this DataSourceRequest request, IDictionary<string, string[]> mappings) { return request.Sorts.Where(sort => mappings.ContainsKey(sort.Member)).ToList(); } private static IEnumerable<string> NewMappings(this IDictionary<string, string[]> mappings, SortDescriptor oldMapping) { return mappings[oldMapping.Member]; } } } |
Groups mapping helper:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
using System.Collections.Generic; using System.Linq; using Kendo.Mvc; using Kendo.Mvc.Extensions; using Kendo.Mvc.UI; using WebGrease.Css.Extensions; namespace KendoHelpers { public static class GroupAttributesHelper { public static DataSourceRequest GroupAttributesMapping(this DataSourceRequest request, IDictionary<string, string[]> mappings) { return request.SelectAttributesToReplace(mappings).ReplaceMappings(request, mappings); } private static DataSourceRequest ReplaceMappings(this IEnumerable<GroupDescriptor> attributesToReplace, DataSourceRequest request, IDictionary<string, string[]> mappings) { attributesToReplace.ForEach(oldMapping => oldMapping.RemoveOldMapping(request).ApplyNewMapping(request, mappings) ); return request; } private static void ApplyNewMapping(this GroupDescriptor oldMapping, DataSourceRequest request, IDictionary<string, string[]> mappings) { mappings.NewMappings(oldMapping).ForEach(newMapping => request.Groups.Add(PrepareDescriptor(newMapping, oldMapping))); } private static GroupDescriptor PrepareDescriptor(string newMapping, GroupDescriptor oldMapping) { var obj = new GroupDescriptor() { Member = newMapping, SortDirection = oldMapping.SortDirection, DisplayContent = oldMapping.DisplayContent }; obj.AggregateFunctions.AddRange(oldMapping.AggregateFunctions); return obj; } private static GroupDescriptor RemoveOldMapping(this GroupDescriptor oldMapping, DataSourceRequest request) { request.Groups.Remove(oldMapping); return oldMapping; } private static IEnumerable<GroupDescriptor> SelectAttributesToReplace(this DataSourceRequest request, IDictionary<string, string[]> mappings) { return request.Groups.Where(group => mappings.ContainsKey(group.Member)).ToList(); } private static IEnumerable<string> NewMappings(this IDictionary<string, string[]> mappings, GroupDescriptor oldMapping) { return mappings[oldMapping.Member]; } } } |
Filters mapping helper:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
using System.Collections.Generic; using System.Linq; using Kendo.Mvc; using Kendo.Mvc.UI; namespace KendoHelpers { public static class FilterAttributesHelper { public static DataSourceRequest FilterAttributesMapping(this DataSourceRequest request, IDictionary<string, string> mappings) { return request.SelectAttributesToReplace(mappings).ReplaceMappings(request, mappings); } private static DataSourceRequest ReplaceMappings(this IEnumerable<FilterDescriptor> attributesToReplace, DataSourceRequest request, IDictionary<string, string> mappings) { attributesToReplace.ToList().ForEach(oldMapping => oldMapping.RemoveOldMapping(request).ApplyNewMapping(request, mappings) ); return request; } private static void ApplyNewMapping(this FilterDescriptor oldMapping, DataSourceRequest request, IDictionary<string, string> mappings) { request.Filters.Add(PrepareDescriptor(mappings.NewMapping(oldMapping), oldMapping)); } private static FilterDescriptor PrepareDescriptor(string newMapping, FilterDescriptor oldMapping) { return new FilterDescriptor() { Member = newMapping, Operator = oldMapping.Operator, Value = oldMapping.Value }; } private static IEnumerable<FilterDescriptor> SelectAttributesToReplace(this DataSourceRequest request, IDictionary<string, string> mappings) { return request.Filters.Select(filter => filter as FilterDescriptor ?? new FilterDescriptor()).Where(filter => mappings.ContainsKey(filter.Member)).ToList(); } private static FilterDescriptor RemoveOldMapping(this FilterDescriptor oldMapping, DataSourceRequest request) { request.Filters.Remove(oldMapping); return oldMapping; } private static string NewMapping(this IDictionary<string, string> mappings, FilterDescriptor oldMapping) { return mappings[oldMapping.Member]; } } } |
People controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
using System.Web.Mvc; using Kendo.Mvc.Extensions; using Kendo.Mvc.UI; using KendoHelpers; using KendoHelpersSampleApp.Models; using KendoHelpersSampleApp.QueryObjects; namespace KendoHelpersSampleApp.Controllers { public class PeopleDataController : Controller { readonly DataContext dataContext = new DataContext(); public ActionResult ReadPeople([DataSourceRequest] DataSourceRequest request) { return Json(dataContext.FindAllPeople().ToDataSourceResult( request.SortAttributesMapping(PeopleExtensions.SortMappings) .GroupAttributesMapping(PeopleExtensions.GroupMappings) .FilterAttributesMapping(PeopleExtensions.FilterMappings)) ); } } } |
Query object:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
using System.Collections.Generic; using System.Linq; using KendoHelpersSampleApp.Models; using KendoHelpersSampleApp.ViewModels; namespace KendoHelpersSampleApp.QueryObjects { public static class PeopleExtensions { private static readonly IDictionary<string, string[]> sortMappings; private static readonly IDictionary<string, string[]> groupMappings; private static readonly IDictionary<string, string> filterMappings; static PeopleExtensions() { sortMappings = new Dictionary<string, string[]>() { {"FullName", new []{"LastName", "FirstName"}} }; groupMappings = new Dictionary<string, string[]>() { {"FullName", new []{"LastName"}} }; filterMappings = new Dictionary<string, string>() { {"FullName", "LastName"} }; } public static IEnumerable<IndexViewModel> FindAllPeople(this DataContext dataContext) { return dataContext.People.Select(person => new IndexViewModel() { FirstName = person.FirstName, LastName = person.LastName }); } public static IDictionary<string, string[]> SortMappings { get { return sortMappings; } } public static IDictionary<string, string[]> GroupMappings { get { return groupMappings; } } public static IDictionary<string, string> FilterMappings { get { return filterMappings; } } } } |
View:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
@using Kendo.Mvc.UI @using KendoHelpersSampleApp.ViewModels @{ ViewBag.Title = "Home Page"; } <div class="row"> <div class="col-md-12"> <h2>People</h2> @(Html.Kendo().Grid<IndexViewModel>() .Name("PeopleGrid") .Columns(columns => { columns.Bound(p => p.FullName).Title("Full name").Width(180); columns.Bound(p => p.LastName).Title("Last name").Hidden(true); // Hidden column is nescessary to display label for full name group. //Name of the property will be displayed if you will remove it. }) .Groupable() .Pageable(pager => pager.PageSizes(new[] { 10, 15, 25 })) .Sortable() .Scrollable(container => container.Height("auto")) .Filterable() .DataSource(dataSource => dataSource .Ajax() .Read("ReadPeople", "PeopleData") .PageSize(15) )) </div> </div> |
And the result (grouping example):
It’s not complex but two things have to be pointed. When you look at grouping helper you will see that I had to add a slight modification into the view as well. I don’ know why Kendo is losing label of the column being used for grouping and it displays name of the property by default. Hidden column is a simple workaround for this.
Second thing is related with filters helper. As you may see sort and group helpers allow to replace one field with multiple fields. It’s reasonable because we often use more than one column to group or sort the data. In other side when you look at filters helpers you will see that it is forbidden to use multiple columns as a new mapping. It is not a mistake, it is a conscious decision. Filters from the collection are applied by Kendo with AND condition instead of OR. In this case ability of using multiple columns wasn’t a good choice because operators like StartWith or EndWith were useless. I know that filter helper is not perfect and it does not work well in specific cases but I’ve tried my best.
It had taken me some time before I found and implemented the solution. I’ve tested it and it works on my machine. Feel free to use it in your projects if you need to. If know better solution for the problem described here just let me know in comments.
Don’t use kendo helpers. They a totally useless. They have bugs and they’re not 100% compatible with js version. We’ve been struggling with it a long time and after moving to js is a different story
Thanks for the advice, I’ll check this approach.
So much code for so simple problem :). Add DTO classe to your solution – for example
class PersionDTO{public string FirstName { get; set; }public string LastName { get; set; } public string FullName{get;set;}}. Add helper method to your data context which return DTO object and as next step use automapper to map model to dto.
Btw it is not kendo problem – every grid will have exactly the same problem…
The particular case I presented is simple but the solution is generic and works with more complex scenarios as well (e.g. more columns). I don’t get your point, I have separation like DTO within ViewModel class and instead of automapper I map the data manually and it is ok. Internal behaviour of KendoUI grid control is the problem. I know that other grids might also have this issue but… clients pay a lot of money for Kendo and in my opinion that feature should be implemented out of the box. I’m sure I’m not the only one who had to deal with the decribed problem.
Grouping is useless in any biz scenario. 🙂
Paging is a data virtualization for poor.