Harness the Power of ASP.NET MVC, Web Api, OData, Kendo UI & RequireJS to Build an Easy & Maintainable SPA (for the .NET Developer) – Published

Apologize for the delay, the original article: “Harness the Power of ASP.NET MVC, Web Api, OData, Kendo UI & RequireJS to Build an Easy & Maintainable SPA (for the .NET Developer)” has now been published as “A .NET Developer Primer to Single-Page Applications (SPA)“.

Download: https://lelong37.files.wordpress.com/2014/03/msdn-mag-1403.pdf
Online: http://msdn.microsoft.com/en-us/magazine/dn605877.aspx
Source code: https://easyspa.codeplex.com
Live Demo: http://easyspa.azurewebsites.net/home/spa#/customer/index

3-4-2014 1-36-58 AM

Telerik’s HTML5 Kendo UI Grid with Server Side Paging, Sorting & Filtering with MVC3, EF4 & Dynamic LINQ

Update: 06/18/2013 – It is recommended that you follow this post for Kendo UI Grid, Datasource filtering http://blog.longle.net/2013/06/18/mvc-4-web-api-odata-entity-framework-kendo-ui-grid-datasource-with-mvvm/

Update: 05/11/2012 – Added support for querying objects with child properties

Update: 04/24/2012 – Added recursion to the filters processing to support multiple search criterias on the same field while at the same time supporting searches across multiple fields.

I recently did a post Telerik’s HTML5 Kendo UI (Grid, Detail Template, TabStrip) which illustrated how to wire up their HTML5 Grid and handle server side paging. After doing so I quickly found myself needing to wire up the rest of server side bells and whistles e.g. sorting, filtering, etc.. Did some relentless googling and didn’t find any good resources on how to do this with MVC3 and EF4 so hence this blog post for the those of us that are doing just that. Rather than starting from scratch I’ll go ahead and continue where the my last blog left off.

So this first thing we need to do is configure our Kendo UI Grid for to do server side sorting and filtering so that we decompose what the requests pay loads look like coming from the Grid when performing these types of actions on it.

Configuring the Kendo UI Grid:


    $(document).ready(function () {
        var grid = $("#grid").kendoGrid({
            dataSource: {
                type: "json",
                serverPaging: true,
                serverSorting: true,
                serverFiltering: true,
                allowUnsort: true,
                pageSize: 5,
                transport: {
                    read: {
                        url: "Products/GetAll",
                        dataType: "json",
                        type: "POST",
                        contentType: "application/json; charset=utf-8",
                        data: {}
                    },
                    parameterMap: function (options) {
                        return JSON.stringify(options);
                    }
                },
                schema: {
                    model: {
                        fields: {
                            ProductId: { type: "number" },
                            Name: { type: "string" },
                            Status: { type: "string" },
                            Created: { type: "date" }
                        }
                    },
                    data: "Products",
                    total: "TotalCount"
                }
            },
            height: 700,
            sortable: true,
            groupable: true,
            pageable: true,
            filterable: true,
            columns: [
                    { field: "ProductId", title: "ProductId" },
                    { field: "ProductType", title: "ProductType" },
                    { field: "Name", title: "Name" },
                    { field: "Created", title: "Created", format: "{0:MM/dd/yyyy}" }
                ],
            detailTemplate: kendo.template($("#template").html()),
            toolbar: kendo.template($("#toolBarTemplate").html()),
            detailInit: detailInit,
            dataBound: function () {
                this.expandRow(this.tbody.find("tr.k-master-row").first());
            }
        });

        var dropDown = grid.find("#requestType").kendoDropDownList({
            dataTextField: "text",
            dataValueField: "value",
            autoBind: false,
            optionLabel: "All",
            dataSource: [
                    { text: "Electronics", value: "2" },
                    { text: "Machinery", value: "1" }
                ],
            change: function () {
                var value = this.value();
                if (value) {
                    grid.data("kendoGrid").dataSource.filter(
                        { field: "ProductType", operator: "eq", value: parseInt(value) });
                } else {
                    grid.data("kendoGrid").dataSource.filter({});
                }
            }
        });
    });

I’ve highlighted some of the major changes we made to our configuration which include setting up the Grid for server side actions: paging, sorting, filter, unsort and surfacing the filteration capabilities to the UI. Lines 54-72 is for setting up a Grid Toolbar which will contain a Kendo UI DrownDownList so that we can filter the Grid on ProductTypes which we will come back around to later on.

