Decrease Font Size
Increase Font Size
   BLOG

ASP-NET AJAX MultiHandleSliderExtender - Slide by Year and Month

by bryian 6. March 2010 18:27

ASP.NET How to use MultiHandleSliderExtender|

multi handle slider extender|

MultiHandleSlider Control|

MultiHandleSliderExtender|

Ajax MultiHandleSliderExtender|

ASP.NET Charts storage mode |

AJAX Control Toolkit|

OnClientDrag, OnClientDragEnd, RaiseChangeOnlyOnMouseUp

Last updated - 03/09/2010

Replaced the PageMethods with client-side JavaScript Array. I received a feedback from the CodeProject member quoted "Here is the problem the dragging is sending many requests to the server and the last one gets executed but then that means the database has to work that extra bit and also the IIS." I thinks he has a point, so I decided to store the year and month data for each RowNumber on the client-side JavaScript array. This will allow the sliders' range label be updated through the client-side. See New Update 1 section.

Introduction

In this tutorial, I will demonstrate how to use MultiHandleSlider extender to choose or display year and month in a range. This control eliminates the need to use four DropDownlist controls to hold the range values and validation control for validating user selection. Then we will use the Column Chart to display number of cars on Sesame Street based on the selected range values. This chart allows the user to drill down into details for the selected car brands. See figure 1.

Figure 1
MultiHandleSlider results

Getting Started

  1. Download AJAX Control Toolkit Release Notes - May 2009 Release
  2. Download Samples Environment for Chart Controls
  3. Displayed below is the snapshot of the project solution explorer. You are welcome to download this demo.

Figure 2

Project Structure

Back-end database

First, add a new column to the table, name it YearMonth and populate it with the concatenation of the Year and Month data. See figure 3. The original table is on the left. With this setup, we can easily select data within the desired year and month range from the table.

Figure 3
Add column to table

Use ROW_NUMBER function to generate row number for each row of the result set. Then we can use the row number to populate the slider range of values. The query shown below return sixty two rows and this means that the slider range of values goes from one to sixty two. In other words we can set the minimum and maximum properties of the slider to one and sixty two respectively.

Figure 4
Create Row Number

Putting everything together

For simplification's sake, I use XML as Data Source in this tutorial. There are two XML file in the App_Data folder namely CarsList.xml and SliderRange.xml. The former file contains all the data in the table that is shown in figure 3. The latter xml file hold the result set shown in figure 4. Create two custom entity classes named CarsList and Range to hold the public properties. See listing 1.

Listing 1

public class CarsList
{
    public CarsList(){}
    public int CarCount { get; set; }
    public int YearMonth { get; set; }
    public string CarBrand { get; set; }
    public string Date { get; set; }
}
public class Range
{
    public Range(){}
    public string Year { get; set; }
    public string Month { get; set; }
    public int RowNumber { get; set; }
}

Let start by adding a Script Manager on to the page and set the EnablePageMethods and EnablePartialRendering properties to true. By setting the EnablePageMethods property to true, the client script can access the static page methods in an ASP.NET page. The EnablePartialRendering property allows us to specify only the region of the page to be refreshed. Next, drag a TextBox, MultiHandleSliderExtender, four HiddenField and two Label controls on to the page and wrap it inside an UpdatePanel. Set the TargetControlID of the MultiHandleSliderExtender control to the ID of the TextBox control. Add two handles to the control and set its ControlID to rangeStart and rangeEnd respectively. See listing 2. The purpose of the Label controls is to display the selected range values. The HiddenField controls are used to hold the handles' values. Initialize the MultiHandleSliderExtender with the setting shown below.

  • OnClientDrag= Drag, The event raised when the user drags the handle.
  • OnClientDragEnd = DragEnd, The event raised when the user stops dragging the handle.
  • Increment = 1, determines the number of points to increment or decrement the slider values.
  • RaiseChangeOnlyOnMouseUp = true, fires the change event on the extended TextBox only when the left mouse button is released.
  • EnableRailClick = false

Listing 2

<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePageMethods="true" EnablePartialRendering="true" /> 
    <div>
    <asp:UpdatePanel ID="UpdatePanel2" runat="server">
<ContentTemplate>
<table>
    <tr><td colspan="2">
       <asp:TextBox ID="txtSlider" runat="server"></asp:TextBox>  
    
    <cc1:MultiHandleSliderExtender ID="MultiHandleSliderExtender1" runat="server" ShowHandleDragStyle="true"
    BehaviorID="mhSlider" TargetControlID="txtSlider" Length="500" ShowInnerRail="true"
        EnableMouseWheel="false" Increment="1" 
        RaiseChangeOnlyOnMouseUp="true" EnableRailClick="false"
       OnClientDragEnd="DragEnd" OnClientDrag="Drag" 
        ShowHandleHoverStyle="true" Maximum="222" Minimum="1">
        <MultiHandleSliderTargets>
         <cc1:MultiHandleSliderTarget ControlID="rangeStart" />
            <cc1:MultiHandleSliderTarget ControlID="rangeEnd" />
        </MultiHandleSliderTargets>  
   </cc1:MultiHandleSliderExtender>
   <br />
   </td></tr>
   <tr>
   <td><asp:Label ID="lblStartRange" runat="server" Text=""></asp:Label></td>
   <td><asp:Label ID="lblEndRange" runat="server" Text=""></asp:Label> </td>
   </tr>  
    <tr><td>
    <asp:HiddenField ID="rangeEnd" Value="10" runat="server" />
    <asp:HiddenField ID="rangeStart" Value="1" runat="server" />
     <asp:HiddenField ID="hdfTrackRangeStart" runat="server" Value="0" />
 <asp:HiddenField ID="hdfTrackRangeEnd" runat="server" Value="0" />
    </td></tr>
