In the first part of this two-part series, published in the June 2011 issue, we developed a Windows Communication Foundation (WCF) service that takes data in and out of the Tasks table. (See " Create a Simple Task-List Application Using ASP.NET, WCF, and jQuery, Part 1.") We created a web form for displaying the tasks in a tabular format using the jQuery Templates plug-in. Finally, we developed a DatePicker plug-in to pick due dates. In this second part, we'll make the task-list application functional by writing jQuery code, including a few helper functions, code to call WCF service methods, and code to filter the task-list table.

Dealing with JSON Format

While developing our WCF service in Part 1, we used the WebInvoke attribute to specify JavaScript Object Notation (JSON) as the format of serialization between the client and the server. JSON is a lightweight data exchange format and is part of JavaScript's overall standards. As far as objects are concerned, JSON makes use of name-value syntax to represent object properties and their values. Our task-list application requires TaskData objects to be passed between the client and the WCF service. Since the data format on the wire will be JSON, we need a way to wrap several pieces of data, such as task information entered in text boxes, in JSON format and then send the package to the WCF service. To accomplish this, we'll create a simple JavaScript function, GetJSONString(). The complete code of the GetJSONString() function is shown in Figure 1.

Figure 1: The GetJSONString() function
function GetJSONString(propertyNames, propertyValues) {
 var jsonString = '{';
 var i=0;
 for(i=0;i<propertyNames.length;i++)
 {
  jsonString += '"' + propertyNames[i] + '"';
  jsonString += ':';
  jsonString += '"' + propertyValues[i] + '"';
  if (i < propertyNames.length-1) {
   jsonString += ',';
  }
 }
 jsonString += '}';
 return jsonString;
}

The GetJSONString() function accepts two arrays as parameters. The propertyNames array contains the property names of an object (TaskData in our case), and the propertyValues array contains their respective values. The code then iterates through the arrays and builds a string representing a JSON object. Figure 2 shows a sample TaskData object in JSON format.

Figure 2: A sample TaskData object in JSON format
{
  "TaskID" : "2e977d0c-4446-4c87-9557-f05f1f30c17a",
  "Title" : "Some Title",
  "Description" : "Some Description",
  "Priority" : 2,
  "Status" : 1
  ...
}

Handling and Passing Dates

JSON format is self-explanatory when it comes to strings, Booleans, and numbers. However, problems arise with the date data type. There is no standard way to identify dates in JSON. To tackle this issue, WCF expects JSON serialized dates to be in a special form, \/Date(n)\/, where n is the number of milliseconds since midnight on January 1, 1970. This way, WCF can try to convert the supplied date into the .NET Framework's DateTime structure. A similar situation arises when receiving dates serialized by the WCF service in JSON format. To deal with this date format conversion, we'll write two small helper functions: ToJSDate() and ToWCFDate(). The first function converts a WCF serialized date into a JavaScript usable date. The second converts a JavaScript date into a WCF-compliant format. Figure 3 shows the ToJSDate() function.

Figure 3: The ToJSDate() function
function ToJSDate(value) {
 var pattern = /Date\(([^)]+)\)/;
 var results = pattern.exec(value);
 var dt = new Date(parseFloat(results[1]));
 return dt.getDate() + "/" + (dt.getMonth()+1) + "/" +  
 dt.getFullYear();
}

Figure 4 shows the ToWCFDate() function.

Figure 4: The ToWCFDate() function
function ToWCFDate(value) {
 var dtArr = value.split("/");
 var dt = new Date(dtArr[2], --dtArr[1], dtArr[0]);
 var date = '\/Date(' + dt.getTime() + '+0530)\/';
 return date;
}

The ToJSDate() function uses the exec() method to check the supplied value for a regular expression pattern. The result is then parsed into a float, and a JavaScript date object is created. Since we are using the day/month/year format in the DatePicker, we return the date value in the same format.

The ToWCFDate() function accepts a date returned by the DatePicker in the day/month/year format and then constructs a new JavaScript Date object based on that date. The function then gets the number of milliseconds since January 1, 1970, and formats the date as required by the WCF service. Although the code uses the India (GMT+05:30) time zone, we can easily edit it to meet our requirements.

Calling a WCF Service from jQuery

