Thursday 9 February 2012

Implementing XML HTTP AJAX with JSON in ASP.NET (Cascading Dropdown, retain the list and selection)


Previously I was blogged one post related to Implementing XML HTTP AJAX with JSON concept in ASP.NET Web Pages. But I forgot to include some more functionality such as retaining the UI content created in client side after post back, retaining dropdown box list and selection after post back etc. This post provides examples by implementing the same functionalities.

Before going to the actual implementation, let us understand the UI requirement of this example.
  1. Require an UI with three cascading dropdown boxes.
  2. The first dropdown box should show list of Countries from the database. This dropdown will be filled when loading the page. (So if post back happening, this dropdown box will not lose the lists and selected index)
  3. The second dropdown box should show list of Cities which are under the Country selected in the first dropdown. As this dropdown require the Country dropdown selected value, this will be filled once the first dropdown selected.
    So on selection of the first dropdown, there will be an AJAX call to the server to get the list of cities using JSON and the values will be filled using JavaScript. As this dropdown lists are created in client side, this lists and selected index will lose when the post back happening.
  4. The third dropdown box should show list of Customers located under City selected in the second dropdown. This dropdown will also act like same as second dropdown i.e., once the City dropdown selected, this drop down box will be filled. As the dropdown list are created in client side, the list and selected index will not retain when post back happening.
  5. There will be a Process button, which is a server control will call a server event on click. So when the user presses this button it PostBack the page.
  6. There will be a GridView, will show list of orders raised for the customer. This will be filled when the Process button clicked.
In this implementation, normally we will get the following issues.
  1. The dropdown box City and Country lists are added in the client side. The concept is, when any post back happens the content created in the client side will lose its state. So when Process button clicked the two dropdown box list will lose and it will show empty only once the page rendered.
  2. As the dropdown box lists are created in client side (JavaScript/JQuery), the server won’t know what the list added are and its values. So when Process button clicked, we will not be able to find the Selected Index or Selected Value of those dropdown boxes.
We have various ways to solve this issue, but I feel the easy way is to use hidden controls to save the state of the controls created in the client side before post back happening and restore it once the post back completed.

For example, in our implementation the post back is happening when clicking the Process button. So the following steps needed to retain the state of the dropdown boxes.
  1. On click of the Process button, it should first call the JavaScript function to save the state of the dropdown boxes (such as the lists and the selected Index) then call the server method. This can be done by defining OnClientClick function of the button.
  2. Declare a div control for each dropdown box and name unique id. So one div control (ex – divCity) will have once dropdown control (DDLCity).
  3. Declare a hidden control for each dropdown box to hold the state of the dropdown box. This control should be defined in outside of the div control declared in above step.
  4. To get the state of the dropdown box we can take the innerHTML script of the div control. So the innerHTML will give its dropdown box lists. (Note - The script won’t include the Selected property for the dropdown. So change script to have Selected property for selected Index)
  5. Assign the HTML script to the hidden control declared for that dropdown box. So the dropdown box state is now with the hidden control. (Note – assigning the actual script will raise issue when post back. So it should be encrypted and assigned.)
  6. Define an OnLoad JavaScript function for the page, so once the post back completed it will call this function. This function will assign the HTML script to the corresponding div control. So the dropdown box will be filled again with Selected Index. (after decrypting the script)
So the screen, it looks like retaining the state of the controls created on client side. The implementation follows.