</table>
</ContentTemplate>            
</asp:UpdatePanel>
</div>

Shown below is the client-side code. Dragging the handle will trigger the ASP.NET page method SliderRange to update the lblStartRange and lblEndRange value. Once the users release the handle, the DragEnd function will be executed.

Listing 3

<script type="text/javascript">
    var isDragging = false;
    function Drag(sender, args) {
        GetSliderRange($get("rangeStart").value, $get("rangeEnd").value);
    }

    function DragEnd(sender, args) {
        //prevent postback on slider click
        if ($get("hdfTrackRangeStart").value !== $get("rangeStart").value) {
             __doPostBack("btnLoadChart", "");
        }
        if ($get("hdfTrackRangeEnd").value !== $get("rangeEnd").value && $get("hdfTrackRangeEnd").value !== '0') {
             __doPostBack("btnLoadChart", "");
        }
    }

    function GetSliderRange(startV, endV) {
        PageMethods.SliderRange(startV, endV, this.callback);
    }

    function callback(result) {
        var arrResult = result.split("--");
        $get("lblStartRange").innerHTML = arrResult[0];
        $get("lblEndRange").innerHTML = arrResult[1];
    }
</script>

Create a generic List<T> of Range objects in the code behind and mark it as static so that is accessible from the client script or other static method. Shown below are the contents in the Page_Load event. The PopulateSlider() method read the contents in the SliderRange.xml file and store it in the lstSliderRange. Initialize the MultiHandleSliderExtender minimum and maximum value to 1 and biggest RowNumber in lstSliderRange respectively. See listing 4.

Listing 4

protected static List<Range> lstSliderRange = null;
protected void Page_Load(object sender, EventArgs e)
    {
	  Chart1.Click += new ImageMapEventHandler(Chart1_Click);
        PopulateSlider();

        if (!Page.IsPostBack)
        {
            //slider min and max value
            MultiHandleSliderExtender1.Minimum = 1;
            MultiHandleSliderExtender1.Maximum = 
			int.Parse(lstSliderRange.Max(r => r.RowNumber).ToString());

            //hidden field
            rangeEnd.Value = MultiHandleSliderExtender1.Maximum.ToString();
            rangeStart.Value = MultiHandleSliderExtender1.Minimum.ToString();

		PopulateChart(int.Parse(rangeStart.Value), int.Parse(rangeEnd.Value));
        }
        SetLabel();
    }

Displayed below is the implementation of PopulateSlider() method. The lstSliderRange object is cached to increase performance and its contents are fetched again when the file contents changes. Depending on how often we update the data source, we can cache it based on the changes in the database, folder, file or time based expiration. Read more about ASP.NET Caching technique from here.

Listing 5

//get slider range
    void PopulateSlider()
    {
        //Cache the frequently used data
        if (Cache["Cache_lstSliderRange"] == null)
        {
            XDocument xmlDoc = XDocument.Load(Server.MapPath(Utilities.Instance.SliderRangeXMLPath));
            lstSliderRange = (from c in xmlDoc.Descendants("Range")
                              select new Range
                              {
                                  Month = (string)c.Attribute("Month"),
                                  Year = (string)c.Attribute("Year"),
                                  RowNumber = (int)c.Attribute("RowNumber")
                              }).ToList();

            Cache.Insert("Cache_lstSliderRange", lstSliderRange,
                new System.Web.Caching.CacheDependency(Server.MapPath(Utilities.Instance.SliderRangeXMLPath)));
        }
        else
        {
            lstSliderRange = Cache["Cache_lstSliderRange"] as List<Range>;
        }
    }

The SetLabel() method display the MultiHandleSliderExtender start and end range values in the lblStartRange and lblStartEnd Label controls. The SliderRange method is decorated with [System.Web.Services.WebMethod] making the method accessible from client-side JavaScript. The GetSliderText method accept two parameters, the first parameter refer to the row number and the second parameter refers to the left or right handle. For instance, calling SliderRange(2, 10) will yield "From Year: 2005 Month:02--To Year: 2005 Month:10". First, it will query the lstSliderRange object and retrieve the Year and Month from the result set. If the pos==s, set the text to From and To if pos==e. See listing 6.

Listing 6