Once our helper functions are ready, we can call the methods of the WCF service. Since we'll be calling multiple methods of the WCF service, let's encapsulate the logic in a function named CallRemoteMethod(). Figure 5 shows the complete code for the CallRemoteMethod() function.

Figure 5: The CallRemoteMethod() function
function CallRemoteMethod(methodName, methodParam, successMethod) {
 var url = "Service.svc/" + methodName;
 if (methodParam == "") {
  methodParam = "{}";
 }
 $.ajax({
    type: "POST",
    url: url,
    data: methodParam,
    contentType: "application/json; charset=utf-8",
    dataType: "json",
    success: successMethod,
    error: OnError
  });
}

The CallRemoteMethod() function takes three parameters: the name of the remote method to call, the parameter (if any) to the remote method, and the reference to a function that will be called when the remote method is completed. The CallRemoteMethod() function uses the ajax() method in jQuery. The ajax() method lets us make an AJAX request. A call to the ajax() method accepts several settings in key-value form. The type parameter indicates the type of request, GET or POST. The url parameter is the URL to the WCF method. Notice that the URL takes the form of <service_file_name>/<method_name>. Therefore, the URL to call the Insert() method of the service will be Service.svc/Insert.

The data parameter supplies the parameters needed to execute the remote method. In our example, the data will be a TaskData object encoded in JSON format. The contentType parameter indicates the type of content being sent as a part of the request. The dataType parameter indicates the type of data returned by the WCF service. Since we have designed our WCF service to return JSON, we set dataType to json. The success parameter is a function that should be called when the remote operation is successfully completed. Finally, the error parameter indicates a function that should be called in case there is any error while invoking the remote method. We'll have a generic error handler that simply displays a JavaScript alert along with the error information. The error handler function OnError is shown in Figure 6.

Figure 6: Handling errors during AJAX calls
function OnError(err) {
alert("There was error executing the remote service!\r\n" +
"Code : " + err.status + "\r\n" +
"Message : " + err.statusText);
}

The error handler function receives an error object that contains specifics of the error. The status property returns an error code, and statusText property returns a descriptive error message.

Handling the Ready Event

jQuery raises the ready event when the HTML Document Object Model (DOM) of a web page is completely loaded. We'll handle this event, populate the task-list table, and wire the event handlers of other controls. This way, we'll ensure that our jQuery code executes when all the elements, scripts, and variables it needs for proper functioning are loaded and accessible. Figure 7 shows the relevant code.

Figure 7: The ready event handler
$(document).ready(OnReady);

function OnReady() {
 $("#btnInsert").click(OnInsert);
 $("#btnUpdate").click(OnUpdate);
 $("#btnDelete").click(OnDelete);
 $("#btnRefresh").click(OnRefresh);
 $("#ddlFilter").change(OnFilter);
 $("#btnRefresh").click();
 $("#calendar1").DatePicker();
 var today = new Date();
 var dt = today.getDate() + "/" +
          (today.getMonth()+1) + "/" +
          today.getFullYear();
 $("#calendar1").DatePicker('setDate', dt);
}

The first line of Figure 7 attaches the OnReady event handler to the ready event. The ready event is raised only for the document object. Many developers use anonymous functions as jQuery event handlers, such as in the following example:

