Introduction
In this tutorial, I will demonstrate how to use the 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 controls for validating user selection. Then, we will use the Column Chart to display the 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
Getting Started
- Download AJAX Control Toolkit Release Notes - May 2009 Release
- Download Samples Environment for Chart Controls
- Displayed below is the snapshot of the project Solution Explorer. You are welcome to download this demo.
Figure 2
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
Use the 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 returns 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
Putting Everything Together
For simplification's sake, I use XML as the data source in this tutorial. There are two XML files 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 holds 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's start by adding a ScriptManager
on to the page and setting 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 them 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 settings 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
values. 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) {
if ($get("hdfTrackRangeStart").value !== $get("rangeStart").value) {
}
if ($get("hdfTrackRangeEnd").value !== $get("rangeEnd").value &&
$get("hdfTrackRangeEnd").value !== '0') {
}
}
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 methods. Shown below are the contents in the Page_Load
event. The PopulateSlider()
method reads the contents in the SliderRange.xml file and stores it in the lstSliderRange
. Initialize the MultiHandleSliderExtender
minimum and maximum value to 1 and the 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)
{
MultiHandleSliderExtender1.Minimum = 1;
MultiHandleSliderExtender1.Maximum =
int.Parse(lstSliderRange.Max(r => r.RowNumber).ToString());
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 the PopulateSlider()
method. The lstSliderRange
object is cached to increase performance, and its contents are fetched again when the file contents change. 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 techniques here.
Listing 5
void PopulateSlider()
{
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 displays 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 the client-side JavaScript. The GetSliderText
method accepts two parameters: the first parameter refers 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 retrieves the year and month from the result set. If pos==s
, set the text to From, and To if pos==e
. See Listing 6.
Listing 6
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 = "From Year: " + rangeText.First().Year +
" Month: " + rangeText.First().Month;
return strRangeText;
}
else
{
strRangeText = "To Year: " + rangeText.First().Year +
" Month: " + rangeText.First().Month;
return strRangeText;
}
}
At this point, you should see something like below on the browser.
Figure 5
Chart Control
Make sure to download the Microsoft Chart Controls for Microsoft .NET Framework 3.5 because the Chart controls require 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's 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
is between the start and end range values. Group the result by car brands, store 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");
}
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 clicks on the column chart, a GridView
will appear 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
is between the selected range values and CarBrand
equals the selected car brand. See Listing 8.
Listing 8
protected void Chart1_Click(object sender, ImageMapEventArgs e)
{
if (!GridView1.Visible)
{
GridView1.Visible = true;
}
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
At design time, we will see the error "MultiHandleSliderExtender could not be set on property MultiHandleSliderTargets", but the code works fine at run time. I have downloaded the example and the latest version of the AJAX Control Toolkit from CodePlex, but it didn't solve the problem. A workaround is adding the TagPrefix
next to the MultiHandleSliderTargets
tag during design time and removing 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 does not work with master pages; the workaround is 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) {
if ($get("<%= hdfTrackRangeStart.ClientID %>").value !==
$get("<%= rangeStart.ClientID %>").value) {
$get("<%= btnLoadChart.ClientID %>").click();
}
if ($get("<%= hdfTrackRangeEnd.ClientID %>").value !==
$get("<%= rangeEnd.ClientID %>").value &&
$get("<%= hdfTrackRangeEnd.ClientID %>").value !== '0') {
$get("<%= btnLoadChart.ClientID %>").click();
}
}
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 an unnecessary post back. To remedy this problem, compare the previously selected range value with the currently selected range value; if they are not equal, then permit the call to the client-side __doPostBack
function. See Listing 10.
Listing 10
function DragEnd(sender, args) {
if ($get("hdfTrackRangeStart").value !== $get("rangeStart").value) {
__doPostBack("btnLoadChart", "");
}
if ($get("hdfTrackRangeEnd").value !==
$get("rangeEnd").value &&
$get("hdfTrackRangeEnd").value !== '0') {
__doPostBack("btnLoadChart", "");
}
}
The chart displays correctly on my local machine, but it displays a sequence of strange characters in the hosting environment. After doing some research, I discovered that I didn't set the 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 row in the lstSliderRange
object, and add the values of year and month to it. Place the CreateArray()
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');
}
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 exploring it in order to grasp the full concept of it because I might have left out some useful information.
Resources
History
- 03/07/2010 - First release.
- 03/09/2010 - Replaced the PageMethods with client-side JavaScript
Array
. I received a feedback from a 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 IIS." I think 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 to be updated through the client-side. See the New Update 1 section.