//set the slider start and end label
    void SetLabel()
    {
        string[] arrLabel = SliderRange(rangeStart.Value, rangeEnd.Value).Split("--".ToCharArray());
        lblStartRange.Text = arrLabel[0];
        lblEndRange.Text = arrLabel[2];
    }

    [System.Web.Services.WebMethod]
    public static string SliderRange(string start, string end)
    {
        if (lstSliderRange != null)
        {
            return GetSliderText(start, "s") + "--" + GetSliderText(end, "e");
        }
        else
        {
            return "";
        }
    }

    protected static string GetSliderText(string rn, string pos)
    {
        string strRangeText = string.Empty;
        IEnumerable<Range> rangeText;

        rangeText = lstSliderRange.Where(r => r.RowNumber == int.Parse(rn))
            .Select(r => new Range
            {
                Year = r.Year,
                Month = r.Month
            });

        if (pos == "s")
        {
            strRangeText = "<b>From</b> Year: " + rangeText.First().Year + " Month: " + rangeText.First().Month;
            return strRangeText;
        }
        else
        {
            strRangeText = "<b>To</b> Year: " + rangeText.First().Year + " Month: " + rangeText.First().Month;
            return strRangeText;
        }
    }

At this point, you should see something like below on the browser.

Figure 5
Check point

Chart Control

Make sure to download the Microsoft Chart Controls for Microsoft .NET Framework 3.5 because the Chart controls required System.Web.DataVisualization.Design.dll and System.Web.DataVisualization.dll. I also included both the dlls in the sample code. Some of the codes in this section are from Samples Environment for Chart Controls. First, let create a method to bind the data source to the Column Chart. This method accepts two parameters, start and end range values. Then use LINQ to query the CarsList.xml data source and find all the records where YearMonth between start and end range values. Group the result by car brands, stores it in lstCarsnTotal and bind it to the chart. See listing 7.

Listing 7

void PopulateChart(int start, int end)
    {
        List<CarsList> lstCarsnTotal = new List<CarsList>();

        XDocument xmlDoc = XDocument.Load(Server.MapPath(Utilities.Instance.CarsListXMLPath));
        lstCarsnTotal = (from c in xmlDoc.Descendants("Car")
                         where (int)c.Attribute("YearMonth") >= GetRange(start) && (int)c.Attribute("YearMonth") <= GetRange(end)
                         group c by (string)c.Attribute("CarBrand") into g

                         select new CarsList
                         {
                             CarCount = g.Sum(c => (int)c.Attribute("Count")),
                             CarBrand = g.Key
                         }).ToList();
	  Chart1.Series["Default"].ChartType = SeriesChartType.Column;
        Chart1.Series["Default"].Points.DataBindXY(lstCarsnTotal, "CarBrand", lstCarsnTotal, "CarCount"); 
}

//return YearMonth
    protected static int GetRange(int rn)
    {
        IEnumerable<Range> rangeText;

        rangeText = lstSliderRange.Where(r => r.RowNumber == rn)
            .Select(r => new Range
            {
                Year = r.Year,
                Month = r.Month
            });

        return int.Parse(rangeText.First().Year + rangeText.First().Month);
    }

Now, when the user click on the column chart, a GridView will appears next to it. The PopulateGrid method takes the car brand as an argument. Then use LINQ to query the SliderRange.xml data source where YearMonth between the selected range values and CarBrand equal to the selected car brand. See listing 8.

Listing 8

protected void Chart1_Click(object sender, ImageMapEventArgs e)
    {
        if (!GridView1.Visible)
        {
            GridView1.Visible = true;
        }
        //kept track of selected car type
        ChartPostBackValue.Value = e.PostBackValue;
        lblCarBrand.Text = "Car Brand: " + e.PostBackValue;

        PopulateGrid(e.PostBackValue);
        PopulateChart(int.Parse(rangeStart.Value), int.Parse(rangeEnd.Value));
    }

    void PopulateGrid(string strPostBavkVal)
    {
        List<CarsList> lstCarsnTotal = new List<CarsList>();

        XDocument xmlDoc = XDocument.Load(Server.MapPath(Utilities.Instance.CarsListXMLPath));
        lstCarsnTotal = (from c in xmlDoc.Descendants("Car")
                         where (int)c.Attribute("YearMonth") >= GetRange(int.Parse(rangeStart.Value))
                         && (int)c.Attribute("YearMonth") <= GetRange(int.Parse(rangeEnd.Value)) && (string)c.Attribute("CarBrand") == strPostBavkVal
                         select new CarsList
                         {
                             CarCount = (int)c.Attribute("Count"),
                             CarBrand = (string)c.Attribute("CarBrand"),
                             Date = (string)c.Attribute("Date")
                         }).ToList();

        GridView1.DataSource = lstCarsnTotal;
        GridView1.DataBind();
    }

Known Issue

On design time, we will see the error "MultiHandleSliderExtender could not be set on property MultiHandleSliderTargets" but code work fine at run time. I have downloaded the example and latest version of Ajax Control Toolkit from CodePlex but didn't solve the problem. A workaround is adding the TagPrefix next to the MultiHandleSliderTargets tag during design time and remove it at run time. Hopefully someone can shed some light on this.

Points of Interest