The ASPX script – (Default.aspx)
<div>
    <table>
        <tr>
            <td><b>Country</b></td>
            <td><asp:DropDownList runat="server" ID="DDLCountry" Width="250px" DataTextField="Country" DataValueField="Country" onchange="BindCity(this.id)">
                </asp:DropDownList>
            </td>
        </tr>
        <tr>
            <td><b>City</b></td>
            <td>
                <div id="divCity">
                    <asp:DropDownList runat="server" ID="DDLCity" Width="250px" onchange="BindCustomer(this.id)">
                        <asp:ListItem Text="Select" Value="0"></asp:ListItem>
                    </asp:DropDownList>
                </div>
                <asp:HiddenField ID="hndCityDropdown" runat="server" Value="" />
            </td>
        </tr>
        <tr>
            <td><b>Customer</b></td>
            <td>
                <div id="divCustomer">
                    <asp:DropDownList runat="server" ID="DDLCustomer" Width="250px">
                        <asp:ListItem Text="Select" Value="0"></asp:ListItem>
                    </asp:DropDownList>
                </div>
                <asp:HiddenField ID="hndCustomerID" runat="server" Value="0" />
                <asp:HiddenField ID="hndCustomerDropdown" runat="server" Value="" />
            </td>
        </tr>
        <tr>
            <td colspan="2" style="text-align:right">
                <asp:Button ID="btnProcess" Text="Process" runat="server" Width="100px" 
                    onclick="btnProcess_Click" OnClientClick="AssignHiddenValues()" />
            </td>
        </tr>
    </table>
    <asp:GridView ID="grdViewOrders" runat="server"
        AllowPaging="True" AutoGenerateColumns="False" TabIndex="1"
        DataKeyNames="OrderID" Width="100%" GridLines="None"
        CellPadding="3" AllowSorting="True"
        onpageindexchanging="grdViewOrders_PageIndexChanging" 
        BackColor="White" BorderColor="White" BorderWidth="2px" 
        BorderStyle="Ridge" CellSpacing="1">
        <Columns>
            <asp:BoundField DataField="OrderID" HeaderText="Order ID" />
            <asp:BoundField DataField="CustomerID" HeaderText="Customer ID"  />
            <asp:BoundField DataField="OrderDate" HeaderText="Order Date" DataFormatString="{0:dd-MMMM-yyyy}" />
            <asp:BoundField DataField="RequiredDate" HeaderText="Required Date" DataFormatString="{0:dd-MMMM-yyyy}" />
            <asp:BoundField DataField="ShippedDate" HeaderText="Shipped Date" DataFormatString="{0:dd-MMMM-yyyy}" />
            <asp:BoundField DataField="Freight" HeaderText="Freight" />
            <asp:BoundField DataField="ShipName" HeaderText="Ship Name" />
        </Columns>
        <FooterStyle BackColor="#C6C3C6" ForeColor="Black" />
        <PagerStyle BackColor="#C6C3C6" ForeColor="Black" HorizontalAlign="Right" />
        <RowStyle BackColor="#DEDFDE" ForeColor="Black" />
        <SelectedRowStyle BackColor="#9471DE" ForeColor="White" Font-Bold="True" />
        <HeaderStyle BackColor="#4A3C8C" Font-Bold="True" ForeColor="#E7E7FF" />
        <SortedAscendingCellStyle BackColor="#F1F1F1" />
        <SortedAscendingHeaderStyle BackColor="#594B9C" />
        <SortedDescendingCellStyle BackColor="#CAC9C9" />
        <SortedDescendingHeaderStyle BackColor="#33276A" />
    </asp:GridView>
</div>
If you take the City dropdown box, it has divCity to get the innerHTML (and to get assigned back) and hndCityDropdown for holding the html script of the div.

As like the same in Customer drop down, but additionally we have hndCustomerID for getting the Customer Id in the server. Because as the Customer dropdown created in the client side, the selected index also will known at server code.

Define a onload event for the page and define the load event.
<body onload="load()" onunload="unload()">
</body>
The other JavaScript functions for getting the dropdown lists using JSON object.
var is_ie = (navigator.userAgent.indexOf('MSIE') >= 0) ? 1 : 0;
var is_ie5 = (navigator.appVersion.indexOf("MSIE 5.5") != -1) ? 1 : 0;
var xmlHttp;

var vDDLCountryClientID;
var vDDLCityClientID;
var vDDLCustomerClientID;

