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

This website is no longer actively supported

Written by John DeVightJohn DeVight on 24 Feb 2012 14:40

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

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've gotten numerous requests to write a wiki on how to use Kendo UI in an ASP.NET MVC application which can be found at: Kendo UI and ASP.NET MVC : Building a Task Manager. I 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. After I finished, I decided that it would be helpful to provide an example of using Kendo UI in an ASP.NET WebForms application. I found that there were subtle differences in implementing Kendo UI in a WebForms application vs. an MVC application.

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 Web Application" called TaskManager.WebForms.
  2. I deleted all the javascript files found in the TaskManager.WebForms/Scripts folder. At the time of this article, the files I deleted were:
    1. jquery-1.4.1-vsdoc.js
    2. jquery-1.4.1.js
    3. jquery-1.4.1.min.js
  3. Download Kendo UI Web Open Source License Edition.
  4. From the Kendo UI download, I copied the following files into the TaskManager.WebForms/Scripts folder:
    1. js/jquery.min.is
    2. js/kendo.web.min.js
  5. In the TaskManager.WebForms application , in the "Styles" folder, I created a folder called "kendo".
  6. From the Kendo UI download, I copied the following files / folders into the TaskManager.WebForms/Styles/kendo folder:
    1. styles/kendo.common.min.css
    2. styles/kendo.silver.min.css
    3. styles/Silver (folder)

Setting up the Master Page

I added a new Master Page and called it TaskManager.Master. I then 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.WebForms/Scripts/Master.js
  5. In the Master page, at the end of the body, I added a reference to Master.js. I then added a script tag that calls the Master.init function.
<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="TaskManager.master.cs" Inherits="TaskManager.TaskManager" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Task Manager</title>
    <link href="Styles/kendo/kendo.common.min.css" rel="Stylesheet" type="text/css" />
    <link href="Styles/kendo/kendo.silver.min.css" rel="Stylesheet" type="text/css" />
    <link href="Styles/Site.css" rel="Stylesheet" type="text/css" />
 
    <script src="Scripts/jquery.min.js" type="text/javascript"></script>
    <script src="Scripts/kendo.web.min.js" type="text/javascript"></script>
 
    <script type="text/javascript">
        var _rootUrl = '<%=string.Format("{0}{1}/", Request.Url.GetLeftPart(UriPartial.Authority), HttpRuntime.AppDomainAppVirtualPath)%>';
    </script>
 
    <asp:ContentPlaceHolder ID="head" runat="server">
    </asp:ContentPlaceHolder>
</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">
                    <asp:ContentPlaceHolder ID="content" runat="server">
                    </asp:ContentPlaceHolder>
                </div>
            </div>
        </div>
    </div>
 
    <script src="Scripts/Master.js" type="text/javascript"></script>
    <script type="text/javascript">
        Master.init();
    </script>
</body>
</html>

The Master.js JavaScript file

In the Master.js file, I created a namespace called "Master". I then created a function called Master.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. Populate the "navigationPane" with the Navigation.aspx page.
    • Important to Note: The contents of a pane in the Kendo UI Splitter can be specified in the initialization of the Splitter by setting the "contentUrl" attribute to the name of a page. However, with an ASPX page, Kendo UI puts the page in an iFrame. I didn't want the Navigation.aspx page to appear in an iFrame, so after the Splitter was initialized, I populated the "navigationPane" using the jQuery.load function.
  4. set an interval that calls the Layout.resize function every 100 miliseconds.

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

The default.aspx Page

I created a "WebForm using Master Page" at TaskManager.WebForms/default.aspx and selected the TaskManager.Master as the Master page.

Creating the Navigation Page

I created a WebForm at TaskManager.WebForms/Views/Navigation.aspx. The Navigation WebForm displays a PanelBar with a list of sprints and a list of developers that can be selected to view the details. A PanelBar consists of a unordered lists with list items. In the Page_Load, I get the sprints and the developers from the SprintRepository and bind each of them to Repeater controls. The sprintRepeater, ItemTemplate renders each sprint as a list item with the ""tm-sprint" css class. The developerRepeater, ItemTemplate renders each developer as a list item with the "tm-developer" css class. At the end of the page, I call the Master.Navigation.init function to initialize the unordered list as a PanelBar.