The Default2.aspx in the sample code includes a master page. If you use a master page, make sure to use the Control.ClientID. For some reason the client __doPostBack function do not work with master page, the work around was to call the button click event. See listing 9. Hopefully someone can shed some light on this too.

Listing 9

<script type="text/javascript">
    var isDragging = false;
    function Drag(sender, args) {
        GetSliderRange($get("<%= rangeStart.ClientID %>").value, $get("<%= rangeEnd.ClientID%>").value);
    }
    function DragEnd(sender, args) {
        //prevent postback on slider click
        if ($get("<%= hdfTrackRangeStart.ClientID %>").value !== $get("<%= rangeStart.ClientID %>").value) {
            $get("<%= btnLoadChart.ClientID %>").click();
            //__doPostBack("<%= btnLoadChart.ClientID %>", "");
        }
        if ($get("<%= hdfTrackRangeEnd.ClientID %>").value !== $get("<%= rangeEnd.ClientID %>").value && $get("<%= hdfTrackRangeEnd.ClientID %>").value !== '0') {
            $get("<%= btnLoadChart.ClientID %>").click();
            //__doPostBack("<%= btnLoadChart.ClientID %>", "");
        }
    }

    function GetSliderRange(startV, endV) {
        PageMethods.SliderRange(startV, endV, this.callback);
    }

    function callback(result) {

        var arrResult = result.split("--");

        $get("<%= lblStartRange.ClientID %>").innerHTML = arrResult[0];
        $get("<%= lblEndRange.ClientID %>").innerHTML = arrResult[1];

    }
    </script>

I have noticed that, clicking on the handle will trigger the DragEnd function and cause unnecessary post back. To remedy this problem, compare the old selected range value and the current selected range value, if they are not equal then permit the call of client-side __doPostBack function. See listing 10.

Listing 10

function DragEnd(sender, args) {
        //prevent postback on slider click
        if ($get("hdfTrackRangeStart").value !== $get("rangeStart").value) {
             __doPostBack("btnLoadChart", "");
        }
        if ($get("hdfTrackRangeEnd").value !== $get("rangeEnd").value && $get("hdfTrackRangeEnd").value !== '0') {
             __doPostBack("btnLoadChart", "");
        }
    }

The chart displayed correctly on my local machine but it displayed a sequence of strange characters in the hosting environment. After doing some research, I discovered that I didn't set appropriate permissions on the storage folder and EnableSessionState on the page directive was set to false. The list of ASP.NET Charts storage mode is available here.

New Update 1

Create a client-side Array object named arrRange, loop through each rows in the lstSliderRange object and add the values year and month to it. Place this method inside the !Page.IsPostBack block. See listing 11.

Listing 11

void CreateArray()
    {
        foreach (Range r in lstSliderRange)
        {
            Page.ClientScript.RegisterArrayDeclaration("arrRange", "'"+r.Year +"--" + r.Month+"'");
        }
    }

Instead of using the PageMethods to lookup the RowNumber text, call the client-side GetSliderText function. See listing 12.

Listing 12

function GetSliderRange(startV, endV) {
        $get("<%= lblStartRange.ClientID %>").innerHTML = GetSliderText(arrRange[startV - 1], 's');
        $get("<%= lblEndRange.ClientID %>").innerHTML = GetSliderText(arrRange[endV - 1], 'e');
        // alert(arrRange[startV - 1]);
        // PageMethods.SliderRange(startV, endV, this.callback);
    }

    function GetSliderText(r, p) {
        var arrResult = r.split("--");
        var strText = '';
        if (p === 's') {
            strText = "<b>From</b> Year: " + arrResult[0] + " Month: " + arrResult[1];

        }
        else {
            strText = "<b>To</b> Year: " + arrResult[0] + " Month: " + arrResult[1];
        }
        return strText;
    }

Conclusion

If you find any bugs or disagree with the contents, please drop me a line and I'll work with you to correct it. I would suggest downloading the demo and explore it in order to grasp the full concept of it because I might left out some useful information.

IE, Firefox, Google Chrome, Safari

Resources

ASP.NET Charts - Storage Mode
MultiHandleSlider Demonstration
ScriptManager..::.EnablePageMethods Property
Using Microsoft's Chart Controls In An ASP.NET Application: Rendering the Chart
Working with Client-Side Script

Watch this script in action

Demo

Downloads

Download

ASP-NET Confirm dialog box based on DropDownList selected value

by bryian 15. February 2010 13:30

ASP.NET Confirm dialog box based DropDownList selected value

ASP.NET JavaScript popup window basedon DropDownList selected value

Popup confirmation before ASP.NET Form Submit |

Add a JavaScript popup to an ASP.NET button |

Display Confirmation pop up window |

ASP.NET DropDownList Confirmation |

ASP.NET Adding JavaScript confirm to button based on DropDownList selected value |

Show JavaScript confirm message if DropDownList selected value equal

 

Requirement

User want to see a popup confirmation dialog box on LinkButton click, only if the DropDownList.SelectedValue == ?


See example below:

