Saturday, November 27, 2021

Searching, Filtering and Sorting with KnockoutJS in C# MVC

 KnockoutJS is a superb companion for client-side data-binding, especially when used in conjunction with ASP.NET MVC. Sometimes however, you need to do something, and while there are numerous examples out there on the great JSFiddle, etc., there is no explanation of how the code works, the docs don’t go deep enough, and one is left head-scratching to work out why things work as they do. 

I was recently trying to get a solid solution for implementing a combination “filter and search” on an observable array in Knockout. The data from the array is received by the browser from the server using Ajax. This article is about the approach I used and clearly explains the following:

  • How to set up a simple observable array
  • How to use Ajax from client browser to server to get a JSON list of data and populate a Knockout observable array
  • How to use Knockout Mapping plugin to link data from server to client
  • How to implement a combined filter and search mechanism using Knockout

Here is a screenshot of the finished product!

Image 1

There are numerous ways to achieve this goal, this is my approach - It's simple, and it works – ‘nuff said.

Setup - Server Side

The first thing we need to do is set up some sample data to work with. Let's deal with the MVC C# side of things first.

Data Model

We create a data model MVC server side to generate some random data to send down to the server. To do this, construct a simple class that has some basic members.

C#
public class ClientModel
{
    public string ClientName { get; set; }
    public string Address { get; set; }
    public string Phone { get; set; }
    public bool Active { get; set; }
    public int Status { get; set; }
}

We will then add a method that generates a list of random data:

C#
public List<clientmodel> GetClients()
{
    List<clientmodel> rslt = new List<clientmodel>();
    Random rnd = new Random();
    for (int i = 0; i < 50; i++)
    {
        int r = rnd.Next(1, 100);
        int z = rnd.Next(1, 5000);
        ClientModel cm = new ClientModel();
        cm.ClientName = i.ToString() + " Name ";
        cm.Address = i.ToString() + " Address " + z.ToString();
        cm.Phone = "Phone " + r.ToString();
        int x = rnd.Next(0, 2);
        if (x == 0)
            cm.Active = true;
        else cm.Active = false;
        cm.Status = rnd.Next(0, 4);
        rslt.Add(cm);
    }
    return rslt;        
}

We then create a wrapper class to carry the data we create and send it back to the browser.

C#
public class ClientList 
{
public List<clientmodel> Clients {get; set;}
 
public ClientList()
{
    Clients = new List<clientmodel>();
}
 
}

Here's a gotcha! … when you are doing any kind of complex model in Knockout, and are sending data back/forth between server/client, it's important to have things lined up *exactly*, especially when you need to wrap/unwrap child observable arrays. In this example, I ensure that I call my list of data “Clients”, so that, critically, when I unwrap this using mapping client-side, it matches up correctly with my observable array of client-side data. You will see the other half of this two sided dance later in the article.

Controller

The next step is to create a controller that uses the model to generate the random data. We create our wrapper list class, fill it with random data, and then use the uber amazing Newtonsoft JSON utility to serialise the list for the browser.

Heads-up!! To use Newtonsoft, get it from NuGet and don’t forget to reference it.

C#
using Newtonsoft.Json;

public string ClientAjaxLoader()
{
    ClientModel cm = new ClientModel();
    ClientList cl = new ClientList();
    cl.Clients.AddRange(cm.GetClients());
    var jsonData = JsonConvert.SerializeObject(cl);
    return jsonData;
}

Setup - Client Side

As this is an MVC application, for simplicity of this article, we will go into the automatically generated index.cshtml, remove everything in there and add the bare bones code we require.
There are many Knockout tutorials out there to cover the basics – so here is just a quick explanation of how I set things up client-side.

Step 1

Create the main Knockout ViewModel. At this stage, this is extremely simple, containing an observable array of “Clients”:

JavaScript
var viewModel = function () {
    var self = this;
    self.Clients = ko.observableArray([]);
}

Step 2