var vhndCustomerId;
var vhndCityDropdown;
var vhndCustomerDropdown;

function load() {

    vDDLCountryClientID = '<%= DDLCountry.ClientID %>';
    vDDLCityClientID = '<%= DDLCity.ClientID %>';
    vDDLCustomerClientID = '<%= DDLCustomer.ClientID %>';

    vhndCustomerId = '<%= hndCustomerID.ClientID %>';
    vhndCityDropdown = '<%= hndCityDropdown.ClientID %>';
    vhndCustomerDropdown = '<%= hndCustomerDropdown.ClientID %>';

    if (document.getElementById(vhndCityDropdown).value.length > 0)
        document.getElementById('divCity').innerHTML = unescape(document.getElementById(vhndCityDropdown).value);

    if (document.getElementById(vhndCustomerDropdown).value.length > 0)
        document.getElementById('divCustomer').innerHTML = unescape(document.getElementById(vhndCustomerDropdown).value);

}
function unload() {
// Use this when no other way to find an event before post back. Because some browser wont fire this event
}

/* This function requests the HTTPRequest, will be used to render the Dynamic content html markup 
* and it will call HandleBindCityResponse to handle the response
*/
function BindCity(id) {
    var url = 'GetAJAXResponse.aspx?CountryID=' + document.getElementById(id).value + '&CallType=CityList';
    xmlHttp = createAjaxObject();
    if (xmlHttp) {
        xmlHttp.open('get', url, true);
        xmlHttp.onreadystatechange = HandleBindCityResponse;
        xmlHttp.send(null);
    }
}

/* This function is used to handler the http response 
 * This Function will bind the City in the dropdown. 
 * When the request is in Server, the dropdown will be Loading... and once client got the response it will bind the items.*/
function HandleBindCityResponse() {

    // If Response completed
    if (xmlHttp.readyState == 4) {

        // Here is the response
        var strResponse = xmlHttp.responseText;

        // Parsing the JSON Response
        // As I generated JSON from Collection, I am getting it back as Array here
        var ArrCities = eval("(" + strResponse + ")");

        // Getting the city Dropdown
        var DDLCity = document.getElementById(vDDLCityClientID);
        while (DDLCity.childNodes.length > 0)
            DDLCity.removeChild(DDLCity.childNodes[0]); // Removing every list item

        var option = document.createElement("option");
        option.value = "0"; 
        option.innerHTML = "Select";
        DDLCity.appendChild(option);

        // Looping the array
        for (var intIndex = 0; intIndex < ArrCities.length; intIndex++) {

            var option = document.createElement("option");
            option.value = ArrCities[intIndex]["CityID"];
            option.innerHTML = ArrCities[intIndex]["CityName"];
            DDLCity.appendChild(option);

        }
        document.getElementById(vDDLCityClientID).disabled = false;
        xmlHttp.abort();
    }
    else {
        ResetDropdown(vDDLCityClientID, 'Loading...');
        ResetDropdown(vDDLCustomerClientID, 'Select City');
    }
}

/* This function requests the HTTPRequest, will be used to render the Dynamic content html markup 
 * and it will call HandleCustomerListResponse to handle the response
 */
function BindCustomer(id) {
    if (document.getElementById(id).value.length > 0) {
        var url = 'GetAJAXResponse.aspx?CountryID=' + document.getElementById(vDDLCountryClientID).value + '&CityID=' + document.getElementById(id).value + '&CallType=CustomerList';
        xmlHttp = createAjaxObject();
        if (xmlHttp) {
            xmlHttp.open('get', url, true);
            xmlHttp.onreadystatechange = HandleCustomerListResponse;
            xmlHttp.send(null);
        }
    }
}

/* This function is used to handler the http response
 * The function will fetch the details of selected item and populate in the respective field.
 * The the request is on the server, there will be a Waiting for your request message in the screen. */