Here is the code behind:

using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using TaskManager.Models;
using TaskManager.Data;
 
namespace TaskManager.Views
{
    public partial class NavigationPage : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            this.sprintRepeater.DataSource = SprintRepository.GetSprints();
            this.sprintRepeater.DataBind();
 
            this.developerRepeater.DataSource = SprintRepository.GetDevelopers();
            this.developerRepeater.DataBind();
        }
    }
}

Here is the markup code:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Navigation.aspx.cs" Inherits="TaskManager.Views.NavigationPage" %>
 
<form runat="server">
    <ul id="navPanelBar">
        <asp:Repeater id="sprintRepeater" runat="server">
            <HeaderTemplate>
                <li>Sprints
                    <ul>
            </HeaderTemplate>
            <ItemTemplate>
                        <li id="<%# Eval("Id")%>" class="tm-sprint"><%# string.Format("Sprint {0}", Eval("Id"))%></li>
            </ItemTemplate>
            <FooterTemplate>
                    </ul>
                </li>
            </FooterTemplate>
        </asp:Repeater>
 
        <asp:Repeater id="developerRepeater" runat="server">
            <HeaderTemplate>
                <li class="tm-developer-pane">Developers
                    <ul>
            </HeaderTemplate>
            <ItemTemplate>
                        <li id="<%# Eval("Id")%>" class="tm-developer"><%# string.Format("{0} {1}", Eval("FirstName"), Eval("LastName"))%></li>
            </ItemTemplate>
            <FooterTemplate>
                    </ul>
                </li>
            </FooterTemplate>
        </asp:Repeater>
    </ul>
</form>
 
<script type="text/javascript">
    Master.Navigation.init();
</script>

Adding Code for the Navigation page in Master.js

I decided to add the code for the Navigation page in Master.js. I created a namespace called Master.Navigation. In the Navigation page, in the script tag, the Master.Navigation.init function is called. The Master.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 Master.Navigation.navPanelBar_onSelect event handler.

The Master.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 jQuery.load function is used to load the Sprint page into the "contentPane" of the "contentSplitter". If the selected item has the "tm-developer" class, then the jQuery.load function is used to load the Developer page into the "contentPane" of the "contentSplitter".

  • Important to Note The Kendo UI Splitter has an ajaxRequest function that can be used to load the contents of a splitter pane. However, with a WebForm, this doesn't work, so I use the jQuery.load function instead.

Here is the code:

Master.Navigation = {}
 
Master.Navigation.init = function () {
    $("#navPanelBar").kendoPanelBar({
        expandMode: "single",
        select: Master.Navigation.navPanelBar_onSelect
    });
}
 
Master.Navigation.navPanelBar_onSelect = function (e) {
    if ($(e.item).hasClass("tm-sprint")) {
        $("#contentPane").load(_rootUrl + "Views/Sprint.aspx?id=" + $(e.item).attr("id"));
    } else if ($(e.item).hasClass("tm-developer")) {
        $("#contentPane").load(_rootUrl + "Views/Developer.aspx?id=" + $(e.item).attr("id"));
    }
}

Creating the Sprint Page

I created a WebForm at TaskManager.WebForms/Views/Sprint.aspx that gets rendered in the "contentPane" of the "contentSplitter" when a Sprint is selected in the PanelBar. In the Page_Load I get the sprint using the id that was passed in by the Request.QueryString. I then set the Id HiddenField and the Start and End date TextBox fields. After the page is rendered in the "contentPane", I populate 2 data sources for the Task grid. I decided to use Page Methods to make jQuery.ajax calls to the code behind for the Sprint page. An article on how to do this can be found at: How Do I Retrieve Data from a Page Method?.

Here is the code behind for the Sprint page:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using TaskManager.Models;
using TaskManager.Data;
using System.Web.Script.Serialization;
 
