Kendo UI Web and ASP.NET MVC : Building a Task Manager

This website is no longer actively supported

Written by John DeVightJohn DeVight on 21 Feb 2012 16: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 Web -> TaskManager.

Overview

Note: This article shows how to use the Kendo UI Web JavaScript Library with ASP.NET MVC and NOT the Kendo UI for ASP.NET MVC library.

The Kendo UI development team is focusing thier attention on developing the best client UI widgets possible. However, the development communitiy needs to know how to use Kendo UI with server languages. I gotten numerous requests to write a wiki on how to use Kendo UI in an ASP.NET MVC application and here it is. I've developed a simple "Task Manager" application that provides a user with the ability to update development sprints and the tasks associated with the development sprints.

This sample application demonstrates the use of the following Kendo UI components: Splitter, PanelBar, DropDownList, DatePicker, Grid and DataSource.

Creating the Project

I created the project with the following steps:

  1. In Visual Studio, create a new ASP.NET MVC application called TaskManager.
  2. Download Kendo UI.
  3. From the Kendo UI download, I copied the following files into the TaskManager.Mvc/Scripts folder:
    1. js/jquery.min.is
    2. js/keno.all.min.js
  4. In the TaskManager application I created a new folder called "kendo" in the TaskManager.Mvc/Content folder.
  5. From the Kendo UI download, I copied the following files / folders into the TaskManager.Mvc/Content:
    1. styles/kendo.common.min.css
    2. styles/kendo.silver.min.css
    3. styles/Silver (folder)

Setting up the Layout View

I opened the TaskManager.Mvc/Views/Shared/_Layout.cshtml view and made the following changes:

  1. Added references to the Kendo UI styles and scripts in the html head section.
  2. Added a script tag in the head section that contains a variable called _rootUrl for the root url of the application.
  3. Added the HTML for the "main" Splitter that has 2 panes, one for the title and one for the content called "mainPane". The "mainPane" contains a Splitter that has 2 panes, one for the navigation and one for the content.
  4. Added a new javascript file at: TaskManager.Mvc/Scripts/Layout.js
  5. In the Layout view, at the end of the body, I added a reference to Layout.js. I then added a script tag that calls the Layout.init function.
<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <link href="@Url.Content("~/Content/kendo/kendo.common.min.css")" rel="stylesheet" type="text/css" />
    <link href="@Url.Content("~/Content/kendo/kendo.silver.min.css")" rel="stylesheet" type="text/css" />
 
    <script src="@Url.Content("~/Scripts/jquery.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/kendo.all.min.js")" type="text/javascript"></script>
 
    <script type="text/javascript">
        var _rootUrl = "@Url.Content("~")";
    </script>
</head>
 
<body>
    <div id="mainSplitter">
        <div id="titlePane">
            <h3>Task Manager</h3>
        </div>
        <div id="mainPane">
            <div id="contentSplitter">
                <div id="navigationPane">
                </div>
 
                <div id="contentPane">
                    @RenderBody()
                </div>
            </div>
        </div>
    </div>
 
    <script src="@Url.Content("~/Scripts/Layout.js")" type="text/javascript"></script>
    <script type="text/javascript">
        Layout.init();
    </script>
</body>
</html>

The Layout.js JavaScript file

In the Layout.js file, I created a namespace called "Layout". I then created a function called Layout.init that does the following:

  1. Initialize the "mainSplitter" div as a Kendo UI Splitter.
  2. Initialize the "contentSplitter" div as a Kendo UI Splitter.
  3. set an interval that calls the Layout.resize function every 100 miliseconds.

I then created the Layout.resize function to check the size of the window, and if it has changed, then resize the Splitters to fit the window.

Here is the code:

var Layout = {}
 
Layout._height = null;
 