function HandleCustomerListResponse() {

    // If Response completed
    if (xmlHttp.readyState == 4) {

        // Here is the response
        var strResponse = xmlHttp.responseText;

        // Parsing the JSON Response
        // As I generated JSON from Collection, I am getting it back as Array here
        var ArrCustomer = eval("(" + strResponse + ")");

        // Getting the customer Dropdown
        var DDLCustomer = document.getElementById(vDDLCustomerClientID);
        while (DDLCustomer.childNodes.length > 0)
            DDLCustomer.removeChild(DDLCustomer.childNodes[0]); // Removing every list item

        var option = document.createElement("option");
        option.value = "0";
        option.innerHTML = "Select";
        DDLCustomer.appendChild(option);

        // Looping the array
        for (var intIndex = 0; intIndex < ArrCustomer.length; intIndex++) {

            var option = document.createElement("option");
            option.value = ArrCustomer[intIndex]["CustomerID"];
            option.innerHTML = ArrCustomer[intIndex]["CustomerName"];
            DDLCustomer.appendChild(option);

        }
        document.getElementById(vDDLCustomerClientID).disabled = false;
        xmlHttp.abort();
    }
    else {
        ResetDropdown(vDDLCustomerClientID, 'Loading');
    }
}

/* function to create Ajax object */
function createAjaxObject() {
    var ro;
    var browser = navigator.appName;
    if (browser == "Microsoft Internet Explorer") {
        if (xmlHttp != null) {
            xmlHttp.abort();
        }
        ro = new ActiveXObject("Microsoft.XMLHTTP");
    }
    else {
        if (xmlHttp != null) {
            xmlHttp.abort();
        }
        ro = new XMLHttpRequest();
    }
    return ro;
}

/* Get the XML Http Object */
function GetXmlHttpObject(handler) {
    var objXmlHttp = null;
    if (is_ie) {
        var strObjName = (is_ie5) ? 'Microsoft.XMLHTTP' : 'Msxml2.XMLHTTP';

        try {
            objXmlHttp = new ActiveXObject(strObjName);
            objXmlHttp.onreadystatechange = handler;
        }
        catch (e) {
            alert('Object could not be created');
            return;
        }
    }
    return objXmlHttp;
}

function xmlHttp_Get(xmlhttp, url) {
    xmlhttp.open('GET', url, true);
    xmlhttp.send(null);
}

// function to assign Product Value in the Hidden control
// Because as the dropdown items are added in Client side, it wont be accessible in code behind.
// As this event called just before post back, we can get the dropdown box HTML script and store it in hidden control. So it can be replaced after post back completed.
function AssignHiddenValues() {
    
    if (document.getElementById(vDDLCustomerClientID).value.length > 0) {

        var browserName = navigator.appName;
        if (browserName == "Microsoft Internet Explorer") {

            // Assigning to Customer ID hidden control for getting the Customer ID for fetching records for Grid (here Customer Id is the key for filtering the records)
            document.getElementById(vhndCustomerId).value = document.getElementById(vDDLCustomerClientID).value;
            
            // Assigning the City dropdown box HTML script for hidden control for retaining the dropdown box list after post back
            document.getElementById(vhndCityDropdown).value = escapeTxt(document.getElementById(vDDLCityClientID).outerHTML.replace('value="' + document.getElementById(vDDLCityClientID).value + '"', 'selected value="' + document.getElementById(vDDLCityClientID).value + '"'))

            // Assigning the Customer dropdown box HTML script for hidden control for retaining the dropdown box list after post back
            document.getElementById(vhndCustomerDropdown).value = escapeTxt(document.getElementById(vDDLCustomerClientID).outerHTML.replace('value="' + document.getElementById(vDDLCustomerClientID).value + '"', 'selected value="' + document.getElementById(vDDLCustomerClientID).value + '"'))
        }
        else {
            // Assigning to Customer ID hidden control for getting the Customer ID for fetching records for Grid (here Customer Id is the key for filtering the records)
            document.getElementById(vhndCustomerId).value = document.getElementById(vDDLCustomerClientID).value;

            // Assigning the City dropdown box HTML script for hidden control for retaining the dropdown box list after post back
            document.getElementById(vhndCityDropdown).value = escapeTxt(outerHTML(document.getElementById(vDDLCityClientID)).replace('value="' + document.getElementById(vDDLCityClientID).value + '"', 'selected value="' + document.getElementById(vDDLCityClientID).value + '"'))

            // Assigning the Customer dropdown box HTML script for hidden control for retaining the dropdown box list after post back
            document.getElementById(vhndCustomerDropdown).value = escapeTxt(outerHTML(document.getElementById(vDDLCustomerClientID)).replace('value="' + document.getElementById(vDDLCustomerClientID).value + '"', 'selected value="' + document.getElementById(vDDLCustomerClientID).value + '"'))
        }
        return true;
    }
    else {
        alert('Please select Product and Press Process');
        return false;
    }
}

