Multi-Check-boxes filter in Telerik Kendo Grid over complex multi-select column fields

This post aims to come up with some useful workaround to filter data in Telerik Kendo Grid over multi-select column fields bound to complex data by using multi-check-boxes filters.

Background

When trying to use default features from Telerik Kendo UI for ASP.NET MVC under the previously-commented scenario, I came across that it was not possible. I was reading a lot about it in forums but I could not find any solution for me except some that forced me to refactor my code largely. Not funny for me. That having said, on one hand, I couldn't display data values in filter on the target column and on the other hand, once I could find a solution to show multi-check-boxes with desired options, it turns out default functionality did not provide filter capabilities for multi-select fields bound to complex data. It would come very handy if someone reading this post could tell me I am wrong and even leading me to some link explaining how to do it...

What is a multi-select column field in Telerik Kendo Grid?

They are columns showing multiple elements for the same column or field, for instance, a list of products as displayed below.  This can be achieved very easily with Telerik Kendo grid. 

Each row in a grid can contain one or more cells with a list of items. In the figure above, I am showing products but they could be anything else. It does not matter the specific case, this is only for demo/documentation purposes. Not only end-users can see the list but performing CRUD actions as well in case of building the application to meet that functionality. 

What is a multi-check-boxes filter in Telerik Kendo Grid?

As displayed below, this is only a filter to let us select one or more elements on a list to filter grid rows based on them. We can use those values for applying "and" or "or" rules according to application requirements. In other words, maybe we need to filter elements that contain ["Chai" and "Chang"] products or perhaps only elements that contains ["Chai" or "Chang"] items. We can apply these operators ("and", "or", etc.) in the way we need.

-

What are complex data types?

Simply put, complex data types are only those constructed primarily from built-in primitive types (char, int, float, etc.). Some very simple complex types could be the displayed ones below. For instance, the Product one is only a class to store typical data for products such as ProductId and ProductName. I've created both of them as string fields for simplicity but perhaps ProductId could be an int or Guid value. Never mind, remember "demo purposes"...So, I'd like to bound a list of Product classes to a column field in Telerik Kendo Grid in such a way that let me display a list of ProductName(s) to the end-users and set ProductId(s) as internal values. This case, I need to make use of a multi-select column in Telerik Kendo Grid bound to a complex object. Additionally, I will make use of another Order class so as to place that list of products within a very simple Order class. 

public class Product
{
    public string ProductId { get; set; }
    public string ProductName { get; set; }
}

public class Order
{
    public string OrderId { get; set; }
    public string CustomerId {get;set; }
    public List<Product> Products { get; set; }
}

 

Basic key concepts in Telerik Kendo Grid for MVC ASP.Net

.data() jQuery Method

To begin with, let me first reminder you the basic use of .data() method in jQuery. It let us store or attach arbitrary data associated with any DOM (Document Object Model) element and then, return those previously-stored data. You can attach data by using .data(key, value) or .data(obj) methods. The former let us attach data based on a key string value and the latter via an object that must contain key-value pairs. In order to get data back, we only have to use the .data(key) or .data() methods. They will return stored data based on a key or an object containing each stored value as a property in case of using the parameterless method. Please, review the jQuery API for more details.

The .data() jQuery method is used to reference the Kendo UI Grid instance. Once the reference is established, we can employ the Grid client-side API to control its behavior.

@(Html.Kendo().Grid((IEnumerable<MyNamespace.Models.Product>)ViewBag.Products)
        .Name("grid")
        .Columns(columns =>
        {
            columns.Bound(product => product.ProductID);
            columns.Bound(product => product.ProductName);
        })
)
<script>
    $(function() {
        // The Grid name along with the "kendoGrid" key string is used in order to get its client-side instance.
        var grid = $("#gridName").data("kendoGrid");
    });
</script>

Bounding complex objects to Kendo Grid columns

Let me start by saying that the aim of this post is not to explain how Telerik Kendo Grid works so that please, review official documentation for more detailed explanations. As I am sure you understand, there is a lot of documentation about it and it is not worth to copy and paste all of this within this post.