$(document).ready(function(){//code here})

Although that technique is often handy and tidy, it's better to write separate functions for the sake of readability and for ease of understanding.

The OnReady function wires click event handlers for several buttons. Notice the jQuery selector for referring to HTML elements that have a specific ID. We need to add the # prefix to the ID of an element we want to access. The btnInsert and btnUpdate elements are buttons, whereas btnDelete and btnRefresh are link buttons. Their click event will be handled by the JavaScript functions OnInsert, OnUpdate, OnDelete, and OnRefresh, respectively. Along the same lines, the change event of the filter drop-down list is handled by the OnFilter function. The change event is raised when the selection in the drop-down list changes. Once the event handlers are attached, we programmatically trigger the click event of the Refresh button so that the task-list table will be populated with all the tasks. (See the "Populating the Task-List Table" section.)

In order to display the DatePicker plug-in we developed earlier, we'll place a DIV element with ID calendar1 on the web form. Notice how we initiate the DatePicker by selecting calendar1, then calling the DatePicker() method. Recall that if we don't pass any method name to the DatePicker, the init() method is called, and the DatePicker is rendered. Finally, we call the setDate() method of the DatePicker plug-in so that the current date is selected by default. Notice that the JavaScript month numbers start from 0. Because January is 0, we need to increment the month number by 1.

Populating the Task-List Table

The OnRefresh function handles the job of populating the task-list table. Event handlers in jQuery take the event parameter that provides information about the event. Such information includes the target element that caused the event, the X and Y coordinates of the mouse, the state of the Ctrl and Shift keys, and so on. Figure 8 shows the skeleton of the OnRefresh function.

Figure 8: Populating the task-list table
function OnRefresh(event) {
 var handler = ...
 CallRemoteMethod("SelectAll", "", handler);
 event.preventDefault();
}

The OnRefresh method calls the CallRemoteMethod() function that we discussed earlier and passes the method name as SelectAll. The SelectAll() method doesn't take any parameter, so an empty string is passed. The third parameter of CallRemoteMethod() indicates a success function and is passed as the handler variable. The success function receives the return value of the WCF service method. The SelectAll() method returns an array of TaskData objects, so the handler function receives this array as a parameter. Figure 9 shows the complete code of the success handler function.

Figure 9: Displaying the results of the SelectAll() method
var handler = function (tasks) {
 $("#taskTable tbody > tr:first-child ~  tr")
  .remove();
 $("#taskTableContainer").tmpl(tasks)
  .appendTo("#taskTable");
 $("#taskTable input[type='button']")
  .click(function (event){OnSelectByID(event);});
 $("#taskTable tr:even").addClass("TaskTableRow");
 $("#taskTable tr:odd").addClass("TaskTableAlternateRow");
 $("#taskTable tr:first").removeClass("TaskTableRow");
 $("#taskTable tr:first").addClass("TaskTableHeader");
 $("#taskTable tr:first-child ~ tr").each(function (index)  
  {
   var item = $(this).tmplItem();
   var arrDt = ToJSDate(item.data.DueDate).split("/");
   var dt1 = new Date(arrDt[2], arrDt[1], arrDt[0]);
   var dt2 = new Date();
   var milli_d1 = dt1.getTime();
   var milli_d2 = dt2.getTime();
   var diff = milli_d1 - milli_d2;
    if (diff < 0) {
     $(this).addClass("Warning");
    }
  });
 }

To populate the task-list table, we first use the remove() method in jQuery to remove all the rows of the task-data table except the header row that contains the column headings. Notice that the selector is used to select all the table rows except the first row, the header row. The first-child child filter filters the first row from all the rows of taskTable. The next siblings selector (~) returns all the rows that come after the header row. Then, we remove those rows by using the remove() method.

The tmpl() method is responsible for processing the jQuery template, and this method accepts the data to be displayed in a template. If the supplied data is in the form of an array, the template is rendered once for each element in the array. If the supplied data is an object, a single template item is rendered. Recall that taskTableContainer is our template definition. The tmpl() method returns the resultant HTML markup after processing the template. In our case, the markup will be in the form of HTML table rows. We'll then append the rows to the taskTable table by using the appendTo() method.

Next, we'll select all the Show buttons from the resultant table by using the jQuery attribute selector. An attribute selector lets us select elements based on their attribute values. We select all input elements that have the type attribute button. We attach the click event handler, which internally calls the OnSelectByID() function, to the buttons. Then, we improve the readability of the task-list table by adding different Cascading Style Sheets (CSS) classes to alternate rows. To do this, we use jQuery's :even, :odd, and :first basic filters. As the names suggest, these filters return even, odd, and first elements. The addClass() method is then used to add a CSS class to the matched elements. (You can find the complete style sheet in the code download for this article.)

We need to notify the user if the due date of any task has already passed. The code inside the each() method handles that job. The each() method is executed for every element returned by a selector—in our case, all table rows except the header. To access individual TaskData objects associated with a specific row, we use the tmplItem() method in jQuery. Inside the each() method, the keyword this refers to the element being iterated. Calling the tmplItem() method on the current row returns a data item associated with the row. Individual properties of the data item can then be accessed. We convert a WCF date into a JavaScript date by using the ToJSDate() function. Then, we compare it with the current date. If the task date is older than the current day, we render that date in some different color and add a Warning CSS class to it.

Notice the use of the event.preventDefault() method in Figure 8. The Refresh button is a LinkButton control and causes postback when clicked. However, we do not need this default behavior. To cancel the postback, we call the preventDefault() method, effectively canceling the event. Figure 10 shows what the task-list table looks like when it is populated with task information.

Figure 10: The task-list table

Displaying a Selected Task for Editing

When we click any of the Show buttons from the table rows, that specific task should be displayed in the top editing region. Earlier, we mentioned that clicking a Show button calls the OnSelectByID() JavaScript function. Figure 11 illustrates this function.

Figure 11: Displaying a selected task for editing
function OnSelectByID(event) {
 var id = event.target.id;
 $("#taskTable tr.TaskTableSelectedRow")
  .removeClass("TaskTableSelectedRow");
 $("#taskTable tr:has(input[id='" + id + "'])")
  .addClass("TaskTableSelectedRow");
 var item = $("#taskTable tr:has(input[id='" +
  id + "'])").tmplItem();
 $("#txtTitle").val(item.data.Title);
 $("#txtDescription").val(item.data.Description);
 $("#ddlPriority").val(item.data.Priority);
 $("#ddlStatus").val(item.data.Status);
 var dt = ToJSDate(item.data.DueDate);
 $("#calendar1").DatePicker('setDate', dt);
 event.preventDefault();
}

The function first stores the ID of the clicked button in a variable. Recall that the ID of a Show button is the same as the TaskID. We then apply the TaskTableSelectedRow CSS class to mark the row as selected. This way, the user gets a visual indication that the row is being edited. Notice that :has() is used in the selector to find a row that contains an input element with a specific ID. The :has() selector will select a row if it contains an element with specified criteria. Finally, we use the tmplItem() method to retrieve the data item for the row being selected. Property values of the data item are then assigned to corresponding controls by using the val() method.

Inserting, Updating, and Deleting Tasks

To insert a task, we must call the Insert() method of the WCF service. We need to retrieve values entered in various controls, form the JSON string equivalent of a TaskData object, then call the Insert() method. Figure 12 shows how this is done.

Figure 12: Adding a new task
function OnInsert(event) {
 var title=$("#txtTitle").val();
 var description = $("#txtDescription").val();
 var priority = $("#ddlPriority").val();
 var status = $("#ddlStatus").val();
 var duedate =  ToWCFDate(
 $("#calendar1").DatePicker('getDate'));
 var jsonData = GetJSONString(["Title", "Description",  
  "Priority", "Status", "DueDate"],
  [title, description, priority, status, duedate]);
 var handler = function (result) {
  $("#lblMsg").text("Task added successfully!");
  $("#btnRefresh").click();
 }
 CallRemoteMethod("Insert", jsonData, handler);
 event.preventDefault();
}

Once data is retrieved from various controls, we form a JSON string by calling the GetJSONString() function we created earlier. Arrays of property names and their values are passed to the GetJSONString() function. The CallRemoteMethod() function then invokes the Insert() method with the JSON object we just constructed. The success handler function simply sets the text of a <SPAN> element (ASP.NET Label control) to a success message and programmatically calls the click event of the Refresh button so that the newly added task will be displayed in the table.

The update operation is performed along similar lines. The only difference is that we also need to pass the TaskID as a part of the JSON object, and we need to call the Update() method. Figure 13 shows how the update operation works.

Figure 13: Updating a task
function OnUpdate(event) {
 var id = "";
 $("#taskTable tr.TaskTableSelectedRow")
  .find(":input[type='button']")
  .each(function (index) { id = $(this).attr("id"); });
 var title = $("#txtTitle").val();
 var description = $("#txtDescription").val();
 var priority = $("#ddlPriority").val();
 var status = $("#ddlStatus").val();
 var duedate = ToWCFDate($("#calendar1")
  .DatePicker('getDate'));
 var jsonData = GetJSONString(["TaskID", "Title",
 "Description", "Priority", "Status", "DueDate"],
 [id, title, description, priority, status, duedate]);
 var handler = function (result) {
  $("#lblMsg").text("Task updated successfully!");
  $("#btnRefresh").click();
 }
 CallRemoteMethod("Update", jsonData, handler);
 event.preventDefault();
}

To get the TaskID of the task that is being updated, we select the table row that has the TaskTableSelectedRow CSS class applied. Only one row at a time will have this class applied. Then, from this row, we find the input element that has the type button. We need to do this checking because there is also a check box in each row. We then use the attr() method to get the ID attribute value of the button. This ID is simply the TaskID. In forming the JSON string, we use the ToWCFDate() function to convert the date selected in the DatePicker to a WCF-compliant format. Finally, the CallRemoteMethod() function is called to invoke the Update() method.

To delete one or more tasks, we need to figure out which check boxes are selected. Then, we need to call the Delete() method of the WCF service for all those TaskIDs. Figure 14 shows the OnDelete() function that is responsible for this job.

Figure 14: Deleting a task
function OnDelete(event) {
 $("#taskTable :checked").each(function (index) {
 var id = "";
 $(this).parents('tr')
  .find(":input[type='button']")
  .each(function (index) { id = $(this).attr("id"); })
 var handler = function (result) {
  $("#lblMsg").text("Task(s) deleted successfully!");
 };
 var jsonData = GetJSONString(["id"], [id]);
 CallRemoteMethod("Delete", jsonData, handler);
});
$("#btnRefresh").click();
event.preventDefault();

First, we use the :checked selector to select all the check boxes that are checked. Next, we need to find out the TaskID for each of the checked rows. To reach the Show button and get the TaskID, we use the parents(), find(), and each() methods. The parents() method returns all the parents for a specific element. Passing tr as a parameter to the parents() method returns only those parents that are of type tr. We then find the input element from the table row that is returned by the parents() method that has a type as button. Finally, the ID of the button is retrieved by using the attr() method, and the Delete() method of the WCF service is invoked by passing the TaskID.

Filtering Tasks

The filter drop-down list lets us filter tasks that are displayed in the task-list table on the basis of their priority and status. A part of the drop-down list's change event handler is shown in Figure 15.

Figure 15: Filtering the task-list table
function OnFilter(event) {
 var filter = $("#ddlFilter").val();
 switch (filter) {
  case "A":
   $("#taskTable tr").css("display", "block");
   break;
  case "HP":
   $("#taskTable tr").each(function () {
   var item = $(this).tmplItem();
   if (item.data.Priority != 1 &&
       item.data.Priority != null) {
    $(this).css("display", "none");
   }
   else {
    $(this).css("display", "block");
   }
  });
 break;
...
 $("#taskTable tr:first-child ~ tr:hidden :checked")
   .removeAttr("checked");
 if ($("#taskTable tr:first-child ~ tr:visible").length == 0) {
  $("#lblMsg").text("No records found!");
 }
 else {
  $("#lblMsg").text("");
 }

The code first retrieves the current selection from the filter drop-down list. If the current selection is A (All Tasks), the display CSS property of all the rows is set to block using the css() method. In this case, all the task rows in the table will be displayed.

If the current selection in the filter drop-down list is HP (High Priority), we iterate through all the data rows of the task table. With each iteration, we retrieve the data item associated with that row by using the tmplItem() method. The Priority value is then checked, and all the items with a Priority other than 1 are kept hidden by setting their display CSS property to none. Other cases of the switch statement are similar except that they check for different conditions. We won't discuss these cases in detail here.

For the correct operation of the delete functionality, we need to ensure that the task rows that are hidden don't have their check boxes selected. To do this, we filter all table rows that are hidden with the help of the :hidden selector and then remove the checked attribute from the check boxes by using the removeAttr() method.

That's it! We can now run the web form and see our task-list application in action.

Further Enhancements

In this two-part series, we developed a simple task-list application that manages tasks and their priorities, statuses, and due dates. We used a WCF service to deal with database operations, such as INSERT and UPDATE. We then invoked this WCF service from the client side by using the jQuery ajax() method. We also worked with many jQuery selectors and methods. To further improve upon what we have already developed, you can

  • incorporate validations on the data that is being entered
  • add visual indicators or flags for Priority and Status
  • modify the DatePicker plug-in for more configuration options, or convert it into a pop-up calendar. (Note: Several ready-made plug-ins can be found on the jQuery website.)
  • improve CSS classes to make the UI more appealing

Whether or not you opt for these enhancements, your task-list application is now functional and ready for action.

Bipin Joshi (bipinjoshi@yahoo.com) has been programming since 1995 and has worked with .NET since its inception. He has authored or co-authored several books and numerous articles about .NET technologies. Bipin blogs about yoga and technology at bipinjoshi.com.