Create a Model that mirrors the ClientModel data incoming from the server.

JavaScript
 var ClientDetail = function (data) 
{ 
    var self = this; 
    if (data != null) 
    { 
        self.ClientName = ko.observable(data.ClientName); 
        self.Address = ko.observable(data.Address); 
        self.Phone = ko.observable(data.Phone); 
        self.Active = ko.observable(data.Active); 
        self.Status = ko.observable(data.Status); 
    } 
}

Step 3

As it stands, the ViewModel has to have data manually pushed to its Clients array. Let's change this so that the array can be populated by passing data in as a parameter.

JavaScript
var viewModel = function (data) {
    if (data != null)
        {
        // DO SOMETHING
}
    var self = this;
    self.Clients = ko.observableArray([]);
}

The “Do something” part, is where we introduce “mapping”. Mapping allows us to more properly define what the member called “Clients” actually consists of.

JavaScript
var viewModel = function (data) {
    if (data != null)
        {
        ko.mapping.fromJS(data, { Clients: clientMapping }, self);
        }
    var self = this;
    self.Clients = ko.observableArray([]);

We call the ko.mapping.fromJS, passing in parameters of:

  1. the data packet (that will be received from the server)
  2. instructions telling it how to unwrap the data using mapping
  3. where to put it (into itself)

Step 4

Create the mapping function “ClientMapping” that will “unwrap” the JSON string that is generated server side.

JavaScript
var clientMapping = {
    create: function (options) {
        return new ClientDetail(options.data);
    }
};

This is the first mention we have in our code of the “ClientDetail” model we started off with and how it relates to the ViewModel. So what we are saying to the client is … you have an array of somethings. When data comes in, take that data, and look for a block of records **inside that data*** that is **called CLIENTS** and unwrap each record you find in that block, casting each record as model type “ClientDetail”. This is part of the gotcha, and important … remember, your data that comes down from the server, must be flagged with the name of the mapping filter text, otherwise it will not find it and your data will not map. Here it is again – don’t forget!

MVC Server side:

C#
ClientList cl = new ClientList();

This contains a primary member called “Clients”:

C#
public List<clientmodel> Clients {get; set;}</clientmodel>

The client side ViewModel has a member that is an array called “Clients”:

C#
self.Clients = ko.observableArray([]);

And the two are linked together by the KO mapping procedure "{ Clients: clientMapping }":

C#
ko.mapping.fromJS(data, { Clients: clientMapping }, self);

Populating the Knockout observable Array using Ajax

To get the data from server to client, we will set up a function within the ViewModel itself, and call it from the click of a link.

JavaScript
self.getClientsFromServer = function () {
    $.ajax("/home/ClientAjaxLoader/", {
        type: "GET",
        cache: false,
    }).done(function (jsondata) {
        var jobj = $.parseJSON(jsondata);
        ko.mapping.fromJS(jobj, { Clients: clientMapping }, self);
        });
}

So this is very simple, we call the url “/home/ClientAjaxLoader” (which calls the domain used, in our test case “localhost”. In the “done” callback, we take the data received “jsondata”, and convert it to a JavaScript object using “parseJSON”. Then we call the Knockout Mapping utility to unwrap and map the JSON data into our array of Clients

To display the data, we need to hook up some HTML controls and bind the Knockout array.
Create a simple table, and use the “for each” binding to fill it with Client detail data.

Image 2

Finally, add a link to use to call the new method:

Image 3

Note the data-binding in this case is an OnClick binding that calls the method we created to get the data from the server.
Ok, we are on the happy path, here's a screenshot everything wired up so far and the data flowing once we click the “get clients from server” link.

(It's not going to win any UX contests, but it works!)

Image 4

Search and Filter

Ok, here's the reason we came to this party – let the games begin!

This search and filter method is based on using the inbuilt “observable” behaviour and two way binding of knockout. In short, what we do is this:

  • Create a new member of the viewmodel that’s a simple observable that acts as our “search filter”.
  • Create a computed member that observes the search filter, and when it changes, for each record in the clients array, compares the value in the array to the search value, and if it matches, returns that client record in an array of “matching records”.
  • Hook up the new computer member in a “for each” to display the filtered results.

Let's start it off simple and put a single search in place to find any client records that have a “ClientAddress” value that *contains* the value of our search item – in other words, a wildcard search.

Step 1

Create a new observable member to hold our search string:

JavaScript
self.search_ClientAddress = ko.observable('');

Step 2

Create a new computed member that when our search sting changes, scans through the client records and filters out just what we want:

JavaScript
self.filteredRecords = ko.computed(function () {
    return ko.utils.arrayFilter(self.Clients(), function (rec) {
            return (
                      (self.search_ClientAddress().length == 0 || 
                       rec.Address().toLowerCase().indexOf
                       (self.search_ClientAddress().toLowerCase()) > -1)
                   )        
    });
});

In this computed member, we use KO.UTILS.arrayFilter to scan through the Clients array, and for each record, make a comparison, and return only records from the Clients array that we allow through the net. Look at what's happening ... the arrayFilter takes a first parameter of the array to search within, and a second parameter of a function that carries out the filtering itself. Within the filtering function, we pass in this case a variable called "rec" - this represents the single array record object being examined at the time. So we are basically saying "scan through our clients list, and for each record you encounter, test to see if the current record being examined should be let through the array filter.

In this case, we say, allow the record through, EITHER if there is ZERO data (length == 0) in the search string:

JavaScript
self.search_ClientAddress().length == 0

OR:

If the value of the search string is contained within the client record being scanned using “arrayfilter”:

JavaScript
rec.Address().toLowerCase().indexOf(self.search_ClientAddress().toLowerCase()) > -1

The arrayFilter method is an extremely powerful tool as we will see shortly.

The last thing to do to check it works is to put an HTML control in place and bind this to the new observable search string member (self.search_ClientAddress).

Address contains<input data-bind=" value:=" valueupdate:="" />

Here, we bind to the new member, and critically, use the “valueUpdate” trigger of ‘afterKeyDown’ to send a message to the search observable that the value has updated. This then triggers the computed member to do its thing.
To show that it's working, we will change our original display table from showing the “Clients” array of data, to showing the “filteredRecords” returned array.
The approach we are taking here is that our original data array of clients stays put, and we do any visual / manipulation on a copy of the data in the data set returned in the filtered search result.

Image 5

So here is it is after entering some values into the address search box. The ‘44’ value is found in any part of the “Address” field, therefore two records return.

Image 6

Now that we know how the basics of search work, we can quickly build up a powerful combined search and filter functionality. We will add functionality to allow the user to search on the first part of the Client.Name, the Client.Address as before, and also filter to show within that search, only Active or Inactive client records, or those with a status value, say greater or equal to 2.

Step 1

Add more search observables !

JavaScript
self.search_ClientName = ko.observable('');
self.search_ClientActive = ko.observable();
self.search_ClientStatus = ko.observable('');

Step 2

Add HTML to set those values:

Image 7

Step 3

Some JQuery goodness to bind to the Click events on those links:

JavaScript
$(function () {
                $('#showAll').click(function () {
                    vm.search_ClientActive(null);
                    vm.search_ClientStatus(null);
                });
                $('#showActive').click(function () {
                    vm.search_ClientActive(true);
                });
                $('#showInActive').click(function () {
                    vm.search_ClientActive(false);
                });
                $('#showStatus').click(function () {
                    vm.search_ClientStatus(2); // set filter to show only status with value >= 2
                });
            });

(Note the “show all” action resets the value of the search observables to null, thus triggering the list to show all records).

Step 4

Finally, we will expand our computed member to include the parameters we have introduced above:

JavaScript
self.filteredRecords = ko.computed(function () {
    return ko.utils.arrayFilter(self.Clients(), function (rec) {
            return (
                      (self.search_ClientName().length == 0 || ko.utils.stringStartsWith
                      (rec.ClientName().toLowerCase(), self.search_ClientName().toLowerCase()))
                        &&
                      (self.search_ClientAddress().length == 0 || rec.Address().toLowerCase().indexOf
                      (self.search_ClientAddress().toLowerCase()) > -1)
                        &&
                      (self.search_ClientActive() == null || rec.Active() == self.search_ClientActive())
                        &&
                      (self.search_ClientStatus() == null || rec.Status() >= self.search_ClientStatus())
                   )                
    });
});

So here it is all working nicely together.

Show all records:

Image 8

Show only active:

Image 9

Show active, with search on ClientName and ClientAddress:

Image 10

Last filtering trick, add to the mix where the Client.Status value is greater or equal to 2:

Image 11

Sorting the List

It's great to be able to apply a filter, and even cooler you can search within filter results – the only missing piece of the puzzle perhaps is the ability to sort. So here's a quick addition to make that happen:

Server side, I am going to add a “SortCode” field to our client model, and throw in some spurious data to fill the space that we can sort on.

C#
cm.Status = rnd.Next(0, 4);

                switch (cm.Status) {

                    case 0:  
                            cm.SortCode = "AAA";
                            break;
                    case 1:  
                            cm.SortCode = "BBB";
                            break;
                    case 2: 
                            cm.SortCode = "CCC";
                            break;
                    case 3: 
                            cm.SortCode = "DDD";
                            break;
                    case 4: 
                            cm.SortCode = "EEE";
                            break;
                    default :
                            break;
                
                }

Client-side, we add in a KO observable that the computed filtered array can monitor:

JavaScript
self.isSortAsc = ko.observable(true);

We add some markup/Jquery to turn that on/off:

Flip sort code

JavaScript
$('#flipSortCode').click(function () {
                   vm.isSortAsc(!vm.isSortAsc());
               });

Next, we add the new observable to the computed function, and finally, chain a SORT function onto the computed member:

JavaScript
self.filteredRecords = ko.computed(function () {            

            return ko.utils.arrayFilter(self.Clients(), function (rec) {
                return (
                          (self.search_ClientName().length == 0 ||
                                ko.utils.stringStartsWith(rec.ClientName().toLowerCase(), 
                                self.search_ClientName().toLowerCase()))
                            &&
                          (self.search_ClientAddress().length == 0 ||
                                rec.Address().toLowerCase().indexOf
                                (self.search_ClientAddress().toLowerCase()) > -1)
                            &&
                          (self.search_ClientActive() == null ||
                                rec.Active() == self.search_ClientActive())
                            &&
                          (self.search_ClientStatus() == null ||
                                rec.Status() >= self.search_ClientStatus())
                            &&
                          (self.isSortAsc() != null)
                       )
            }).sort(            
               function (a, b) {
                   if (self.isSortAsc() === true)
                       {
                       var x = a.SortCode().toLowerCase(), y = b.SortCode().toLowerCase();
                       return x < y ? -1 : x > y ? 1 : 0;
                       }
                   else {
                       var x = b.SortCode().toLowerCase(), y = a.SortCode().toLowerCase();
                       return x < y ? -1 : x > y ? 1 : 0;
                       }                  
                    }
                );
        });

So, clicking on the “flip sort” now sorts our list asc/desc as required.

Image 12

Image 13

Summary

This article has given you a whirlwind tour of using the very powerful “Ko.Utils.arrayFilter” feature to implement clean quick search and filter functionality using Knockout, with data server to the ViewModel via Ajax from a MVC backend. As always, the trick is in the detail, so don't forget the gotchas. If you liked the article, please vote and comment!

No comments:

Post a Comment

No String Argument Constructor/Factory Method to Deserialize From String Value

  In this short article, we will cover in-depth the   JsonMappingException: no String-argument constructor/factory method to deserialize fro...