Going straight to the point and as you can see in the code below, we only have to bind the Telerik Kendo Grid to the complex Products property by referencing it. The OrderViewModel contains a Products property in a similar way as the domain Order class displayed above. So, with ClientTemplate method, we can provide a template to make use of MultiSelect functionality in order to set the data text and value fields from the underlying model. This way, the grid can show product names and store internally product codes that we can use in the front-end or even send to the back-end according to rest of grid configuration.

<div>
@(Html.Kendo().Grid<ViewModels.OrderViewModel>()
        .Name("GridName")
        .Columns(columns =>
        {
            columns.Bound(c => c.OrderId);
            columns.Bound(c => c.Products)
                .ClientTemplate(
                    Html.Kendo().MultiSelect()
                        .Name("#=OrderId#")
                        .DataTextField("ProductName")
                        .DataValueField("ProductId")
                        .Enable(false)
                        .BindTo((IEnumerable<Product>)ViewData["Products"])
                        .ToClientTemplate()
                        .ToHtmlString()
                )
                .Filterable(p => p.Multi(true)
                                    .CheckAll(false)
                                    .ItemTemplate("productsFilterItemTemplate")
                                    .BindTo(((IEnumerable<Product>)ViewData["Products"]))
                )
                ;
            columns.Bound(c => c.CommaDelimitedProductNames).Visible(false);

        })
        .Editable(e => e.Mode(GridEditMode.InLine))
        .Filterable(f => f.Mode(GridFilterMode.Menu).Extra(false))
        .Events(e => e.Filter("OnProductFilter"))
        .DataSource(dataSource => dataSource
                    .Ajax()
                    .Events(e => {
                        e.Error("OnGridNameError");
                        e.RequestStart("OnGridNameRequestStart");
			e.RequestEnd("OnGridNameRequestEnd");
			})
	)
)
</div>

 

Setting Multi-Check-boxes filter with personalized template

Assuming that we already have ViewData object with an entry called "Products" with the list of available products, we can configure the grid to display the multi-check-boxes filter by using this code:

.Filterable(p => p.Multi(true)
  .CheckAll(false)
  .ItemTemplate("productsFilterItemTemplate")                        
  .BindTo(((IEnumerable<Product>)ViewData["Products"]))
)

And here is the productsFilterItemTemplate javascript function referenced in the ItemTemplate method to provide the customized user interface. Eventually, it will be employed for rendering a list of input checkbox fields:

function productsFilterItemTemplate(e) {
        return "<span><label><input class='product-filter-input' type='checkbox' name='" + e.field + "' value='#= Description #'/><span>#= Description #</span></label></span><br/>"
    }

Notice the use of Multi(true) method for making the filter multiple and CheckAll(false) to avoid displaying the "Select All" option. You can configure this as you best think it meets your requirements.

Providing customized handling for filter client-side Kendo Grid events

The handling for filter event can be configured as displayed below:

.Events(e => e.Filter("OnProductFilter"))

And here is the linked onProductFilter Javascript function:

function onProductFilter(e) {
        var grid = $("#GridName").data("kendoGrid");
        var dataSource = grid.dataSource;

        if (e.field == "Products" && e.filter != null && e.filter.filters.length > 0) {
            var filterProducts = [];
            
            // Apply filters coming from "Products" field into new field
            e.filter.filters.forEach(function (item, i) {
                filterProducts.push({
                    field: "CommaDelimitedProductNames",
                    operator: "contains",
                    value: e.filter.filters[i].value
                });
            });

            // Provide default logic operators ("Or")
            dataSource.filter(
            {
                logic: "or",
                filters: filterProducts
            });
           
            // Update User Interface by using Kendo classes. This must be done very carefully as it is not very recommendable if it can be done in any other way
            $("th[data-field='Products'] a").first().addClass("k-state-active");
            $("th[data-field='Products'] a").first().removeClass("k-state-border-down");

            e.filter.filters.forEach(function (item, i) {
                $("input[name='Products'][value='" + e.filter.filters[i].value + "']").prop("checked", true)
            });

            e.preventDefault();
        }
        else if (e.field == "Products") {
            var dataSource = grid.dataSource;
            if (dataSource.filter() != null) {
                filters = dataSource.filter().filters;
                if (filters.length > 0)
                {
                   removeFilter(filters, "CommaDelimitedProductNames");
                   $("th[data-field='Products'] a").first().addClass("k-state-border-down");
                   $("th[data-field='Products'] a").first().removeClass("k-state-active");
                }
            }
        }
    }