Now that we have the Grid configured for server side processing let’s take a quick look at what’s going down the wire in terms of pay loads for each of these actions so that we can mock up our models for these requests. When loading up IE Developer Tools (hit F12 or Tools > Developer Tools) and clicking on the Network Tab to start capturing network traffic we can see the actual pay load request for each of these actions.

So we can see that the pay load that is coming down the wire when a user performs a filter and sort on the grid is:


{"take":5,"skip":0,"page":1,"pageSize":5,"group":[],"filter":{"filters":[{"field":"ProductType","operator":"eq","value":"3"}],"logic":"and"},"sort":[{"field":"Name","dir":"desc"}]}

From this we can start mocking up our models needed for these types of Grid Actions for our Controller.


namespace MvcApplication3.Models
{
    public class GridFilter
    {
        public string Operator { get; set; }
        public string Field { get; set; }
        public string Value { get; set; }
    }

    public class GridFilters
    {
        public List<GridFilter> Filters { get; set; }
        public string Logic { get; set; }
    }

    public class GridSort
    {
        public string Field { get; set; }
        public string Dir { get; set; }
    }
}

Making changes to our Controller Action

We need to make changes to our existing Action on our Controller to support these new Grid objects that is being posted from our Grid when a user does a server side sort, filter, etc..


    public class ProductsController : Controller
    {
        [HttpPost]
        public JsonResult GetAll(int skip, int take, int page, int pageSize, 
            List<GridSort> sort = null, GridFilters filter = null)
        {
            var myDatabaseContext = new MyDatabaseContext();

            var products = myDatabaseContext.Products.AsQueryable();
            var totalCount = myDatabaseContext.Products.AsQueryable();

            if (filter != null && (filter.Filters != null && filter.Filters.Count > 0))
            {
                string whereClause = null;
                var parameters = new List<object>();
                var filters = filter.Filters;

                for (var i = 0; i < filters.Count; i++)
                {
                    if (i == 0)
                        whereClause += string.Format(" {0}", 
                            BuildWhereClause<Product>(i, filter.Logic, filters[i], 
                            parameters));
                    else
                        whereClause += string.Format(" {0} {1}", 
                            ToLinqOperator(filter.Logic), 
                            BuildWhereClause<Product>(i, filter.Logic, filters[i], 
                            parameters));
                }

                products = products.Where(whereClause, parameters.ToArray());
                totalCount = products.Where(whereClause, parameters.ToArray());
            }

            if (sort != null && sort.Count > 0)
                foreach (var s in sort)
                {
                    s.Field = (s.Field == "ProductType") ? "ProductTypeId" : s.Field;
                    products = products.OrderBy(s.Field + " " + s.Dir);
                }

            products = products.Skip(skip).Take(take);

            List<Product> productList = products.ToList();

            var productViewModels = new List<ProductViewModel.Product>();

            foreach (var p in productList)
            {
                productViewModels.Add(new ProductViewModel.Product
                                            {
                                                Completed = p.Completed.Date,
                                                CompletedBy = p.CompletedBy,
                                                Created = p.Created.Date,
                                                CreatedBy = p.CreatedBy,
                                                Name = p.Name,
                                                ProductId = p.ProductId,
                                                ProductType = p.ProductType.Name,
                                                ProductDetails = p.ProductDetails,
                                                Status = p.Status,
                                                Updated = p.Updated.Date,
                                                UpdatedBy = p.UpdatedBy
                                            });
            }

            return Json(
                new ProductViewModel
                    {
                        Products = productViewModels,
                        TotalCount = totalCount.Count()
                    });
        }

        public static string BuildWhereClause<T>(int index, string logic, 
            GridFilter filter, List<object> parameters)
        {
            var entityType = (typeof(T));
            var property = entityType.GetProperty(filter.Field);

            switch (filter.Operator.ToLower())
            {
                case "eq":
                case "neq":
                case "gte":
                case "gt":
                case "lte":
                case "lt":
                    if (typeof(DateTime).IsAssignableFrom(property.PropertyType))
                    {
                        parameters.Add(DateTime.Parse(filter.Value).Date);
                        return string.Format("EntityFunctions.TruncateTime({0}){1}@{2}", 
                            filter.Field, 
                            ToLinqOperator(filter.Operator), 
                            index);
                    }
                    if (typeof(int).IsAssignableFrom(property.PropertyType))
                    {
                        parameters.Add(int.Parse(filter.Value));
                        return string.Format("{0}{1}@{2}", 
                            filter.Field, 
                            ToLinqOperator(filter.Operator), 
                            index);
                    }
                    parameters.Add(filter.Value);
                    return string.Format("{0}{1}@{2}", 
                        filter.Field, 
                        ToLinqOperator(filter.Operator), 
                        index);
                case "startswith":
                    parameters.Add(filter.Value);
                    return string.Format("{0}.StartsWith(" + "@{1})", 
                        filter.Field, 
                        index);
                case "endswith":
                    parameters.Add(filter.Value);
                    return string.Format("{0}.EndsWith(" + "@{1})", 
                        filter.Field, 
                        index);
                case "contains":
                    parameters.Add(filter.Value);
                    return string.Format("{0}.Contains(" + "@{1})", 
                        filter.Field, 
                        index);
                default:
                    throw new ArgumentException(
                        "This operator is not yet supported for this Grid", 
                        filter.Operator);
            }
        }

        public static string ToLinqOperator(string @operator)
        {
            switch (@operator.ToLower())
            {
                case "eq": return " == ";
                case "neq": return " != ";
                case "gte": return " >= ";
                case "gt": return " > ";
                case "lte": return " <= ";
                case "lt": return " < ";
                case "or": return " || ";
                case "and": return " && ";
                default: return null;
            }
        }

        public JsonResult GetProductDetails(int skip, int take, int page, 
            int pageSize, string group)
        {
            var myDatabaseContext = new MyDatabaseContext();

            var productDetails = myDatabaseContext.ProductDetails
                .OrderBy(p => p.ProducDetailtId);

            return Json(
                new ProductDetailsViewModel
                    {
                        ProductDetails = productDetails.Skip(skip).Take(take),
                        TotalCount = productDetails.Count()
                    },
                JsonRequestBehavior.AllowGet);
        }
    }

