This website is no longer actively supported
Written by John DeVight on 13 Apr 2012 19:55
Download the Sample Code at: Kendo UI ASP.NET Sample Applications, Source Code tab by clicking on "Download" under "Latest Version" and looking for Mobile -> TaskManager.
Overview
In Kendo UI Mobile and ASP.NET MVC : Building the Mobile Task Manager, Part 2 - Displaying Data the MobileController was created so that the Task Manager Mobile application can communicate with the TaskManager ASP.NET MVC applciation, to get data. The next step is to edit and save the data. In this article I'll be:
- Editing and saving a Sprint
- Editing and saving a Developer
- Handling client side and server side validation
Editing and Saving a Sprint
Displaying the Sprints ListView Items as Links
In Part 2 - Displaying Data I created a template with an id of "sprints-listview-template" that display the sprint id:
<script id="sprints-listview-template" type="text/x-kendo-template"> Sprint #= Id # </script>
Now, I would like to be able to click on a sprint to see the details. To do this, I alter the template to display each sprint as an anchor:
<script id="sprints-listview-template" type="text/x-kendo-template"> <a href="\#sprint?id=#= Id #" class="km-listview-link" data-role="listview-link">Sprint #= Id #</a> </script>
The href needs to begin with a pound sign (#). However, since the syntax for indicating that a value will be inserted (#= attr #), I need to escape the pound sign. I add a class of km-listview-link and a data-role of listview-link that that the sprints will render properly in the listview.
When a sprint is clicked, it will navigate to the sprint page and pass the id of the sprint in the query string.
Displaying the Sprint Details
The Sprint page will display the start and end dates of the sprint and a list of tasks for the sprint. Here is the sprint page:
<div id="sprint" data-role="view" data-layout="layout" data-title="Sprints" data-show="TaskManager.Sprint.show"> <input id="sprint-id" type="hidden" /> <h2>Details</h2> <ul id="sprint-fields" data-role="listview"> <li> Start: <input id="sprint-start-date" type="date"/> </li> <li> End: <input id="sprint-end-date" type="date"/> </li> </ul> <h2>Tasks</h2> <ul id="sprint-tasks-listview" data-role="listview"> <li>Loading...</li> </ul> </div>
The sprint page, data-show attribute is set the TaskManager.Sprint.show function that will be called each time the page is displayed. I've listed the Start and End dates in a ListView. There is also a ListView for the list of tasks associated with a sprint.
Updating the MobileController
In the TaskManager.Mvc.Mobile class library, MobileController, I add the SprintTasks Controller Action to get the tasks for the sprint. Here is the code:
public JsonpResult SprintTasks(int id) { Sprint sprint = SprintRepository.GetSprints().Where(s => s.Id == id).First(); return new JsonpResult(new { tasks = sprint.Tasks }); }
Getting the Data
In the taskmanager.dataaccess.js, I add 2 new functions, TaskManager.DataAccess.getSprint and TaskManager.DataAccess.getSprintTasks. Here is the code:
TaskManager.DataAccess.getSprint = function(id) { try { return JSON.parse(JSON.stringify(JSON.find({ data: TaskManager.DataAccess._sprints, criteria: [{ elementName: "Id", elementValue: id }] }))); } catch (err) { } } TaskManager.DataAccess.getSprintTasks = function(id, callback) { try { $.ajax({ url: TaskManager._server + "/SprintTasks", data: { id: id }, dataType: "jsonp", jsonp: "d", cache: false }) .done(function (response) { callback(response.tasks); }); } catch (err) { } }
Displaying the Data
Since the sprints have already been retrieved from the MobileController and stored in the TaskManager.DataAccess._sprints variable, all I need to do is find the sprint and return it. However, I don't have the tasks for the sprint, so the TaskManager.DataAccess.getSprintTasks function retrieves the tasks from the MobileController, SprintTasks Controller Action. When the tasks are returned, the callback function is called and the tasks are passed to it.
In the taskmanager.sprints.js, I add a new namespace called TaskManager.Sprint and add the function TaskManager.Sprint.show. The function is passed a parameter that contains the query string values. I get the id for the sprint from the e.view.params object and then call TaskManager.DataAccess.getSprintTasks to get the tasks for the sprint. While that is happening, I get the sprint by calling TaskManager.DataAccess.getSprint and then set the Start and End dates. Here is the code:
TaskManager.Sprint = {} TaskManager.Sprint.show = function(e) { $("#sprint-id").val(e.view.params.id); TaskManager.DataAccess.getSprintTasks(e.view.params.id, function(tasks) { if (tasks.length == 0) { $("#sprint-tasks-listview").empty().append($("<li/>").text("There are no tasks defined")); } else { // Initialize the tasks listview. $("#sprint-tasks-listview").empty().kendoMobileListView({ dataSource: kendo.data.DataSource.create({ data: tasks }), template: kendo.template($("#sprint-tasks-listview-template").html()) }); } }); var sprint = TaskManager.DataAccess.getSprint(e.view.params.id); $("#sprint-start-date").val(Date.fromRaw(sprint.Start).toShortDate()); $("#sprint-end-date").val(Date.fromRaw(sprint.End).toShortDate()); }
The sprint dates that were returned from the MobileController are in the format "/Date(a very long number)/". I needed to be able to parse them. To do this, I created a Date.fromRaw function. I then wanted to display the date as a short date, so I created a Date.prototype.toShortDate function. Since I will need to be able to convert the date beck into the "/Date(a very long number)/" format, I also created a Date.prototype.toRaw function. I added the functions to taskmanager.js. Here is the code:
Date.fromRaw = function(raw) { return new Date(parseInt(raw.substr(6))); } Date.prototype.toShortDate = function() { return (this.getMonth() + 1) + "/" + this.getDate() + "/" + this.getFullYear(); } Date.prototype.toRaw = function() { return "\/Date(" + this.valueOf() + ")\/"; }
The TaskManager.Sprint.show function displays the list of tasks in the tasks listview. Each item is displaying using the sprint-tasks-listview-template. Similar to the sprints, each task is displayed as link to the task page. Here is the template:
<script id="sprint-tasks-listview-template" type="text/x-kendo-template"> <a href="\#task?id=#= Id #" class="km-listview-link" data-role="listview-link">#= Description #</a> </script>
We'll come back to the task page a little later.
The Save and Back Commands
With Kendo UI Mobile, the footer is displayed at the top and the header is displayed at the bottom for an Android application. I decided to add a header to the sprint page where I would put 2 buttons, "Save" and "Back". Since the header is specific to this page, I have defined it within the sprint page div. The buttons are defined as anchor elements. The Save button saves the sprint and the Back button takes the user back to the sprints page with the list of all the sprints. For the Back button, I set the href to "#sprints". For the Save button, I set the onclick attribute to call the TaskManager.Sprints.save function.
<header data-role="header"> <div data-role="navbar"> <a data-align="left" data-role="button" class="nav-button" href="#sprints">Back</a> <span data-role="title">Sprint</span> <a data-align="right" data-role="button" class="nav-button" onclick="TaskManager.Sprint.save();">Save</a> </div> </header>
Client-side Validation
I wanted to be sure that the dates were correctly entered. If they are not, then I wanted to display a message to let the user know that there was a problem. In the sprint page, I added a hidden h2 element with an id of sprint-save-results to display the errors. I also needed to provide the user with a save button. I decided to put the save button in the page heading. I also put a back button as well so that the user can get back to the list of sprints. Since the heading only applies to the sprint page, I put the heading in the page. Here is the page:
<div id="sprint" data-role="view" data-layout="layout" data-title="Sprints" data-show="TaskManager.Sprint.show"> <header data-role="header"> <div data-role="navbar"> <a data-align="left" data-role="button" class="nav-button" href="#sprints">Back</a> <span data-role="title">Sprint</span> <a data-align="right" data-role="button" class="nav-button" onclick="TaskManager.Sprint.save();">Save</a> </div> </header> <h2 id="sprint-save-results" style="display:none;position:absolute;"></h2> <input id="sprint-id" type="hidden" /> <h2>Details</h2> <ul id="sprint-fields" data-role="listview"> <li> Start: <input id="sprint-start-date" type="date"/> </li> <li> End: <input id="sprint-end-date" type="date"/> </li> </ul> <h2>Tasks</h2> <ul id="sprint-tasks-listview" data-role="listview"> <li>Loading...</li> </ul> </div>
When the user clicks the save button, the TaskManager.Sprint.save function is called. The function gets the dates and verifies that they are valid before setting them. If the date is invalid, the TaskManager.Sprint.results function is called to display the error. Here is the code:
TaskManager.Sprint.save = function() { try { var id = $("#sprint-id").val(); var sprint = TaskManager.DataAccess.getSprint(id); var valid = true; var start = new Date($("#sprint-start-date").val()); if ((valid = start != "Invalid Date") == true) { var end = new Date($("#sprint-end-date").val()); if ((valid = end != "Invalid Date") == true) { sprint.Start = start.toRaw(); sprint.End = end.toRaw(); } else { TaskManager.Sprint.results(true, "Red", "Invalid End Date"); } } else { TaskManager.Sprint.results(true, "Red", "Invalid Start Date"); } } catch (err) { TaskManager.Sprint.results(true, "Red", err); } } TaskManager.Sprint.results = function(show, color, message) { var $results = $("#sprint-save-results"); if (show) { $results.css("display", "").css("position", "").css("color", color).text(message); } else { $results.css("display", "none").css("position", "absolute").text(""); } }
Saving the Sprint Details
Updating the MobileController
To save the data, I added a SaveSprint Controller Action. The sprint is passed in as a string. The start and end dates are in the "/Date(a very long number)/" format and the JavaScriptSerializer doesn't know what to do with it. I found a project on CodePlex called JSON.NET that handles it, so I added a reference to it in the TaskManager.Mvc.Mobile class library and call the JsonConvert.DeserializeObject static function to deserialize the string into a Sprint. I then call the SprintRepository.SaveSprint to save the sprint. If there is an error message, I pass back the error message and also the original values for the sprint. Here is the code:
public JsonpResult SaveSprint(string data) { Sprint sprint = JsonConvert.DeserializeObject<Sprint>(data); string result = SprintRepository.SaveSprint(sprint); return new JsonpResult(new { result = result, sprint = (result.Length == 0 ? sprint : SprintRepository.GetSprints().Where(s => s.Id == sprint.Id).First()) }); }
Saving the data
In the taskmanager.dataaccess.js, I add a new function, TaskManager.DataAccess.saveSprint to save the sprint to the SaveSprint Controller Action. If there is a server side error, then I locate the sprint in the TaskManager.DataAccess._sprints and replace it with the sprint that is passed back. Here is the code:
TaskManager.DataAccess.saveSprint = function(sprint, callback) { try { $.ajax({ url: TaskManager._server + "/SaveSprint", data: { data: JSON.stringify(sprint) }, dataType: "jsonp", jsonp: "d", cache: false }) .done(function (response) { // If there was an error... if (response.result.length > 0) { var sprint = JSON.find({ data: TaskManager.DataAccess._sprints, criteria: [{ elementName: "Id", elementValue: response.sprint.Id }] }); sprint = response.sprint; } callback(response); }) } catch (err) { } }
In the taskmanager.sprints.js, I update the TaskManager.Sprint.save function to save the sprint if there are no client-side errors. If the save is successful, I display a "Save Complete" message and then hide the message after 2 seconds. If there is a server-side error, I display the server-side error. Here is the code:
TaskManager.Sprint.save = function() { try { var id = $("#sprint-id").val(); var sprint = TaskManager.DataAccess.getSprint(id); var valid = true; var start = new Date($("#sprint-start-date").val()); if ((valid = start != "Invalid Date") == true) { sprint.Start = start.toRaw(); var end = new Date($("#sprint-end-date").val()); if ((valid = end != "Invalid Date") == true) { sprint.End = end.toRaw(); } else { TaskManager.Sprint.results(true, "Red", "Invalid End Date"); } } else { TaskManager.Sprint.results(true, "Red", "Invalid Start Date"); } if (valid) { TaskManager.Sprint.results(true, false, "Saving..."); TaskManager.DataAccess.saveSprint(sprint, function(response) { TaskManager.Sprint.results(response.result.length > 0, response.result); if (response.result.length == 0) { TaskManager.Sprint.results(true, "Green", "Save Complete"); // After 2 seconds, hide the message. setTimeout('$("#sprint-save-results").fadeOut("slow", function() { TaskManager.Sprint.results(false); });', 2000); } else { TaskManager.Sprint.results(true, "Red", response.result); } }); } } catch (err) { TaskManager.Sprint.results(true, "Red", err); } }
Screenshots
Details | Successful Save |
---|---|
![]() |
![]() |
Client Error | Server Error |
![]() |
![]() |
Editing and Saving a Task
Implementing the editing and saving of a task is very similar to the editing and saving of a sprint. The steps I are:
- Create a task page for editing a task.
- Create the taskmanager.task.js for the task code.
- Modify the MobileController to be able to save a task.
- Modify the taskmanager.dataaccess.js to save a task.
Displaying the Task Details
When a task is selected in the list of tasks for a sprint, the task page is displayed. The task page contains the following:
- A header that is specific to the task page.
- A hidden field that is displayed to show the results of the save.
- A listview to list the fields for the task.
The Here is the code for the task page:
<div id="task" data-role="view" data-layout="layout" data-title="Task" data-show="TaskManager.Task.show"> <header data-role="header"> <div data-role="navbar"> <a id="task-back" data-align="left" data-role="button" class="nav-button" href="#sprint">Back</a> <span data-role="title">Sprint</span> <a data-align="right" data-role="button" class="nav-button" onclick="TaskManager.Task.save();">Save</a> </div> </header> <h2 id="task-save-results" style="display:none;position:absolute;"></h2> <input id="task-id" type="hidden" /> <ul id="task-fields" data-role="listview"> <li> Type: <select id="task-type" class="tm-input"> <option value="0">Scenerio</option> <option value="1">Change Request</option> </select> </li> <li> Desc.: <input id="task-description" type="text" class="tm-input"/> </li> <li> Dev.: <select id="task-assignedto" class="tm-input"> </select> </li> <li> Hours: <input id="task-estimated-hours" type="number" class="tm-input"/> </li> <li> Completed: <input id="task-completed" type="date" class="tm-input"/> </li> </ul> </div>
The task fields didn't fit well on the mobile device, so I had to do the following:
- Shorten the label for each field.
- Alter the width of each input field.
I altered the width of each input field by creating a file called taskmanager.css and adding a css class called tm-input that I applied to all the input fields. The code for tm-input is:
.tm-input { width: 150px; }
The Back button for a task needs to include the id for the sprint to be displayed in the sprint page. I set this in the TaskManager.Task.show function when the task page is displayed by getting the sprint id from the "sprint-id" hidden field that is on the sprint page.
The rest of the TaskManager.Task.show function is similar to the sprint page:
- Get the task.
- Get a list of developers for the Assigned to dropdownlist.
- Set the fields with the task values.
Here is the code:
TaskManager.Task.show = function(e) { $("#task-id").val(e.view.params.id); $("#task-back").attr("href", "#sprint?id=" + $("#sprint-id").val()); var task = TaskManager.DataAccess.getTask(e.view.params.id); TaskManager.DataAccess.getDevelopers(function(developers) { var assignedto = $("#task-assignedto"); assignedto.empty(); $.each(developers, function(idx, developer) { var option = $("<option/>") .attr("value", developer.Id) .text(developer.FirstName + " " + developer.LastName); if (developer.Id == task.AssignedToId) { option.attr("selected", "selected"); } assignedto.append(option); }); try { assignedto.kendoDropDownList(); } catch (err) { } }); $("#task-type").val(task.Type); $("#task-description").val(task.Description); $("#task-estimated-hours").val(task.EstimatedHours); $("#task-completed").val(Date.fromRaw(task.CompletedDate).toShortDate()); }
Since the list of tasks for a sprint have already been retrieved from the server, the code for the TaskManager.DataAccess.getTask function finds the task in the list and returns it.
TaskManager.DataAccess.getTask = function(id) { try { return JSON.parse(JSON.stringify(JSON.find({ data: TaskManager.DataAccess._sprintTasks.items, criteria: [{ elementName: "Id", elementValue: id }] }))); } catch (err) { } }
Saving the Task Details
Saving the task is also very similar to saving the sprint:
- The SaveTask method is added to the MobileController.
- The TaskManager.DataAccess.saveTask function is added to taskmanager.dataaccess.js.
- The TaskManager.Task.save function is added to taskmanager.task.js.
Here is the code for the SaveTask method in the MobileController:
public JsonpResult SaveTask(string data) { Task task = JsonConvert.DeserializeObject<Task>(data); SprintRepository.SaveTask(task, false); return new JsonpResult(new { result = string.Empty }); }
Here is the code for the TaskManager.DataAccess.saveTask function:
TaskManager.DataAccess.saveTask = function(task, callback) { $.ajax({ url: TaskManager._server + "/SaveTask", data: { data: JSON.stringify(task) }, dataType: "jsonp", jsonp: "d", cache: false }) .done(function (response) { JSON.replaceArrayElement(TaskManager.DataAccess._sprintTasks.items, "Id", task.Id, task); callback(response); }) }
Here is the code for the TaskManager.Task.save function:
TaskManager.Task.save = function() { var id = $("#task-id").val(); var task = TaskManager.DataAccess.getTask(id); task.Description = $("#task-description").val(); task.EstimatedHours = $("#task-estimated-hours").val(); var completedDate = new Date($("#task-completed").val()); if (completedDate != "Invalid Date") { task.CompletedDate = completedDate.toRaw(); } TaskManager.DataAccess.saveTask(task, function(response) { if (response.result.length == 0) { TaskManager.Task.results(true, "Green", "Save Complete"); // After 2 seconds, hide the message. setTimeout('$("#task-save-results").fadeOut("slow", function() { TaskManager.Task.results(false); });', 2000); } }); } TaskManager.Task.results = function(show, color, message) { var $results = $("#task-save-results"); if (show) { $results.css("display", "").css("position", "").css("color", color).text(message); } else { $results.css("display", "none").css("position", "absolute").text(""); } }
Screenshots
Details | Successful Save |
---|---|
![]() |
![]() |
Editing and Saving a Developer
The code for editing and saving a developer is similar to editing and saving a sprint and a task. Instead of providing a very similar write-up as I did for a editing and saving a sprint and a task, here are some screenshots. Take a look at the code at: Kendo UI ASP.NET Sample Applications, Source Code tab.
Details | Successful Save |
---|---|
![]() |
![]() |
Conclusion
Editing and saving data in a mobile application is easy to do. The biggest challenge is trying to getting everything to fit.
References
- Kendo UI Mobile and ASP.NET MVC : Building the Mobile Task Manager, Part 1 - Getting Started
- Kendo UI Mobile and ASP.NET MVC : Building the Mobile Task Manager, Part 2 - Displaying Data
- JSON.NET