function outerHTML(node) {
    return node.outerHTML || new XMLSerializer().serializeToString(node);
}

function ResetDropdown(id, msg) {
    document.getElementById(id).disabled = true;

    // Getting the Customer Dropdown
    var DDL = document.getElementById(id);
    while (DDL.childNodes.length > 0)
        DDL.removeChild(DDL.childNodes[0]); // Removing every list item

    var option = document.createElement("option");
    option.value = "0";
    option.innerHTML = msg;
    DDL.appendChild(option);
}

// CONVERTS *ALL* CHARACTERS INTO ESCAPED VERSIONS.
function escapeTxt(os) {
    var ns = '';
    var t;
    var chr = '';
    var cc = '';
    var tn = '';
    for (i = 0; i < 256; i++) {
        tn = i.toString(16);
        if (tn.length < 2) tn = "0" + tn;
        cc += tn;
        chr += unescape('%' + tn);
    }
    cc = cc.toUpperCase();
    os.replace(String.fromCharCode(13) + '', "%13");
    for (q = 0; q < os.length; q++) {
        t = os.substr(q, 1);
        for (i = 0; i < chr.length; i++) {
            if (t == chr.substr(i, 1)) {
                t = t.replace(chr.substr(i, 1), "%" + cc.substr(i * 2, 2));
                i = chr.length;
            }
        }
        ns += t;
    }
    return ns;
}
The C# code behind
protected void Page_Load(object sender, EventArgs e)
{
    if (!Page.IsPostBack)
    {
        BindCountryDropdown();
    }
}

protected void btnProcess_Click(object sender, EventArgs e)
{
    grdViewOrders.PageIndex = 0;
    BindGrid();
}

protected void grdViewOrders_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
    grdViewOrders.PageIndex = e.NewPageIndex;
    BindGrid();
}

private void BindGrid()
{
    if (hndCustomerID.Value.Trim().Length &gt; 0)
    {
        using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings[&quot;SQLConnection&quot;].ConnectionString))
        {

            SqlDataAdapter dataAdapter = new SqlDataAdapter(
                    &quot;SELECT OrderID, CustomerID, OrderDate, RequiredDate, ShippedDate, Freight, ShipName &quot; +
                    &quot;FROM Orders WHERE CustomerID = '&quot; + hndCustomerID.Value + &quot;'&quot;, connection);

            DataSet ds = new DataSet();
            connection.Open();
            dataAdapter.Fill(ds);

            grdViewOrders.DataSource = ds.Tables[0];
            grdViewOrders.DataBind();
        }
    }
}

public void BindCountryDropdown()
{
    using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings[&quot;SQLConnection&quot;].ConnectionString))
    {
        SqlDataAdapter adaptor = new SqlDataAdapter(&quot;SELECT DISTINCT Country FROM Customers&quot;, connection);

        DataSet ds = new DataSet();

        connection.Open();
        adaptor.Fill(ds);

        DDLCountry.DataSource = ds.Tables[0];
        DDLCountry.DataBind();
        DDLCountry.Items.Insert(0, new ListItem(&quot;Select&quot;, &quot;0&quot;));
    }
}