<!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></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    
    <asp:DropDownList runat="server" ID="ddlTest">
        <asp:ListItem Selected="True">select</asp:ListItem>
        <asp:ListItem>a</asp:ListItem>
        <asp:ListItem>b</asp:ListItem>
        <asp:ListItem>c</asp:ListItem>
        <asp:ListItem>d</asp:ListItem>
        <asp:ListItem>e</asp:ListItem>   
    </asp:DropDownList>
    
    <asp:LinkButton ID="IsertButton"
                  runat="server"
                  Text="Insert" onclick="IsertButton_Click"
                  OnClientClick="return CheckIfZeroSelected();" />
   
    </div>
    </form>
    
  <script type="text/javascript">
      function Insert_Confirmation() {
          if (confirm("Are you certain that you want to insert with selected value 0?"))
              return true; else return false;
      }

      function CheckIfZeroSelected() {
          var selObj = document.getElementById('<%= ddlTest.ClientID %>');

          if (selObj.selectedIndex === 0) {
              return Insert_Confirmation();
          }    
      }
  </script>  
</body>
</html>

ASP.NET redirect a web page with AJAX loading indicator image

by bryian 6. February 2010 17:14

ASP.NET page redirecting progress indicator |

Progress Indicator |

Progress Indicator For Slow-Loading Pages |

ASP.NET intermediate processing page |

ASP.NET AJAX loading page |

ASP.NET intermediate page |

ASP.NET Menu Target |

ASP.NET progress indicator |

AJAX loading indicator |

Page Loading Indicator |

ASP.NET progress bar

 

Introduction

There are times when the web pages that the web applications are redirecting to, might take some time to response or load. For instance, after the users successfully login into a web application, the application will redirect the request to the home page. Imagine that, if the mentioned page contains a dashboard and many personalized WebParts. The requested web page might take some time to respond depending on the users Internet connection speed and the amount of network traffic at any given time. Some users might get impatient and tempted to click on other buttons or links on the page, see figure 1. To overcome this problem, first, redirect the request to an intermediate page, and then load the requested page from there. The main purpose of the intermediate page is to display an AJAX loading indicator image (figure 2), while the users wait for the requested web page to respond.

Figure 1
no loading indicator
Figure 2
with loading indicator

Implementation

Create an HTML page and name it Redirecting.html, add an image tag in the body section and wrap it in a div element. See listing 1

Listing 1

<div style='position:absolute;z-index:5;top:45%;left:45%;'>
    <img id="imgAjax" alt="loading..." title="loading..." src="images/ajax-loading.gif" style="width: 100px; height: 100px" /><br /> <br />
 </div>

Add the JavaScript shown in listing 2 below the div tag. In this JavaScript, we have two global variables namely querystring and page, and a public function called toPage. After reading the JavaScript best practices article, I decided to employ the JavaScript module pattern. I have created a namespace called redirect to wrap the mentioned public variables and function. The variable querystring will return the query string value in the current URL. The page variable will return the substring of the querystring from index of "=" to the end of the string. The purpose of the function toPage() is to append a Header to refresh the web page for browsers other than Internet Explorer (IE). If the browser type is IE and its version is greater than or equal to 4, then use the location.replace function to replace the current URL with new location URL. The redirect.begin() will invoke the toPage() method.

Listing 2

<script type="text/javascript">
    /* <![CDATA[ */
    this.focus(); //focus on new window
    redirect = function() {
        var querystring = window.location.search.substring(1); //query string
        var page = querystring.substring(querystring.indexOf('=') + 1, querystring.length);
        function toPage() {
            if (page !== undefined && page.length > 1) {
                document.write('<!--[if !IE]>--> <head><meta http-equiv="REFRESH" content="1;url=' + page + '" /><\/head><!--<![endif]-->');
                document.write(' \n <!--[if IE]>');
                document.write(' \n <script type="text/javascript">');
                document.write(' \n var version = parseInt(navigator.appVersion);');
                document.write(' \n if (version>=4 || window.location.replace) {');
                document.write(' \n window.location.replace("' + page + '");');
                document.write(' document.images["imgAjax"].src = "images/ajax-loading.gif"');
                document.write(' \n } else');
                document.write(' \n window.location.href="' + page + '";');
                document.write(' \n  <\/script> <![endif]-->');
            }
        }
        return {
            begin: toPage
        }
    } ();

    redirect.begin();

    /* ]]> */
</script>  

Using the Code

Response.Redirect("Redirecting.html?page=test.aspx");

<a href="Redirecting.html?page=test.aspx" target="_blank"> Redirect and Open on new page</a>

//requested web page with multiple query string
<a href="Redirecting.html?page=http://www.amazon.com/gp/offer-listing/059610197X?tag=asp-net-20&linkCode=sb1&camp=212353&creative=380553">
	Redirect with multiple query string</a>

Points of Interest

When I tested this JavaScript on browsers other than Internet Explorer, the image did not render correctly. Injecting a Header to refresh the page instead of calling the location.replace or location.href solves the problem.

I encapsulated the JavaScript in a HTML page because the whole process does not require server side programming and I can reuse it in others web applications that are written in different programming languages.