/// <summary>Initialize the layout view.</summary>
Layout.init = function () {
    // Initialize the main splitter.
    $("#mainSplitter").kendoSplitter({
        orientation: "vertical",
        panes: [
            { size: "50px", resizable: false },
        ]
    });
 
    // Initialize the content splitter.
    $("#contentSplitter").kendoSplitter({
        panes: [
            { contentUrl: _rootUrl + "Home/Navigation", size: "200px", collapsible: true }
        ]
    });
 
    // Hide th e scrollbar in the main splitter.
    $('#mainSplitter').children('div.k-pane').css('overflow-y', 'hidden');
 
    // Call the Layout.resize function every 100 miliseconds.
    setInterval("Layout.resize()", 100);
}
 
/// <summary>Check the size of the window, and if it has changed, then resize the splitters to fit the window.</summary>
Layout.resize = function () {
    var height = $(window).height();
 
    // Has the window height changed?
    if (height != Layout._height) {
        Layout._height = height;
 
        // Resize the main splitter.
        var mainSplitter = $('#mainSplitter');
        mainSplitter.height(height - 25);
        mainSplitter.resize();
 
        // Resize the content splitter.
        var contentSplitter = $('#contentSplitter');
        contentSplitter.height(height - 77);
        contentSplitter.resize();
    }
}

Creating the Navigation Partial View

I created a partial view at TaskManager.Mvc/Views/Shared/_Navigation.cshtml. The Navigation partial view displays a PanelBar with a list of sprints and a list of developers that can be selected to view the details. The partial view takes an instance of the Navigation class as the model. The Navigation class contains a list of sprints and a list of developers. A PanelBar consists of a unordered lists with list items. In the view, the sprints are rendered as list items. Each sprint list item has the "tm-sprint" class. The developers are rendered as list items. Each developer list item has the "tm-developer" class.

Here is the code:

@using TaskManager.Models
@model TaskManager.ViewModels.Navigation
@{
    Layout = null;
}
 
<ul id="navPanelBar">
    <li>Sprints
        <ul>
        @{foreach (Sprint sprint in Model.Sprints)
          {
            <li id="@sprint.Id" class="tm-sprint">@string.Format("Sprint {0}", sprint.Id)</li>
          }
        }
        </ul>
    </li>
 
    <li class="tm-developer-pane">Developers
        <ul>
        @{foreach (Developer developer in Model.Developers)
          {
            <li id="@developer.Id" class="tm-developer">@string.Format("{0} {1}", developer.FirstName, developer.LastName)</li>
          }
        }
        </ul>
    </li>
</ul>
 
<script type="text/javascript">
    Layout.Navigation.init();
</script>

Adding the Navigation Controller Action.

In the Home Controller, I added a Controller Action called Navigation that returns the _Navigation partial view and an instance of the Navigation class as the model. Here is the code:

public ActionResult Navigation()
{
    return View("_Navigation", new Navigation());
}

Adding Code for the Navigation Partial View in Layout.js

I decided to add the code for the navigation partial view in Layout.js. I created a namespace called Layout.Navigation. In the Navigation partial view, in the script tag, the Layout.Navigation.init function is called. The Layout.Navigation.init function initializes the "navPanelBar" unordered list as a Kendo UI PanelBar. When the navPanelBar is initialized, I set the select event to the Layout.Navigation.navPanelBar_onSelect event handler.

The Layout.Navigation.navPanelBar_onSelect function looks to see what class is associated with the item that was selected. If it has the "tm-sprint" class, then the Splitter ajaxRequest function is used to load the Sprint partial view into the "contentPane" of the "contentSplitter". If the selected item has the "tm-developer" class, then the Splitter ajaxRequest function is used to load the Developer partial view into the "contentPane" of the "contentSplitter".

Here is the code:

Layout.Navigation = {}
 
Layout.Navigation.init = function () {
    $("#navPanelBar").kendoPanelBar({
        expandMode: "single",
        select: Layout.Navigation.navPanelBar_onSelect
    });
}
 
Layout.Navigation.navPanelBar_onSelect = function (e) {
    if ($(e.item).hasClass("tm-sprint")) {
        var splitter = $("#contentSplitter").data("kendoSplitter");
        splitter.ajaxRequest("#contentPane", _rootUrl + "Home/Sprint/", { id: $(e.item).attr("id") });
    } else if ($(e.item).hasClass("tm-developer")) {
        var splitter = $("#contentSplitter").data("kendoSplitter");
        splitter.ajaxRequest("#contentPane", _rootUrl + "Home/Developer/", { id: $(e.item).attr("id") });
    }
}