namespace TaskManager.Views
{
    public partial class SprintPage : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (Request.RequestType != "POST")
            {
                Sprint sprint = SprintRepository.GetSprints().Where(s => s.Id == Convert.ToInt32(Request.QueryString["id"])).First();
                this.Id.Value = sprint.Id.ToString();
                this.Start.Text = sprint.Start.ToString();
                this.End.Text = sprint.End.ToString();
            }
        }
 
        [System.Web.Services.WebMethod]
        public static IList<Developer> GetDevelopers()
        {
            return SprintRepository.GetDevelopers().ToList();
        }
 
        [System.Web.Services.WebMethod]
        public static IList<Task> GetSprintTasks(int id)
        {
            return SprintRepository.GetTasks().Where(t => t.SprintId == id).ToList();
        }
 
        [System.Web.Services.WebMethod]
        public static void UpdateSprint(string json)
        {
            Sprint sprint = new JavaScriptSerializer().Deserialize(json, typeof(Sprint)) as Sprint;
            SprintRepository.SaveSprint(sprint);
        }
 
        [System.Web.Services.WebMethod]
        public static void SaveTasks(string models)
        {
            IList<Task> tasks = new JavaScriptSerializer().Deserialize(models, typeof(IList<Task>)) as IList<Task>;
            foreach (Task task in tasks)
            {
                SprintRepository.SaveTask(task, false);
            }
        }
 
        [System.Web.Services.WebMethod]
        public static void DeleteTasks(string models)
        {
            IList<Task> tasks = new JavaScriptSerializer().Deserialize(models, typeof(IList<Task>)) as IList<Task>;
            foreach (Task task in tasks)
            {
                SprintRepository.SaveTask(task, true);
            }
        }
    }
}

In the markup 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 HiddenField.
    2. The start and end dates are TextBox 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.WebForms/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 markup code:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Sprint.aspx.cs" Inherits="TaskManager.Views.SprintPage" EnableEventValidation="false" %>
 
<div style="margin: 5px 5px 5px 5px;">
    <a id="saveButton" class="k-button" href="#">Save</a>
</div>
 
<form id="sprintForm" runat="server">
    <asp:HiddenField id="Id" runat="server"></asp:HiddenField>
 
    <fieldset><legend>Dates</legend>
        <div class="tm-field" style="width:100px;">Start Date:</div><div class="tm-field"><asp:TextBox id="Start" runat="server"></asp:TextBox></div>
        <div class="tm-field" style="width:20px;">&nbsp;</div>
        <div class="tm-field" style="width:100px;">End Date:</div><div class="tm-field"><asp:TextBox id="End" runat="server"></asp:TextBox></div>
    </fieldset>
</form>
 
<fieldset><legend>Tasks</legend>
    <div id="taskGrid"></div>
</form>
 
<script src="Scripts/Sprint.js" type="text/javascript"></script>
<script type="text/javascript">
    Sprint.init();
</script>

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 DataSources to Read From a PageMethod

The Kendo UI DataSource needs to have a few extra settings added to the DataSource.transport.read configuration to get it to work with a PageMethod. The Kendo UI DataSource by default uses the "GET" method to read data. However, PageMethods only work with the "POST" method. Also, the dataType and contentType need to be explicitly set. Any "parameters" sent to the PageMethod must be a string representation of a JSON object. However, if the "parameters" are set in the "data" configuration setting, they don't get sent to the PageMethod. Therefore, I had to set the data in the "beforeSend" event handler. When the data is returned, it is stored in a attribute called "d". So, in the Kendo UI DataSource, schema configuration setting, the data setting must be set to indicate that the data can be found in the "d" attribute.

When saving data, the update, create and destroy transport settings must also have the type, contentType and dataType explicitly set. Additionally, the parameterMap must be setup to handle the following:

  • When the model objects are read from a PageMethod, the data type for the model is added to the JSON returned from the PageMethod as an attribute called "__type". This has to be removed before saving back to the server.
  • The models to be saved must be a string representation of the JSON objects, replace all double quotes with a backslash + double quote, and the JSON objects must be assigned to an attribute that corresponds to the parameter in the PageMethod. For example, in the SaveTasks PageMethod identified above, the parameter passed in is "models". In the Data Source, transport.parameterMap, the data that is returned is assigned to the "models" attribute.

Here is the code:

The transport.parameterMap setting.