On IE 6, the order of the lines shown below is very important. If we place 2 before 1, the image will not display on IE 6. The current order works fine on both the IE 6 and 7.
1) document.write(' \n window.location.replace("' + page + '");');
2) document.write(' document.images["imgAjax"].src = "images/ajax-loading.gif"');

Conclusion

I hope someone will find this tips useful. If you find any bugs or disagree with the contents, please drop me a line and I'll work with you to correct it. I would suggest downloading the demo and explore it in order to grasp the full concept of it because I might have left out some useful information. Tested on IE 6.0/7.0, Firefox, Google Chrome, Apple Safari 4.0.4, Verizon BlackBerry Storm

Tested on IE 6.0/7.0/8.0, Google Chrome, Firefox, Safari

Resources

JavaScript best practices

Watch this script in action

Demo

Downloads

Download

ASP NET Horizontal Menu Control

by bryian 14. December 2009 18:18

ASP.NET menu control safari|

Problem displaying asp .net menu control in Google Chrome|

Breadcrumb (Sitemappath) Control|

ASP.NET Menu control|

Displaying Menu Control Horizontal With SubMenus|

ASP.NET Menu AccessKey|

ASP.NET Menu Target

Last Updated - 02/26/2010

  • Removed PathSeparator of SiteMapPath before the root node as suggested by The Code Project member, kentex2000.
  • Added SiteMap.CurrentNode != null to SiteMap_SiteMapResolve method.

Last Updated - 02/03/2010

Introduction

A few weeks ago, I was working on an ASP.NET web application and need a simple horizontal menu with submenu. I decided to use ASP.NET Menu control, just drag and drop the control on to the page. Simple enough, but the control does not provide access key and target window support on menu item. I have put together a tutorial on how to:

  1. Include an access key attribute
  2. Include a target attribute
  3. Include a Site Map Path


Figure 1
Sample results

 

Getting Started

Here is the structure of my project. You are welcome to download this demo.

Figure 2
Project Structure

Putting everything together

First, add a Site Map to the website project. Open the web.sitemap file and populate it with your navigation data and structures. To underline certain character of the menu title, we can use the HTML underline tag (<u></u>). In order to parse the XML flawlessly we must replace the less than sign (<) with & lt; (no spaces). Then, include an accesskey and target attribute with a value to each siteMapNode. See below for example.

Site Map

Listing 1

<siteMapNode>
      <siteMapNode url="Default.aspx" title="& lt;u>H& lt;/u>ome" description="Home" 
                   accesskey="H" />
      <siteMapNode url="~/Views/Menu1.aspx" title="<u>M</u>enu1"  
                   description="Menu1" accesskey="M" />
        <siteMapNode url="~/Views/Menu2.aspx" title="M<u>e</u>nu2" 
                     description="Menu2" accesskey="E" />
    
    <siteMapNode url="~/Views/Menu3.aspx" title="Me<u>n</u>u3" 
                 description="Menu3" accesskey="N" target="_blank" />
        
    <siteMapNode url="~/Views/Menu4.aspx" title="Men<u>u</u>4" 
                 description="Menu4" accesskey="U">
      <siteMapNode url="~/Views/Menu4Sub1.aspx" title="Menu4<u>S</u>ub1" 
                   description="Menu4Sub1" 
                   accesskey="S" />
      <siteMapNode url="~/Views/Menu4Sub2.aspx" title="Menu4Su<u>b</u>2" 
                   description="Menu4Sub2" 
                   target="_blank" accesskey="B" />
    </siteMapNode>
……
….
  </siteMapNode>
</siteMap>

Master Page

Add a Master Page to the website project. Drag a SiteMapDataSource control on to the page and then the menu control and wrap the menu control inside a div tag. The details description of each menu property can be found on Menu Properties Menu Properties. Set the staticdisplaylevels ="2" and orientation="Horizontal" to display the menu control in horizontal mode. We can use an inline style sheets or place the CSS style in an external file. In this tutorial, the CSS style is located in style.css file. See listing 2.

Listing 2

<asp:SiteMapDataSource id="MenuSource" runat="server" />
<div class="background">
  <asp:menu id="NavigationMenu" CssClass="NavigationMenu"  
        staticdisplaylevels="2" DynamicHorizontalOffset="1"
        staticsubmenuindent="1px" MaximumDynamicDisplayLevels="4"
        orientation="Horizontal"   
        DynamicPopOutImageUrl="~/Images/right-arrow.gif" 
        StaticPopOutImageUrl="~/Images/drop-arrow.gif"
        datasourceid="MenuSource"    
        runat="server" Height="30px">

        <staticmenuitemstyle ItemSpacing="10" CssClass="staticMenuItemStyle"/>
        <statichoverstyle CssClass="staticHoverStyle" />
       <StaticSelectedStyle CssClass="staticMenuItemSelectedStyle"/> 
        <DynamicMenuItemStyle CssClass="dynamicMenuItemStyle" />      
        <dynamichoverstyle CssClass="menuItemMouseOver" />
        <DynamicMenuStyle CssClass="menuItem" />
       <DynamicSelectedStyle CssClass="menuItemSelected" />
     
       <DataBindings>        
             <asp:MenuItemBinding DataMember="siteMapNode" 
                    NavigateUrlField="url" TextField="title"  
                    ToolTipField="description" />
        </DataBindings>

      </asp:menu>