Creating the Sprint Partial View

I created a partial view at TaskManager.Mvc/Views/Home/_Sprint.cshtml that gets rendered in the "contentPane" of the "contentSplitter" when a Sprint is selected in the PanelBar. An instance of the Sprint model is passed into the the Sprint partial view and the Sprint partial view displays the sprint details and the list of tasks associated with the sprint. In the partial view I did the following:

  1. Added a Save button. I wanted the Save button to use the Kendo UI theme, so I made the save button an anchor with the Kendo UI k-button class.
  2. Added a form containing the sprint Id, start date and end date.
    1. The sprint Id is a hidden field.
    2. The start and end dates are input fields that are initialized as Kendo UI DatePicker controls.
  3. Added a div with the id of "taskGrid" that is initialized as a grid.
  4. Added a new javascript file at: TaskManager.Mvc/Scripts/Sprint.js
  5. Added a reference to Sprint.js. I then added a script tag that calls the Sprint.init function.

Here is the code:

@model TaskManager.Models.Sprint
@{
    Layout = null;
}
 
<div style="margin: 5px 5px 5px 5px;">
    <a id="saveButton" class="k-button" href="#">Save</a>
</div>
 
<form id="sprintForm">
    @Html.HiddenFor(m => m.Id)
 
    <fieldset><legend>Dates</legend>
        <div class="tm-field" style="width:100px;">@Html.LabelFor(m => m.Start)</div><div class="tm-field">@Html.TextBoxFor(m => m.Start)</div>
        <div class="tm-field" style="width:20px;">&nbsp;</div>
        <div class="tm-field" style="width:100px;">@Html.LabelFor(m => m.End)</div><div class="tm-field">@Html.TextBoxFor(m => m.End)</div>
    </fieldset>
</form>
 
<fieldset><legend>Tasks</legend>
    <div id="taskGrid"></div>
</form>
 
<script src="@Url.Content("~/Scripts/Sprint.js")" type="text/javascript"></script>
<script type="text/javascript">
    Sprint.init();
</script>

Adding the Sprint Controller Action.

In the Home Controller, I added a Controller Action called Sprint that gets the sprint from the SprintRepository and returns the _Sprint partial view and the sprint as the model. Here is the code:

public ActionResult Sprint(int id)
{
    Sprint sprint = SprintRepository.GetSprints().Where(s => s.Id == id).First();
    return View("_Sprint", sprint);
}

The Sprint.js JavaScript File

In the Sprint.js file, I created a namespace called "Sprint". I then created a function called Sprint.init that does the following:

  1. Initialize the input for Sprint.Start as a Kendo UI DatePicker.
  2. Initialize the input for Sprint.End as a Kendo UI DatePicker.
  3. Initialize the Sprint._developerDataSource data source. The grid displays a list of tasks for the sprint. Each task has an "AssignedToId" that is the Id for a developer. The list of developers is needed so that the name of the developer can be displayed in the grid instead of the "AssignedToId".
  4. After the Sprint._developerDataSource data source is initialized, I initialize the Sprint._taskDataSource data source. This data source contains the tasks for the sprint.
  5. Initialize the "taskGrid" div as a Kendo UI Grid.
  6. Bind the saveButton click event to the Sprint.save function.

Configuring the Sprint._taskDataSource Data Source

The Sprint._taskDataSource data source saves changes to the server. However, when the data is posted to the server, each property of each model in the list of models appears as a different parameter in the HttpContext.Request.Params. To made it easier to work with, in the Sprint._taskDataSource, parameterMap configuration setting, I define a function that looks at the type of transport, and if it is not read, then it transforms the data into JSON object where the models attribute is set to the stringified array of models to be saved. I also configured the data source to use batch editing. By doing this, I can save all the sprint information at one time.