Below is the ASPX page code behind which returns the JSON object (GetAJAXResponse. This page called using XML HTTP AJAX from the JavaScript function.
protected void Page_Load(object sender, EventArgs e)
{
    string strResponse = string.Empty;

    if (Request.QueryString["CallType"] != null)
    {
        string strCallType = Request.QueryString["CallType"].ToString();
        if (strCallType == "CityList")
        {
            if (Request.QueryString["CountryID"] != null)
                strResponse = GetCityList(Request.QueryString["CountryID"].ToString());
        }
        if (strCallType == "CustomerList")
        {
            if ((Request.QueryString["CountryID"] != null) && 
                (Request.QueryString["CityID"] != null))
                strResponse = GetCustomerList(Request.QueryString["CountryID"].ToString(), Request.QueryString["CityID"].ToString());
        }
    }

    Response.Clear();
    Response.ContentType = "text/xml";
    Response.Write(strResponse);
    Response.End();
}

public string GetCityList(string strCountryID)
{
    using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLConnection"].ConnectionString))
    {

        SqlCommand command = new SqlCommand(
                "SELECT DISTINCT City FROM Customers WHERE Country = '" + strCountryID + "'", connection);

        connection.Open();
        SqlDataReader dr = command.ExecuteReader(CommandBehavior.CloseConnection);

        IList<City> cityList = new List<City>();
        while (dr.Read())
        {
            City productView = new City();
            productView.CityID = dr["City"].ToString();
            productView.CityName = dr["City"].ToString();
            cityList.Add(productView);
        }

        // I am delaying the response to see the Loading... message on the dropdown
        System.Threading.Thread.Sleep(1000);

        System.Web.Script.Serialization.JavaScriptSerializer objSerializer = 
                new System.Web.Script.Serialization.JavaScriptSerializer();

        return objSerializer.Serialize(cityList);
    }
}

public string GetCustomerList(string strCountryID, string strCityID)
{
    using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLConnection"].ConnectionString))
    {

        SqlCommand command = new SqlCommand(
                "SELECT CustomerID, CompanyName FROM Customers WHERE Country = '" + strCountryID + "' AND City = '" + strCityID + "'", connection);

        connection.Open();
        SqlDataReader dr = command.ExecuteReader(CommandBehavior.CloseConnection);

        IList<Customer> customerList = new List<Customer>();
        while (dr.Read())
        {
            Customer customer = new Customer();
            customer.CustomerID = dr["CustomerID"].ToString();
            customer.CustomerName = dr["CompanyName"].ToString();
            customerList.Add(customer);
        }

        // I am delaying the response to see the Loading... message on the dropdown
        System.Threading.Thread.Sleep(1000);

        System.Web.Script.Serialization.JavaScriptSerializer objSerializer =
                new System.Web.Script.Serialization.JavaScriptSerializer();

        return objSerializer.Serialize(customerList);
    }
}
Below is the output of the page.







Second Implementation:

Addition to the above implementation, I wish to show the details of the customer when Customer dropdown selected. So when selecting a customer from Customer dropdown, it will call the server to get the details of the customer as a JSON object and show the details on the page.

As the details content created at the client side, this also will lose when post back happened. So I implemented the same way done previously to hold the state of the content.

I have not given the source code for this implementation in this page as it is almost same as above code. But the downloadable source code contains this implementation (Example1.aspx).

download the working copy of the source code n C# here and in VB here.

This code is been tested with IE 9.0, Firefox 10.0.

The output of the second implementation will be -

The video output of this example will be -




3 Responses to “Implementing XML HTTP AJAX with JSON in ASP.NET (Cascading Dropdown, retain the list and selection)”

Post a Comment