// Get the tasks assigned to the sprint.
Sprint._taskDataSource = new kendo.data.DataSource({
    transport: {
        read: {
            beforeSend: function (xhr, s) {
                s.data = JSON.stringify({id: Sprint._id});
            },
            type: "POST",
            url: _rootUrl + "Views/Sprint.aspx/GetSprintTasks",
            contentType: "application/json; charset=utf-8",
            dataType: "json"
        },
        update: {
            type: "POST",
            url: _rootUrl + "Views/Sprint.aspx/SaveTasks",
            contentType: "application/json; charset=utf-8",
            dataType: "json"
        },
        create: {
            type: "POST",
            url: _rootUrl + "Views/Sprint.aspx/SaveTasks",
            contentType: "application/json; charset=utf-8",
            dataType: "json"
        },
        destroy: {
            type: "POST",
            url: _rootUrl + "Views/Sprint.aspx/DeleteTasks",
            contentType: "application/json; charset=utf-8",
            dataType: "json"
        },
        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") {
                $.each(data.models, function (idx, model) {
                    // When the data was retrieved from the server, an extra attribute of __type
                    // was added to each model.  Remove this to allow the data to be deserialized
                    // when it gets to the server.
                    delete model.__type;
 
                    // Set the SprintId for each model.
                    model.SprintId = Sprint._id;
                });
                return '{ models: "' + JSON.stringify(data.models).replace(/\"/g, '\\"') + '"}';
            } else {
                return data;
            }
        }
    },
    batch: true,
    schema: {
        data: "d",
        model: {
            id: "Id",
            fields: {
                Id: { type: "number" },
                Type: { type: "number" },
                EstimatedHours: { type: "number" },
                Description: { type: "string" },
                AssignedToId: { type: "number", defaultValue: 1 },
                SprintId: { type: "number", defaultValue: Sprint._id }
            }
        }
    }
});

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) {
            // Setup the hidden field for the value.
            $("<input />")
                .attr("class", "k-input k-textbox tm-field-value")
                .attr("id", options.field)
                .attr("name", options.field)
                .css({
                    position: "absolute",
                    display: "none"
                })
                .appendTo(container);
 
            // Display a dropdownlist.
            $("<input />")
                .attr("id", options.field + "DropDownList")
                .appendTo(container)
                .kendoDropDownList({
                    dataSource: [
                        { text: "Scenerio", value: "0" },
                        { text: "Change Request", value: "1" }
                    ],
                    change: function (e) {
                        // Find the field that will hold the actual value and set it to the selected value.
                        $(this.element).closest('td').children('input.tm-field-value').val(this.value());
                    },
                    index: options.model.data.Type
                });
        }
    }, {
        field: "Description"
    }, {
        field: "AssignedToId",
        title: "Assigned To",
        template: "#= Sprint.sprintGrid_displayPerson(AssignedToId) #",
        editor: function (container, options) {
            // Setup the hidden field for the value.
            $("<input />")
                .attr("class", "k-input k-textbox tm-field-value")
                .attr("id", options.field)
                .attr("name", options.field)
                .css({
                    position: "absolute",
                    display: "none"
                })
                .appendTo(container);
 
            // Figure out the index for the person assigned to this task.
            var devIdx = 0;
            $.each(Sprint._developerDataSource.data(), function (idx, developer) {
                if (developer.Id == options.model.data.AssignedToId) {
                    devIdx = idx;
                    return false;
                }
            });
 
            // Display a dropdownlist or developers.
            var dropdownid = options.field + "DropDownList";
            $("<input />")
                .attr("id", dropdownid)
                .appendTo(container)
                .kendoDropDownList({
                    index: devIdx,
                    dataSource: Sprint._developerDataSource,
                    change: function (e) {
                        // Find the field that will hold the actual value and set it to the selected value.
                        $(this.element).closest('td').children('input.tm-field-value').val(this.value());
                    },
                    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 ($("#" + dropdownid + "-listHeading").length == 0) {
                $("<div/>")
                    .attr("id", dropdownid + "-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($("#" + dropdownid + "-list"));
            }
        }
    }, {
        field: "EstimatedHours",
        title: "Estimated Hours"
    }, {
        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:

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, I needed to have a dropdown list for some of the columns. This took a bit of work to figure out. For the Developer column, to display a dropdownlist of developers, I did the following in the column's editor function:

  • Create a hidden field that contains the "AssignedToId". This is the field that the Grid will use to update the AssignedToId for the task. The position is set to absolute so that it doesn't take up any room on the page.
// Setup the hidden field for the value.
$("<input />")
    .attr("class", "k-input k-textbox tm-field-value")
    .attr("id", options.field)
    .attr("name", options.field)
    .css({
     position: "absolute",
     display: "none"
    })
    .appendTo(container);
 
// Figure out the index for the person assigned to this task.
var devIdx = 0;
$.each(Sprint._developerDataSource.data(), function (idx, developer) {
    if (developer.Id == options.model.data.AssignedToId) {
     devIdx = idx;
     return false;
    }
});
  • Get the index of the developer model found in the Sprint._developerDataSource for the developer currently assigned to the task. I do this so that when I initialize the dropdownlist with the list of developers, I can set the index of the developer that should be selected.
// Figure out the index for the person assigned to this task.
var devIdx = 0;
$.each(Sprint._developerDataSource.data(), function (idx, developer) {
    if (developer.Id == options.model.data.AssignedToId) {
     devIdx = idx;
     return false;
    }
});
  • Create a Kendo UI DropDownList with the list of developers.
    1. When the DropDownList is initialized, I implement an event handler for the change event so that when a developer is selected, the hidden field for the AssignedToId is updated with the selected developer's Id.
    2. I define a template for the list items that formats each item as a table with the FirstName, LastName and Title of each developer.
// Display a dropdownlist or developers.
var dropdownid = options.field + "DropDownList";
$("<input />")
    .attr("id", dropdownid)
    .appendTo(container)
    .kendoDropDownList({
     index: devIdx,
     dataSource: Sprint._developerDataSource,
     change: function (e) {
     // Find the field that will hold the actual value and set it to the selected value.
     $(this.element).closest('td').children('input.tm-field-value').val(this.value());
     },
     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".
// Display column headings for the dropdown list.
if ($("#" + dropdownid + "-listHeading").length == 0) {
    $("<div/>")
     .attr("id", dropdownid + "-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($("#" + dropdownid + "-list"));
}

Saving the Sprint

To save the sprint, I post an AJAX call to the Sprint code behind, UpdateSprint PageMethod. The data that is passed to it must be a string representation of a JSON object. To do this, I added a JavaScript file called json.extensions.js to the TaskManager.WebForms/Scripts folder. In the TaskManager.Master, in the HTML head section, I added a reference to the json.extensions.js file:

<script src="Scripts/json.extensions.js" type="text/javascript"></script>

I created a javascript function that extends the JSON object called serializeElements that serializes an array of elements. Here is the code:

(function () {
    JSON.serializeElements = function (elements) {
        var json = {};
        for (var idx = 0; idx < elements.length; idx++) {
            var name = elements[idx].name;
            if (json[name]) {
                json[name] = json[name] + ',' + elements[idx].value;
            } else {
                json[name] = elements[idx].value;
            }
        }
        return json;
    }
} ());

In Sprint.js, the Sprint.save function creates a JSON array of all the input fields (except the __VIEWSTATE field) and sends the data to the UpdateSprint PageMethod. 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:

/// <summary>Save the sprint information.  Then save the sprint tasks.</summary>
Sprint.save = function () {
    $.ajax({
        type: "POST",
        url: _rootUrl + "Views/Sprint.aspx/UpdateSprint",
        dataType: "json",
        contentType: "application/json; charset=utf-8",
        data: '{ json: "' + JSON.stringify(JSON.serializeElements($("#sprintForm").find(":input[name]").not("input[name='__VIEWSTATE']"))).replace(/\"/g, '\\"') + '"}'
    })
    .done(function () {
        // Tell the task data source to save all pending changes.
        Sprint._taskDataSource.sync();
    })
    .fail(function () {
        alert("fail");
    });
}

When the data gets to the server, it needs to be deserialized from string to a C# object. I use the System.Web.Script.Serialization.JavaScriptSerializer to do that.

Creating the Developer Page

I created a WebForm at TaskManager.WebForms/Views/Developer.aspx that gets rendered in the "contentPane" of the "contentSplitter" when a Developer is selected in the PanelBar. In the Page_Load I get the developer using the id that was passed in by the Request.QueryString. I then set the Id HiddenField and the FirstName and LastName TextBox fields.

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/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 behind for the Developer page:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using TaskManager.Data;
using TaskManager.Models;
using System.Web.Script.Serialization;
 
namespace TaskManager.Views
{
    public partial class DeveloperPage : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (Request.RequestType != "POST")
            {
                Developer developer = SprintRepository.GetDevelopers().Where(d => d.Id == Convert.ToInt32(Request.QueryString["id"])).First();
                this.Id.Value = developer.Id.ToString();
                this.FirstName.Text = developer.FirstName;
                this.LastName.Text = developer.LastName;
                this.Title.Text = developer.Title;
            }
        }
 
        [System.Web.Services.WebMethod]
        public static IList<Task> GetDeveloperTasks(int id)
        {
            return SprintRepository.GetTasks().Where(t => t.AssignedToId == id).ToList();
        }
 
        [System.Web.Services.WebMethod]
        public static void UpdateDeveloper(string json)
        {
            Developer developer = new JavaScriptSerializer().Deserialize(json, typeof(Developer)) as Developer;
            SprintRepository.SaveDeveloper(developer);
        }
    }
}

In the markup 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 HiddenField.
    2. The first name, last name and title are TextBox fields.
  3. Added a div with the id of "taskGrid" that is initialized as a grid.
  4. Added a new javascript file at: TaskManager.WebForms/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:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Developer.aspx.cs" Inherits="TaskManager.Views.DeveloperPage" EnableEventValidation="false" %>
 
<div style="margin: 5px 5px 5px 5px;">
    <a id="saveButton" class="k-button" href="#">Save</a>
</div>
 
<form id="developerForm" runat="server">
    <asp:HiddenField id="Id" runat="server"></asp:HiddenField>
 
    <fieldset><legend>Developer</legend>
        <div class="tm-field" style="width:100px;">First Name:</div><div class="tm-field"><asp:TextBox id="FirstName" runat="server"></asp:TextBox></div>
        <div class="tm-field" style="width:20px;">&nbsp;</div>
        <div class="tm-field" style="width:100px;">Last Name:</div><div class="tm-field"><asp:TextBox id="LastName" runat="server"></asp:TextBox></div><br />
 
        <div class="tm-field" style="width:100px;margin-top:5px;">Title</div><div class="tm-field"><asp:TextBox id="Title" runat="server"></asp:TextBox></div>
    </fieldset>
</form>
 
<fieldset><legend>Tasks</legend>
    <div id="taskGrid"></div>
</form>
 
<script src="Scripts/Developer.js" type="text/javascript"></script>
<script type="text/javascript">
    Developer.init();
</script>

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 Developer.save function.

Saving the Developer

To save the developer, I post an AJAX call to the UpdateDeveloper PageMethod. The data that is passed to it is the string representation of the input fields found in the "developerForm" that contains the developer Id, FirstName, LastName and Title. When the AJAX call is done, I call the Master.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 Master.Navigation.expandDeveloperItem that executes after the PanelBar has been reloaded that expands the "Developers" pane. Here is the code:

/// <summary>Save the developer information.</summary>
Developer.save = function () {
    $.ajax({
        type: "POST",
        url: _rootUrl + "Views/Developer.aspx/UpdateDeveloper",
        dataType: "json",
        contentType: "application/json; charset=utf-8",
        data: '{ json: "' + JSON.stringify(JSON.serializeElements($("#developerForm").find(":input[name]").not("input[name='__VIEWSTATE']"))).replace(/\"/g, '\\"') + '"}'
    })
    .done(function () {
        // Display the updates to the developer's name in the navigation panelbar.
        Master.Navigation.syncNavPanelBar(Master.Navigation.expandDeveloperItem);
    });
}

The Master.Navigation.syncNavPanelBar reloads the PanelBar by reloading the "navigationPane" pane in the "contentSplitter" splitter using the jQUery.load function. This causes the Navigation partial view to be reloaded. After the "navigationPane" is loaded, the function pointer that was passed in gets executed. Here is the code:

Master.Navigation.syncNavPanelBar = function (funcptr) {
    $('#navigationPane').load(_rootUrl + "Views/Navigation.aspx", funcptr);
}
 
Master.Navigation.expandDeveloperItem = function () {
    var panelBar = $("#navPanelBar").data("kendoPanelBar");
    panelBar.expand(panelBar.element.find("li.tm-developer-pane"));
}

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 WebForms 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. PageMethods were ideal to use with jQuery ajax and load functions.

References


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