Client-side validation is performed on the EstimatedHours, Description and CompletedDate fields. The Description and CompletedDate fields have the required configuration setting set to true. The EstimatedHours field implements a custom validator so that if a value is entered, the EstimatedHours cannot be negative. The CompletedDate field also implements a custom validator so that the CompletedDate cannot be outside of the Sprint start and end dates.

Here is the code:

Sprint._taskDataSource = new kendo.data.DataSource({
    transport: {
        read: {
            url: _rootUrl + "Home/GetSprintTasks/" + Sprint._id
        },
        update: {
            type: "POST",
            url: _rootUrl + "Home/SaveTasks"
        },
        create: {
            type: "POST",
            url: _rootUrl + "Home/SaveTasks"
        },
        destroy: {
            type: "POST",
            url: _rootUrl + "Home/SaveTasks?delete=true"
        },
        parameterMap: function (data, type) {
            // If update, create or destroy, then serialize the data
            // as JSON so that it can be more easily read on the server.
            if (type != "read") {
                return { models: JSON.stringify(data.models) };
            } else {
                return data;
            }
        }
    },
    batch: true,
    schema: {
        model: {
            id: "Id",
            fields: {
                Id: { type: "number" },
                Type: { type: "number" },
                EstimatedHours: {
                    type: "number",
                    validation: {
                        custom: function (input) {
                            if (input.length > 0) {
                                var valid = true;
                                if (input.val().length > 0) {
                                    valid = input.val() >= 0;
                                    input.attr("data-custom-msg", "The Estimated Hours cannot be negative");
                                }
                                return valid;
                            } else {
                                return true;
                            }
                        }
                    }
                },
                Description: {
                    type: "string",
                    validation: { required: true }
                },
                AssignedToId: { type: "number", defaultValue: 1 },
                SprintId: { type: "number", defaultValue: Sprint._id },
                CompletedDate: {
                    type: "date",
                    validation: {
                        required: true,
                        custom: function (input) {
                            if (input.length > 0) {
                                var startDate = $("#Start").data("kendoDatePicker").value();
                                var endDate = $("#End").data("kendoDatePicker").value();
                                var completedDate = new Date(input.val());
                                input.attr("data-custom-msg", "The Completed Date should be between the Start Date and the End Date");
                                return (completedDate >= startDate && completedDate <= endDate);
                            } else {
                                return true;
                            }
                        }
                    }
                }
            }
        }
    }
});

Working with Columns in the Grid

There were a few challenges to working with the Kendo UI Grid. Before diving into them, here is the source code for initializing the grid:

$("#taskGrid").kendoGrid({
    dataSource: Sprint._taskDataSource,
    height: 400,
    editable: true,
    toolbar: ["create"],
    columns: [{
        field: "Type",
        template: "#= Type == 0 ? 'Scenerio' : 'Change Request' #",
        width: "200px",
        editor: function (container, options) {
            $("<input />")
                .attr("data-bind", "value:Type")
                .appendTo(container)
                .kendoDropDownList({
                    dataSource: [
                        { text: "Scenerio", value: "0" },
                        { text: "Change Request", value: "1" }
                    ],
                    dataTextField: "text",
                    dataValueField: "value"
                });
        }
    }, {
        field: "Description"
    }, {
        field: "AssignedToId",
        title: "Assigned To",
        template: "#= Sprint.sprintGrid_displayPerson(AssignedToId) #",
        editor: function (container, options) {
            // Setup the hidden field for the value.
            var dropdown = $("<input />")
                .attr("id", "AssignedToIdDropDownList")
                .attr("data-bind", "value:AssignedToId")
                .appendTo(container)
                .kendoDropDownList({
                    dataSource: Sprint._developerDataSource,
                    template: "<table><tr><td width='100px'>${ FirstName }</td><td width='100px'>${ LastName }</td><td width='100px'>${ Title }</td></tr></table>",
                    dataValueField: "Id",
                    dataTextField: "Name"
                });
 
            // Display column headings for the dropdown list.
            if ($("#AssignedToIdDropDownList-listHeading").length == 0) {
                $("<div/>")
                    .attr("id", "AssignedToIdDropDownList-listHeading")
                    .html("<table><tr><td width='100px'><b>First Name</b></td><td width='100px'><b>Last Name</b></td><td width='100px'><b>Title</b></td></tr></table>")
                    .prependTo($("#AssignedToIdDropDownList-list"));
            }
        }
    }, {
        field: "EstimatedHours",
        title: "Estimated Hours"
    }, {
        field: "CompletedDate",
        title: "Completed",
        template: "#= kendo.toString(CompletedDate, 'MM/dd/yyyy') #"
    }, {
        command: "destroy",
        title: " ",
        width: "110px"
    }]
});