Note: Instead of downloading the LINQ Dynamic Query Library, you may want to actually download the sample application for this post because the DynamicQueryable.cs class from the Linq Dynamic Libray has been slightly modified to handle EntityFunctions to support string search actions from our Grid such as Contains, StartsWidth and EndsWith string searches.

A few quick notes in regards to our changes to our Action on our Controller to now support complete server side processing of paging, sorting and filtering.

  • BuildWhereClause<T>(int index, string logic, GridFilter filter, List parameters)
    This helper method will build our our where clauses and predicates so tha we can chain them up and pass them into Dynamic LINQ.

  • ToLinqOperator(string @operator)

    This helper method will convert operators that are sent from our Grid to C# operators that Dynamic LINQ will understand and convert them for us

  • Lines 48-64, here we are iterating through the results to trim off the timestamp off of any properties that are of type datetime, so that when we do any grouping or filtering from the grid the timestamp of these fields are ignored.

                foreach (var p in productList)
                {
                    productViewModels.Add(new ProductViewModel.Product
                                                {
                                                    Completed = p.Completed.Date,
                                                    CompletedBy = p.CompletedBy,
                                                    Created = p.Created.Date,
                                                    CreatedBy = p.CreatedBy,
                                                    Name = p.Name,
                                                    ProductId = p.ProductId,
                                                    ProductType = p.ProductType.Name,
                                                    ProductDetails = p.ProductDetails,
                                                    Status = p.Status,
                                                    Updated = p.Updated.Date,
                                                    UpdatedBy = p.UpdatedBy
                                                });
                }
    
  • Lines 88-103, here we are checking against the type of the column (property) that we are searching against so that we can convert the search criteria to the appropriate type. Currently we are supporting searches against types of string, datetime and int. If you need to add more types simply enhance this section of the implementation.

                        if (typeof(DateTime).IsAssignableFrom(property.PropertyType))
                        {
                            parameters.Add(DateTime.Parse(filter.Value).Date);
                            return string.Format(&quot;EntityFunctions.TruncateTime({0}){1}@{2}&quot;, 
                                filter.Field, 
                                ToLinqOperator(filter.Operator), 
                                index);
                        }
                        if (typeof(int).IsAssignableFrom(property.PropertyType))
                        {
                            parameters.Add(int.Parse(filter.Value));
                            return string.Format(&quot;{0}{1}@{2}&quot;, 
                                filter.Field, 
                                ToLinqOperator(filter.Operator), 
                                index);
                        }
                        parameters.Add(filter.Value);
    
    
  • Lines 109-123, here we are just framing up the different queries for string searches from the Grid. The Grid supports StartsWith, Contains, and EndsWith.

                    case &quot;startswith&quot;:
                        parameters.Add(filter.Value);
                        return string.Format(&quot;{0}.StartsWith(&quot; + &quot;@{1})&quot;, 
                            filter.Field, 
                            index);
                    case &quot;endswith&quot;:
                        parameters.Add(filter.Value);
                        return string.Format(&quot;{0}.EndsWith(&quot; + &quot;@{1})&quot;, 
                            filter.Field, 
                            index);
                    case &quot;contains&quot;:
                        parameters.Add(filter.Value);
                        return string.Format(&quot;{0}.Contains(&quot; + &quot;@{1})&quot;, 
                            filter.Field, 
                            index);
    

    As you can see in the screenshot right below these are the current string search capabilites that the Grid has.