First of all, I created a new non-visible column called CommaDelimitedProductNames to store the list of products in a new plain string field property. The aim of this column is to apply in a silent way the selected filter values in the multi-check-boxes filter so that it works fine yet unnoticed for the end user. In addition to this, I incorporated some code to update the user interface accordingly, for instance, changing the appearance of icons, titles, etc. based on multi-filter state. 

You can add some code similar to this one to create this property:

public string CommaDelimitedProductNames
        {
            get { return Products != null && Products.Any() ? Products.Select(r=>r.ProductName).Aggregate((x,y) => x + ", " + y) : string.Empty; }
        }

To summarize, the algorithm looks like:

 1) Detect target column for handling "Products" filter.

 2) Extract selected filters on "Products" filter and apply those filters to CommaDelimitedProductNames column in a sequential way with "or" operator. With this "or" operator, the grid will show products with values matching at least one product among all the selected.

 3) Update user interface accordingly to provide the most friendly and similar user experience compared to the default one as the code is preventing us from getting the default behavior (see e.preventDefault() code line). This must be done because in this scenario, default filtering does not work at all.

Finally, notice that I am making use of a custom removeFilter() javascript function in order to remove filters as needed:

function removeFilter(filter, searchFor) {
    if (filter == null)
        return [];

    for (var x = 0; x < filter.length; x++) {

        if (filter[x].filters != null && filter[x].filters.length >= 0) {
            if (filter[x].filters.length == 0) {
                filter.splice(x, 1);
                return removeFilter(filter, searchFor);
            }
            filter[x].filters = removeFilter(filter[x].filters, searchFor);
        }
        else {
            if (filter[x].field == searchFor) {
                filter.splice(x, 1);
                return removeFilter(filter, searchFor);
            }
        }
    }

    return filter;
}

Providing customized handling for client-side Telerik Kendo Grid events

The handling for client-side Telerik Kendo Grid events (Error, RequestStart, RequestEnd, etc.) can be configured as displayed below:

.DataSource(dataSource => dataSource
                    .Ajax()
                    .Events(e => {
                        e.Error("OnGridNameError");
                        e.RequestStart("OnGridNameRequestStart");
			e.RequestEnd("OnGridNameRequestEnd");
			})
	)

 

 

It turns out there are cases in which we need to provide further code to make behavior more clear or similar to the default one. For instance, filter event is not triggered when deselecting all the check-boxes and then clicking the "Filter" button. It is odd. I came across this behavior not being sure how to resolve. As a workaround, users can click "Clear" button and actions will be performed fine but I wanted filters to work fine in both situations. I've not still found another workaround except handling the RequestEnd event for controlling the object triggering the filter action and then, performing the clear/reset. We only have to check whether there are no checked inputs in the Products filter and then, clearing the target filters. This is the interim code I implemented while trying to get something better. Getting help on this would be very useful for me, though.

function onGridNameRequestEnd(e) {
       ...
        if (e.type == 'read' && $("input[name='Products']:checked").length == 0) {
            grid = $("#GridName").data("kendoGrid");
            var dataSource = grid.dataSource;
           if (dataSource.filter() != null) {
                filters = dataSource.filter().filters;
                if ((filters.length > 0) 
                    && (filters.filter(function (e) { return e.field === 'CommaDelimitedRoleDescriptions'; }).length > 0))
                {
                    removeFilter(filters, "CommaDelimitedRoleDescriptions");
                    $("th[data-field='Roles'] a").first().addClass("k-state-border-down");
                    $("th[data-field='Roles'] a").first().removeClass("k-state-active");
                    dataSource.read();
                }
            }
        }
        ...
    }

 

Reference and Samples

Kendo UI Grid widget | Kendo UI Grid dataSource

You can find other samples here.

Conclusion

To be honest, this is a valid interim workaround that I've checked it works quite fine yet showing a very similar behavior to the default one but I would like to know another better way to do it. I'd would like to have default functionality in Telerik Kendo for accomplishing it or at least some other more flexible and maintainable. Some idea or proposal?

Kind regards.

 

 

Add comment