Displaying the Developer Name for the AssignedToId

Each task in the grid has an AssignedToId. This is a foreign key to a developer. To display the developer's name instead of the I use the column template to call a Sprint.sprintGrid_displayPerson function. I pass in the AssignedToId. In the Sprint.sprintGrid_displayPerson function, I use the data source get function to get the developer from the Sprint._developerDataSource data source.

Here is the code for the code:

Sprint.sprintGrid_displayPerson = function (id) {
    var person = Sprint._developerDataSource.get(id);
    if (person == undefined) {
        return "";
    } else {
        return person.data.Name;
    }
}

Configuring the DropDownList Editor for the Developer Column

When a cell is edited in the grid, the Kendo UI Grid does a good job with most types of columns. However, a dropdown list requires a custom editor. For the AssignedToId, I did the following in the column's editor function:

  • Create an input field. The input field has to have the "data-bind" attribute set to "value:" + the name of the field.
  • Initialize the input field as a Kendo UI DropDownList with the list of developers. In the configuration for the DropDownList, I define a template for the list items that formats each item as a table with the FirstName, LastName and Title of each developer.
var dropdown = $("<input />")
    .attr("id", "AssignedToIdDropDownList")
    .attr("data-bind", "value:AssignedToId")
    .appendTo(container)
    .kendoDropDownList({
        dataSource: Sprint._developerDataSource,
        template: "<table><tr><td width='100px'>${ FirstName }</td><td width='100px'>${ LastName }</td><td width='100px'>${ Title }</td></tr></table>",
        dataValueField: "Id",
        dataTextField: "Name"
    });
  • After the Kendo UI DropDownList is initialized, Kendo UI creates a hidden div with the list of items to display for the DropDownList. The name of the div is the name of the DropDownList + "-list". I decided it might be nice to display some "column headings" in my DropDownList. To do this, I locate the div that contains the list of items, and prepend a div with a table in it that has the "column headings".
if ($("#AssignedToIdDropDownList-listHeading").length == 0) {
    $("<div/>")
        .attr("id", "AssignedToIdDropDownList-listHeading")
        .html("<table><tr><td width='100px'><b>First Name</b></td><td width='100px'><b>Last Name</b></td><td width='100px'><b>Title</b></td></tr></table>")
        .prependTo($("#AssignedToIdDropDownList-list"));
}

Saving the Sprint

To save the sprint, I post an AJAX call to the Home Controller UpdateSprint Action. The data that is passed to it is the serialized "sprintForm" that contains the sprint Id as well as the Start, and End dates. When the AJAX call is done, I tell the Sprint._taskDataSource data source, that has all the updates to the tasks, to "sync" with the server. This will send all the tasks that were created, updated or destroyed to the server. Here is the code:

Sprint.save = function () {
    $.ajax({
        type: "POST",
        dataType: "json",
        data: $("#sprintForm").serialize(),
        url: _rootUrl + "Home/UpdateSprint"
    })
    .done(function (result) {
        if (result.message.length == 0) {
            // Tell the task data source to save all pending changes.
            Sprint._savingTasks = true;
            Sprint._taskDataSource.sync();
        } else {
            // Display the error in a modal window.
            var window = $("#messageWindow")
                .text(result.message)
                .kendoWindow({
                    title: "Sprint Save Error",
                    height: "200px",
                    width: "300px",
                    draggable: false,
                    modal: false,
                    visible: false,
                    resizable: false
                }).data("kendoWindow");
            window.center();
            window.open();
        }
    })
    .fail(function () {
        alert("fail");
    });
}