</div>

Drag a SiteMapPath control on to the page. The purpose of this control is to display navigation path that shows the user the current page location. See listing 3.

Listing 3

<div id="e">
       <asp:SiteMapPath ID="SiteMapPath1" runat="server" 
                RenderCurrentNodeAsLink="true" 
                CssClass="currentNodeStyle"
            PathSeparator=" >> ">
            <PathSeparatorStyle ForeColor="#5D7B9D" CssClass="currentNodeStyle" />
            <CurrentNodeStyle ForeColor="#333333" CssClass="currentNodeStyle" />
            <NodeStyle ForeColor="#7C6F57"  CssClass="currentNodeStyle"  />
            <RootNodeStyle  ForeColor="#5D7B9D" CssClass="currentNodeStyle"  />
    </asp:SiteMapPath> 
</div>  

Master Page code behind

In the Master Page code behind, include a MenuItemDataBound and SiteMapResolve event handlers on to the Page_Load event. The purpose of the former event is to insert the target attribute value and create access key for the menu item before it is rendered or displayed in a Menu control. The latter event is to modify the text displayed by the SiteMapPath control.

Listing 4

NavigationMenu.MenuItemDataBound += new MenuEventHandler(NavigationMenu_MenuItemDataBound);
SiteMap.SiteMapResolve += new SiteMapResolveEventHandler(SiteMap_SiteMapResolve);

Below is the implementation of the NavigationMenu_MenuItemDataBound method. The MenuItemDataBound event occurs when a menu item in a Menu control is bound to data. That being said, it will loop through each siteMapNode and look for the accesskey and target attribute. There is a target property associated with the menu item and we can set its target window with the target attribute value. See listing 5.

Listing 5

void NavigationMenu_MenuItemDataBound(object sender, MenuEventArgs e)
    {
        SiteMapNode node = (SiteMapNode)e.Item.DataItem;
       
        //set the target of the navigation menu item (blank, self, etc...)
        if (node["target"] != null)
        {
            e.Item.Target = node["target"];
        }
        //create access key button
        if (node["accesskey"] != null)
        {
            CreateAccessKeyButton(node["accesskey"] as string, node.Url);
        }
    }

To get the access key to work, add a Panel control on to the master page and a JavaScript function to redirect the webpage to the one that is specified. See below.

Listing 6

<asp:Panel ID="AccessKeyPanel" runat="server" />
<script type="text/javascript">
 function navigateTo(url) {
    window.location = url;
 }
</script>

Below is the implementation of the CreateAccessKeyButton method. Create an HtmlButton control dynamically and attach an onclick event to it. Set the style.left property to -2555px to hide the control. A complete list of access key in different browsers is available here.

Listing 7

//create access key button
    void CreateAccessKeyButton(string ak, string url)
    {
        HtmlButton inputBtn = new HtmlButton();
        inputBtn.Style.Add("width", "1px");
        inputBtn.Style.Add("height", "1px");
        inputBtn.Style.Add("position", "absolute");
        inputBtn.Style.Add("left", "-2555px");
        inputBtn.Style.Add("z-index", "-1");
        inputBtn.Attributes.Add("type", "button");
        inputBtn.Attributes.Add("value", "");
        inputBtn.Attributes.Add("accesskey", ak);
        inputBtn.Attributes.Add("onclick", "navigateTo('" + url + "');");

        AccessKeyPanel.Controls.Add(inputBtn);
    }

The SiteMap.SiteMapResolve event get trigger when the CurrentNode property is accessed. It will call the ReplaceNodeText method recursively and replace the HTML underline tag. See listing 8.

Listing 8

SiteMapNode SiteMap_SiteMapResolve(object sender, SiteMapResolveEventArgs e)
    {
        if (SiteMap.CurrentNode != null)
        {
            SiteMapNode currentNode = SiteMap.CurrentNode.Clone(true);
            SiteMapNode tempNode = currentNode;
            tempNode = ReplaceNodeText(tempNode);

            return currentNode;
        }
        return null;
    }

    //remove <u></u> tag recursively
    internal SiteMapNode ReplaceNodeText(SiteMapNode smn)
    {
        //current node
        if (smn != null && smn.Title.Contains("<u>"))
        {
            smn.Title = smn.Title.Replace("<u>", "").Replace("</u>", "");
        }

        //parent node
        if (smn.ParentNode != null)
        {
            if (smn.ParentNode.Title.Contains("<u>"))
            {
                SiteMapNode gpn = smn.ParentNode;
                smn.ParentNode.Title = smn.ParentNode.Title.Replace("<u>", "").Replace("</u>", "");
                smn = ReplaceNodeText(gpn);
            }
        }
        return smn;
    }

Using the Code

Since the menu is in the master page, right click the website project, add new item, Web Form and check the Select Master Page checkbox.

Points of Interest