Great, let’s run a few searches from the Grid now.

  • Search on ProductId: 3

  • Search on Created Date: 04/20/2013

  • Search on Created Date >= 04/15/2012 and Name containing “sample product 3”

    Voila! Our controller only returns 3 records which assert our test case and are all of Created date >= 04/20/2012 and all contain the string “sample product 3” in the name.

Update: 04/24/2012

Added recursion to the filters processing to support multiple search criterias on the same field while at the same time supporting searches across multiple fields.

 

        [HttpPost]
        public JsonResult GetAll(int skip, int take, int page, int pageSize, 
            List&lt;GridSort&gt; sort = null, GridFilter filter = null)
        {
            var myDatabaseContext = new MyDatabaseContext();

            var products = myDatabaseContext.Products.AsQueryable();
            var totalCount = myDatabaseContext.Products.AsQueryable();

            if (filter != null &amp;&amp; (filter.Filters != null &amp;&amp; filter.Filters.Count &gt; 0))
            {
                ProcessFilters(filter, ref products);
                totalCount = products;
            }

            if (sort != null &amp;&amp; sort.Count &gt; 0)
                foreach (var s in sort)
                {
                    s.Field = (s.Field == &quot;ProductType&quot;) ? &quot;ProductTypeId&quot; : s.Field;
                    products = products.OrderBy(s.Field + &quot; &quot; + s.Dir);
                }

            products = products.Skip(skip).Take(take);

            List&lt;Product&gt; productList = products.ToList();

            var productViewModels = new List&lt;ProductViewModel.Product&gt;();

            foreach (var p in productList)
            {
                productViewModels.Add(new ProductViewModel.Product
                                            {
                                                Completed = p.Completed.Date,
                                                CompletedBy = p.CompletedBy,
                                                Created = p.Created.Date,
                                                CreatedBy = p.CreatedBy,
                                                Name = p.Name,
                                                ProductId = p.ProductId,
                                                ProductType = p.ProductType.Name,
                                                ProductDetails = p.ProductDetails,
                                                Status = p.Status,
                                                Updated = p.Updated.Date,
                                                UpdatedBy = p.UpdatedBy
                                            });
            }

            return Json(
                new ProductViewModel
                    {
                        Products = productViewModels,
                        TotalCount = totalCount.Count()
                    });
        }

        public static void ProcessFilters(GridFilter filter, ref IQueryable&lt;Product&gt; queryable)
        {
            var whereClause = string.Empty;
            var filters = filter.Filters;
            var parameters = new List&lt;object&gt;();
            for (int i = 0; i &lt; filters.Count; i++)
            {
                var f = filters[i];

                if (f.Filters == null)
                {
                    if (i == 0)
                        whereClause += BuildWhereClause&lt;Product&gt;(f, i, parameters) + &quot; &quot;;
                    if (i != 0)
                        whereClause += ToLinqOperator(filter.Logic) + 
                            BuildWhereClause&lt;Product&gt;(f, i, parameters) + &quot; &quot;;
                    if (i == (filters.Count - 1))
                    {
                        CleanUp(ref whereClause);
                        queryable = queryable.Where(whereClause, parameters.ToArray());
                    }
                }
                else
                    ProcessFilters(f, ref queryable);
            }
        }

Looks like our server side paging, sorting and filteration is golden!