Home Controller UpdateSprint Action

In the Sprint._taskDataSource data source, parameterMap configuration setting, I serialized the "updates" as JSON object and assign it to the "models" attribute.

{ models: JSON.stringify(data.models) }

The UpdateSprint method is used for create, update and destroy. The UpdateSprint takes 2 parameters, tasks and delete. The tasks is the list of tasks to either create, update or destroy. The delete flag is an optional flag that is true if the list of tasks passed in are to be deleted. The delete flag gets set in the Sprint._taskDataSource, destroy url. Here is the code:

[AcceptVerbs(HttpVerbs.Post)]
[JsonParameter(Input="models", Output="tasks", DataType=typeof(IList<Task>))]
public JsonResult SaveTasks(IList<Task> tasks, bool delete = false)
{
    foreach (Task task in tasks)
    {
        SprintRepository.SaveTask(task, delete);
    }
    return Json(new { result = "complete" });
}

JsonParameterAttribute Class

The JSON object that was serialized and set to the "models" attribute in the Sprint._taskDataSource data source doesn't automatically get converted into IList<Task> tasks. That's where the JsonParameter comes in. The Home Controller UpdateSprint Action will recieve a HttpContext.Request.Params parameter of type string with a key of models. To make it a bit easier to work with, I created an ActionFilterAttribute called JsonParameterAttribute. This converts the HttpContext.Request.Params parameter from a stringified JSON to a C# object. The C# object is then assigned to the appropriate ActionExecutingContext.ActionParameters parameter. In the case of the UpdateSprint method, the JsonParameterAttribute converts the HttpContext.Request.Params "models" parameter into IList<Task> and assigns it to the ActionExecutingContext.ActionParameters "tasks" parameter.

Here is the code:

using System;
using System.Web.Mvc;
using System.Web.Script.Serialization;
 
namespace TaskManager.Extensions
{
    public class JsonParameterAttribute : ActionFilterAttribute
    {
        public string Input { get; set; }
        public string Output { get; set; }
        public Type DataType { get; set; }
 
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            string json = filterContext.HttpContext.Request.Params[this.Input].ToString();
            filterContext.ActionParameters[this.Output] = new JavaScriptSerializer().Deserialize(json, this.DataType);
        }
    }
}

Creating the Developer Partial View

I created a partial view at TaskManager.Mvc/Views/Home/_Developer.cshtml that gets rendered in the "contentPane" of the "contentSplitter" when a Developer is selected in the PanelBar. An instance of the Developer model is passed into the the Developer partial view and the Developer partial view displays the developer details and the list of tasks associated with the developer. In the partial view I did the following:

  1. Added a Save button. I wanted the Save button to use the Kendo UI theme, so I made the save button an anchor with the Kendo UI k-button class.
  2. Added a form containing the developer Id, FirstName, LastName, and Title.
    1. The developer Id is a hidden field.
    2. The first name, last name and title are input fields.
  3. Added a div with the id of "taskGrid" that is initialized as a grid.
  4. Added a new javascript file at: TaskManager.Mvc/Scripts/Developer.js
  5. Added a reference to Developer.js. I then added a script tag that calls the Developer.init function.

Here is the code:

@model TaskManager.Models.Developer
@{
    Layout = null;
}
 
<div style="margin: 5px 5px 5px 5px;">
    <a id="saveButton" class="k-button" href="#">Save</a>
</div>
 
<form id="developerForm">
    @Html.HiddenFor(m => m.Id)
 
    <fieldset><legend>Developer</legend>
        <div class="tm-field" style="width:100px;">@Html.LabelFor(m => m.FirstName)</div><div class="tm-field">@Html.TextBoxFor(m => m.FirstName)</div>
        <div class="tm-field" style="width:20px;">&nbsp;</div>
        <div class="tm-field" style="width:100px;">@Html.LabelFor(m => m.LastName)</div><div class="tm-field">@Html.TextBoxFor(m => m.LastName)</div><br />
 
        <div class="tm-field" style="width:100px;margin-top:5px;">@Html.LabelFor(m => m.Title)</div><div class="tm-field">@Html.TextBoxFor(m => m.Title)</div>
    </fieldset>