The hover menu appears to not working on mobile devices. To remedy this problem, I include a TreeView control and set its visible property to false. This control expands its entire node by default. That will take care of the mentioned problem. In the code behind, hide the Menu control and show the TreeView control if requesting browser is a mobile device. See listing 9.

Listing 9

protected void Page_Load(object sender, EventArgs e)
    {
	if (Request.Browser.IsMobileDevice)
        {
            NavigationMenu.Visible = false;
            NavigationTreeView.Visible = true;
        }
}

When I tested the menu on IE 8, the hover menu did not render correctly. To overcome this problem, I set the DynamicMenuStyle z-index to 200, see style.css. The submenu is also not working with Google Chrome. After some research, I found the solution for it. See listing 10.

Listing 10

protected void Page_Load(object sender, EventArgs e)
    {
	if (Request.UserAgent.IndexOf("AppleWebKit") > 0)
        {
            Request.Browser.Adapters.Clear();
            NavigationMenu.DynamicMenuStyle.Width = Unit.Pixel(120);
        }
}

New Update

I have received several complaints from the reader concerning the menu control not displaying correctly on Safari and Google Chrome browsers. Somehow the menu items are stacked on each other and the submenu widths are gapped apart. After doing some research, I found the answer here, see listing 11. To fix the submenu width, remove the display:block from the dynamicMenuItemStyle in the css file.

Listing 11

protected override void AddedControl(Control control, int index)
    {
        if (Request.ServerVariables["http_user_agent"].IndexOf("Safari", StringComparison.CurrentCultureIgnoreCase) != -1)
            this.Page.ClientTarget = "uplevel";

        base.AddedControl(control, index);
    }

I also rewrite the logic to detect mobiles browser with the code from Vincent Van Zyl. See listing 12.

Listing 12

    public static readonly string[] mobiles =
          new[]
                {
                    "midp", "j2me", "avant", "docomo", 
                    "novarra", "palmos", "palmsource", 
                    "240x320", "opwv", "chtml",
                    "pda", "windows ce", "mmp/", 
                    "blackberry", "mib/", "symbian", 
                    "wireless", "nokia", "hand", "mobi",
                    "phone", "cdm", "up.b", "audio", 
                    "SIE-", "SEC-", "samsung", "HTC", 
                    "mot-", "mitsu", "sagem", "sony"
                    , "alcatel", "lg", "eric", "vx", 
                    "NEC", "philips", "mmm", "xx", 
                    "panasonic", "sharp", "wap", "sch",
                    "rover", "pocket", "benq", "java", 
                    "pt", "pg", "vox", "amoi", 
                    "bird", "compal", "kg", "voda",
                    "sany", "kdd", "dbt", "sendo", 
                    "sgh", "gradi", "jb", "dddi", 
                    "moto", "iphone"
                };

    public static bool isMobileBrowser()
    {
        //GETS THE CURRENT USER CONTEXT
        HttpContext context = HttpContext.Current;

        //FIRST TRY BUILT IN ASP.NT CHECK
        if (context.Request.Browser.IsMobileDevice)
        {
            return true;
        }
        //THEN TRY CHECKING FOR THE HTTP_X_WAP_PROFILE HEADER
        if (context.Request.ServerVariables["HTTP_X_WAP_PROFILE"] != null)
        {
            return true;
        }
        //THEN TRY CHECKING THAT HTTP_ACCEPT EXISTS AND CONTAINS WAP
        if (context.Request.ServerVariables["HTTP_ACCEPT"] != null &&
            context.Request.ServerVariables["HTTP_ACCEPT"].ToLower().Contains("wap"))
        {
            return true;
        }
        //AND FINALLY CHECK THE HTTP_USER_AGENT 
        //HEADER VARIABLE FOR ANY ONE OF THE FOLLOWING
        if (context.Request.ServerVariables["HTTP_USER_AGENT"] != null)
        {
            for (int i = 0; i < mobiles.Length; i++)
            {
                if (context.Request.ServerVariables["HTTP_USER_AGENT"].
                                                    ToLower().Contains(mobiles[i].ToLower()))
                {
                    return true;
                }
            }
        }

        return false;
    }

Add the code shown below in the Page_Load event.

Listing 13

if (isMobileBrowser())
        {
            NavigationMenu.Visible = false;
            NavigationTreeView.Visible = true;
        }

Conclusion

If you find any bugs or disagree with the contents, please drop me a line and I'll work with you to correct it.

Tested on IE 6.0/7.0/8.0, Google Chrome, Firefox, Safari

IE, Firefox, Google Chrome, Safari

History

  • 02/03/2010 - Fixed the Menu display problem in Safari and Google Chrome browsers, added new logic to detect mobiles browser

Resources

Access in different Browsers
ASP.NET Menu Control Cannot Work in Google Chrome browser and Apple Safari browser
Detecting a mobile browser in ASP.NET
IE8 and the ASP.NET Menu control
Menu Properties
Menu..::.MenuItemDataBound Event
Problem displaying asp .net menu control in Google Chrome
SiteMap..::.SiteMapResolve Event

Watch this script in action

Demo

Downloads

Download