Friday, August 10, 2012

Master-detail with knockout in asp.net MVC 3

In this post we will be discussing creating a master detail relationship with Knockout.js in asp.net MVC 3. For this let’s take a simple view model like below-
    public class PersonViewModel
    {
        public int PersonID { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Sex { get; set; }
        public int Age { get; set; }
        public string Address { get; set; }
    }
How we will be starting the problem is that, we will be retrieving a list of person with id, and name only and present the data in a HTML table. And on click of the table row we will be showing detail of the person with the additional information.

If we are not aware of knockout we can here and download and study the it.

For this example we will be using the following controller method to retrieve some in memory data-
public ActionResult Index()
{
    List<PersonViewModel> data = new List<PersonViewModel>();
    for (int i = 0; i < 10; i++)
        data.Add(new PersonViewModel() {PersonID=i, FirstName = "First Name " + i.ToString(), LastName = "Last Name " + i.ToString() });
    return View(data);
}
Next step is to prepare UI for the problem. First we will take the reference of jQuery and knockout file like below-
<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
<script src="http://knockoutjs.com/js/knockout-2.1.0.js" type="text/javascript"></script>
And we will be using the following HTML for displaying the person list and the detail-
<table>
    <tr><th>First Name</th><th>Last Name</th></tr>
    <tbody >
    <tr id="PersonID" style="cursor:pointer" >@*repeat the row to display all the person*@
        <td >First name of the person</td>
        <td >Last name of the person</td>
    </tr>
    </tbody>
</table>
<p data-bind="with: personViewModel.selectedItem">
    <b>Name:</b> <span >First name of the person</span> &nbsp;<span >Last name of the person</span><br />
    <b>Age:</b> <span >Age of the person</span> <br />
    <b>Sex:</b> <span >Sex of the person</span> <br />
    <b>Address:</b> <span >Address of the person</span> <br />
</p>
As you can see that the controller method is returning List<PersonModelView> as the type of the strongly type view, so, we will have the following model directive in the view-
@model List<KnockoutMasterDetail.Models.PersonViewModel>
This will give generic list of Person data. But as per out requirement of knockout we need to convert this list to JavaScript array. We can do this by the following code-
var persons = new Array();
@foreach (var d in Model)
{
    <text>persons.push({PersonID:</text>@d.PersonID<text>, FirstName:"</text>@d.FirstName<text>", LastName:"</text>@d.LastName<text>"});</text>
}
The above code will be inside the script tag in the view. And it’s simply looping the person list and creating an array of person object like {PersonID:1, FirstName:”First Name 1”, LastName:”Last name 1” }. Now we have our person array ready. Next let’s create view model to be used with knockout.
function personModel() {
    var self = this;
    self.persons = ko.observableArray(persons);
    self.selectedItem = ko.observable();
    self.getDetail = function(item) { 
      //to be implemented  
    }
}
personViewModel = new personModel();
In the above JavaScript function, we are adding this line self.persons = ko.observableArray(persons);. What it means is that we are directing the knockout that the persons array is an observable. That mean if there is any change in the array then its corresponding UI will also be changed automatically. For this example, if we do not make the array as observable, it will work as we are not expecting any change in the array. So, we can simply have self.persons = persons;. But we need the next statement, as on clicking a row we need to change the detail. To activate the viewmodel to work with knockout, we need to register the viewmodel. We can do this by the following like-
ko.applyBindings(personViewModel);
Just this registration will not work. We need to make the necessary changes in the HTML.

Now self.persons is an array and we need to make changes in the HTML table to link this array to the HTML table. We can do this by following-
    <tbody data-bind="foreach: personViewModel.persons">
    <tr data-bind="click:personViewModel.getDetail, attr: {id: PersonID}" style="cursor:pointer" >
        <td data-bind="text: FirstName"></td>
        <td data-bind="text: LastName"></td>
    </tr>
    </tbody>
What we are doing here is adding data-bind attribute that is related to knockout. Whenever we will make any change in the array, knockout will use this attribute to make changes back to the UI. In the first line we are using foreach to loop through all the items of the array persons and putting the values of the array item to row. Next line we have click:personViewModel.getDetail, attr: {id: PersonID}. In first part of it we are associating click event of the row to a function in the viewmodel named getDetail. In the second part we are assigning id of the row to PersonID. And in the remaining two lines we are assigning first name and last name to two cells.

Next we need to modify the detail section and we need to associate this to observable selectedItem in the model and we can do this by altering the HTML as below-
<p data-bind="with: personViewModel.selectedItem">
    <b>Name:</b> <span data-bind="text: FirstName"></span> &nbsp;<span data-bind="text: LastName"></span><br />
    <b>Age:</b> <span data-bind="text: Age"></span> <br />
    <b>Sex:</b> <span data-bind="text: Sex"></span> <br />
    <b>Address:</b> <span data-bind="text: Address"></span> <br />
</p>
So, now whenever we will click the row it will call the function getDetail in the model. Currently the function is empty. Let’s first have a controller method that returns the detail of the person like below-
        public JsonResult GetDetail(int personID)
        {
            return Json(new PersonViewModel()
            {
                FirstName = "FirstName " + personID.ToString(),
                LastName = "LastName " + personID.ToString(),
                Sex = "Sex " + personID.ToString(),
                Age = personID,
                Address = "Address " + personID.ToString()
            });
        }
Here we are just building an in memory detail. And we can call the controller method like-
    self.getDetail = function(item) { 
        $.ajax({
            url:  '@Url.Action("GetDetail","Person")',
            data: { personID: item.PersonID },
            type: "POST",
            success: function (response) {
                self.selectedItem(response); 
            },
            error:function(x,y,z){debugger;}
        });
    }
Inside the success method we are getting the result of the ajax call and assigning the result to the selectedItem(self.selectedItem(response);). As it’s an observable, knockout will automatically update the corresponding HTML for detail. That’s all. You can download the source code from this link.