</form>
 
<fieldset><legend>Tasks</legend>
    <div id="taskGrid"></div>
</form>
 
<script src="@Url.Content("~/Scripts/Developer.js")" type="text/javascript"></script>
<script type="text/javascript">
    Developer.init();
</script>

Adding the Developer Controller Action.

In the Home Controller, I added a Controller Action called Developer that gets the developer from the SprintRepository and returns the _Developer partial view and the developer as the model. Here is the code:

public JsonResult GetDeveloper(int id)
{
    Developer developer = SprintRepository.GetDevelopers().Where(d => d.Id == id).First();
    return Json(developer, JsonRequestBehavior.AllowGet);
}

The Developer.js JavaScript File

In the Developer.js file, I created a namespace called "Developer". I then created a function called Developer.init that does the following:

  1. Initialize the Developer._taskDataSource data source. The data source contains the tasks for the developer.
  2. Initialize the "taskGrid" div as a Kendo UI Grid.
  3. Bind the saveButton click event to the Sprint.save function.

Saving the Developer

To save the developer, I post an AJAX call to the Home Controller UpdateDeveloper Action. The data that is passed to it is the serialized "developerForm" that contains the developer Id, FirstName, LastName and Title. When the AJAX call is done, I call the Layout.Navigation.syncNavPanelBar function to tell the PanelBar to reload so that the updates to the developer's name (if there were any) are reflected in the PanelBar. I pass a function pointer to the Layout.Navigation.expandDeveloperItem that executes after the PanelBar has been reloaded that expands the "Developers" pane. Here is the code:

Developer.save = function () {
    $.ajax({
        type: "POST",
        dataType: "json",
        data: $("#developerForm").serialize(),
        url: _rootUrl + "Home/UpdateDeveloper"
    })
    .done(function () {
        // Display the updates to the developer's name in the navigation panelbar.
        Layout.Navigation.syncNavPanelBar(Layout.Navigation.expandDeveloperItem);
    });
}

The Layout.Navigation.syncNavPanelBar reloads the PanelBar by reloading the "navigationPane" pane in the "contentSplitter" splitter using the Splitter.ajaxRequest function. This causes the Navigation partial view to be reloaded. However, before I reload the splitter, I bind the function pointer that was passed in to the "contentSplitter" splitter's contentLoad event. Here is the code:

Layout.Navigation.syncNavPanelBar = function (funcptr) {
    var splitter = $("#contentSplitter").data("kendoSplitter");
 
    if (funcptr != undefined) {
        splitter.bind("contentLoad", funcptr);
    }
    splitter.ajaxRequest("#navigationPane", _rootUrl + "Home/Navigation");
}

When the Navigation partial view is reloaded, the Layout.Navigation.expandDeveloperItem function that was bound to the "contentSplitter" splitter's contentLoad event is called. In the Layout.Navigation.expandDeveloperItem function, I get the "navPanelBar" and tell it to expand the "Developers" pane. I then unbind the Layout.Navigation.expandDeveloperItem function from the "contentSplitter" splitter's contentLoad event. Here is the code:

Layout.Navigation.expandDeveloperItem = function (e) {
    var splitter = $("#contentSplitter").data("kendoSplitter");
    var panelBar = $("#navPanelBar").data("kendoPanelBar");
    panelBar.expand(panelBar.element.find("li.tm-developer-pane"));
    splitter.unbind("contentLoad", Layout.Navigation.expandDeveloperItem);
}

Conclusion

Kendo UI is really nice to work with and with a bit of investigating, I was able to quickly build a simple ASP.NET MVC application using Kendo UI. I really like the Kendo UI DataSource as it reminds me of when I worked with ExtJS and I find that having the data source separate from the control provides more flexibility as well as consistency across all the controls.

References


Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License