Update: 05/11/2012- Added support for querying objects with child properties


    public static class GridHelper
    {
        public static void ProcessFilters&lt;T&gt;(GridFilter filter, ref IQueryable&lt;T&gt; queryable)
        {
            var whereClause = string.Empty;
            var filters = filter.Filters;
            var parameters = new List&lt;object&gt;();
            for (int i = 0; i &lt; filters.Count; i++)
            {
                var f = filters[i];

                if (f.Filters == null)
                {
                    if (i == 0)
                        whereClause += BuildWherePredicate&lt;T&gt;(f, i, parameters) + &quot; &quot;;
                    if (i != 0)
                        whereClause += ToLinqOperator(filter.Logic) + BuildWherePredicate&lt;T&gt;(f, i, parameters) + &quot; &quot;;
                    if (i == (filters.Count - 1))
                    {
                        TrimWherePredicate(ref whereClause);
                        queryable = queryable.Where(whereClause, parameters.ToArray());
                    }
                }
                else
                    ProcessFilters(f, ref queryable);
            }
        }

        public static string TrimWherePredicate(ref string whereClause)
        {
            switch (whereClause.Trim().Substring(0, 2).ToLower())
            {
                case &quot;&amp;&amp;&quot;:
                    whereClause = whereClause.Trim().Remove(0, 2);
                    break;
                case &quot;||&quot;:
                    whereClause = whereClause.Trim().Remove(0, 2);
                    break;
            }

            return whereClause;
        }

        public static string BuildWherePredicate&lt;T&gt;(GridFilter filter, int index, List&lt;object&gt; parameters)
        {
            var entityType = (typeof(T));
            PropertyInfo property;
            
            if(filter.Field.Contains(&quot;.&quot;))
                property = GetNestedProp&lt;T&gt;(filter.Field);
            else 
                property = entityType.GetProperty(filter.Field);
            
            var parameterIndex = parameters.Count;

            switch (filter.Operator.ToLower())
            {
                case &quot;eq&quot;:
                case &quot;neq&quot;:
                case &quot;gte&quot;:
                case &quot;gt&quot;:
                case &quot;lte&quot;:
                case &quot;lt&quot;:
                    if (typeof(DateTime).IsAssignableFrom(property.PropertyType))
                    {
                        parameters.Add(DateTime.Parse(filter.Value).Date);
                        return string.Format(&quot;EntityFunctions.TruncateTime(&quot; + filter.Field + &quot;)&quot; + ToLinqOperator(filter.Operator) + &quot;@&quot; + parameterIndex);
                    }
                    if (typeof(int).IsAssignableFrom(property.PropertyType))
                    {
                        parameters.Add(int.Parse(filter.Value));
                        return string.Format(filter.Field + ToLinqOperator(filter.Operator) + &quot;@&quot; + parameterIndex);
                    }
                    parameters.Add(filter.Value);
                    return string.Format(filter.Field + ToLinqOperator(filter.Operator) + &quot;@&quot; + parameterIndex);
                case &quot;startswith&quot;:
                    parameters.Add(filter.Value);
                    return filter.Field + &quot;.StartsWith(&quot; + &quot;@&quot; + parameterIndex + &quot;)&quot;;
                case &quot;endswith&quot;:
                    parameters.Add(filter.Value);
                    return filter.Field + &quot;.EndsWith(&quot; + &quot;@&quot; + parameterIndex + &quot;)&quot;;
                case &quot;contains&quot;:
                    parameters.Add(filter.Value);
                    return filter.Field + &quot;.Contains(&quot; + &quot;@&quot; + parameterIndex + &quot;)&quot;;
                default:
                    throw new ArgumentException(&quot;This operator is not yet supported for this Grid&quot;, filter.Operator);
            }
        }

        public static string ToLinqOperator(string @operator)
        {
            switch (@operator.ToLower())
            {
                case &quot;eq&quot;:
                    return &quot; == &quot;;
                case &quot;neq&quot;:
                    return &quot; != &quot;;
                case &quot;gte&quot;:
                    return &quot; &gt;= &quot;;
                case &quot;gt&quot;:
                    return &quot; &gt; &quot;;
                case &quot;lte&quot;:
                    return &quot; &lt;= &quot;;
                case &quot;lt&quot;:
                    return &quot; &lt; &quot;;
                case &quot;or&quot;:
                    return &quot; || &quot;;
                case &quot;and&quot;:
                    return &quot; &amp;&amp; &quot;;
                default:
                    return null;
            }
        }

        public static PropertyInfo GetNestedProp&lt;T&gt;(String name)
        {
            PropertyInfo info = null;
            var type = (typeof(T));
            foreach(var prop in name.Split('.'))
            {
                info = type.GetProperty(prop);
                type = info.PropertyType;
            }
            return info;
        }
    }

Happy Coding…! 🙂

Download sample application: https://skydrive.live.com/redir.aspx?cid=949a1c97c2a17906&resid=949A1C97C2A17906!465&parid=949A1C97C2A17906!361

https://drive.google.com/file/d/0B91gwLeUpEWBU2tnN0dNeVhSTzA/view?usp=sharing