ASP.NET AJAX MultiHandleSliderExtender - 按年和月滑动






4.68/5 (14投票s)
教程 - 如何利用 ASP.NET MultiHandleSlider 扩展器来选择一个范围内的年份和月份,并设置图表控件使其正常工作。
引言
在本教程中,我将演示如何使用 MultiHandleSlider 扩展器来选择或显示一个范围内的年份和月份。该控件消除了使用四个 DropDownList 控件来保存范围值和验证控件来验证用户选择的需要。然后,我们将使用柱状图根据选定的范围值显示芝麻街汽车的数量。此图表允许用户深入查看所选汽车品牌的详细信息。参见图 1。

入门
- 下载 AJAX Control Toolkit 版本说明 - 2009 年 5 月发布
- 下载 图表控件示例环境
- 下面显示的是项目解决方案资源管理器快照。欢迎下载此演示。

后端数据库
首先,向表中添加一个新列,命名为 YearMonth,并用年份和月份数据的连接填充它。参见图 3。左侧是原始表。有了这个设置,我们就可以轻松地从表中选择所需年份和月份范围内的所有数据。

使用 ROW_NUMBER 函数为结果集中的每一行生成行号。然后,我们可以使用行号来填充滑块的范围值。下面显示的查询返回六十二行,这意味着滑块的范围值从一到六十二。换句话说,我们可以将滑块的最小值和最大值属性分别设置为一和六十二。

将所有内容整合
为了简化起见,本教程中使用 XML 作为数据源。App_Data 文件夹中有两个 XML 文件,分别是 CarsList.xml 和 SliderRange.xml。前者包含图 3 中所示表中的所有数据。后者 XML 文件包含图 4 中所示的结果集。创建两个名为 CarsList 和 Range 的自定义实体类来保存公共属性。参见列表 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; }
}
让我们首先在页面上添加一个 ScriptManager,并将 EnablePageMethods 和 EnablePartialRendering 属性设置为 true。通过将 EnablePageMethods 属性设置为 true,客户端脚本就可以访问 ASP.NET 页面的静态页面方法。EnablePartialRendering 属性允许我们仅指定要刷新的页面区域。接下来,将一个 TextBox、MultiHandleSliderExtender、四个 HiddenField 和两个 Label 控件拖放到页面上,并将它们包装在 UpdatePanel 中。将 MultiHandleSliderExtender 控件的 TargetControlID 设置为 TextBox 控件的 ID。向控件添加两个滑块,并分别将其 ControlID 设置为 rangeStart 和 rangeEnd。参见列表 2。Label 控件的目的是显示选定的范围值。HiddenField 控件用于保存滑块的值。使用如下所示的设置初始化 MultiHandleSliderExtender。
- OnClientDrag= Drag- 当用户拖动滑块时引发的事件。
- OnClientDragEnd = DragEnd- 当用户停止拖动滑块时引发的事件。
- Increment = 1- 确定滑块值递增或递减的点数。
- RaiseChangeOnlyOnMouseUp = true- 仅当释放鼠标左键时,才在扩展的 TextBox 上触发更改事件。
- EnableRailClick = false.
<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>
下面显示的是客户端代码。拖动滑块将触发 ASP.NET 页面方法 SliderRange 来更新 lblStartRange 和 lblEndRange 的值。一旦用户释放滑块,DragEnd 函数将被执行。
<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>
在代码隐藏中创建一个通用的 List<T> Range 对象,并将其标记为 static,以便客户端脚本或其他静态方法可以访问它。下面显示的是 Page_Load 事件中的内容。PopulateSlider() 方法读取 SliderRange.xml 文件中的内容并将其存储在 lstSliderRange 中。将 MultiHandleSliderExtender 的最小值和最大值分别初始化为 1 和 lstSliderRange 中的最大 RowNumber。参见列表 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();
}
下面显示的是 PopulateSlider() 方法的实现。lstSliderRange 对象被缓存以提高性能,并在文件内容更改时再次获取其内容。根据数据源更新的频率,我们可以基于数据库、文件夹、文件或基于时间的过期进行缓存。在此处阅读更多关于 ASP.NET 缓存技术的信息。
//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>;
    }
}
SetLabel() 方法在 lblStartRange 和 lblStartEnd Label 控件中显示 MultiHandleSliderExtender 的开始和结束范围值。SliderRange 方法带有 [System.Web.Services.WebMethod] 装饰,使其可以通过客户端 JavaScript 访问。GetSliderText 方法接受两个参数:第一个参数是指行号,第二个参数是指左滑块或右滑块。例如,调用 SliderRange(2, 10) 将返回“从年份:2005 月份:02--到年份:2005 月份:10”。首先,它将查询 lstSliderRange 对象并从结果集中检索年份和月份。如果 pos==s,则文本设置为 **From**,如果 pos==e,则设置为 **To**。参见列表 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 = "From Year: " + rangeText.First().Year + 
                       " Month: " + rangeText.First().Month;
        return strRangeText;
    }
    else
    {
        strRangeText = "To Year: " + rangeText.First().Year + 
                       " Month: " + rangeText.First().Month;
        return strRangeText;
    }
}
此时,您应该在浏览器中看到类似以下的界面。

图表控件
请确保下载 Microsoft Chart Controls for Microsoft .NET Framework 3.5,因为图表控件需要 System.Web.DataVisualization.Design.dll 和 System.Web.DataVisualization.dll。我也将这两个 DLL 包含在示例代码中。此部分中的一些代码来自 图表控件示例环境。首先,让我们创建一个方法将数据源绑定到柱状图。此方法接受两个参数:开始和结束范围值。然后,使用 LINQ 查询 CarsList.xml 数据源,找到 YearMonth 在开始和结束范围值之间的所有记录。按汽车品牌对结果进行分组,将其存储在 lstCarsnTotal 中,并将其绑定到图表。参见列表 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);
}
现在,当用户单击柱状图时,一个 GridView 将出现在其旁边。PopulateGrid 方法以汽车品牌作为参数。然后,使用 LINQ 查询 SliderRange.xml 数据源,其中 YearMonth 在选定范围值之间,并且 CarBrand 等于选定的汽车品牌。参见列表 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();
}
已知问题
在设计时,我们会看到错误“MultiHandleSliderExtender 无法设置在 MultiHandleSliderTargets 属性上”,但代码在运行时可以正常工作。我下载了示例和最新版本的 AJAX Control Toolkit 从 CodePlex 下载,但未能解决问题。一种变通方法是在设计时在 MultiHandleSliderTargets 标签旁边添加 TagPrefix,并在运行时将其删除。希望有人能对此有所阐明。
关注点
示例代码中的 Default2.aspx 包含一个母版页。如果您使用母版页,请确保使用 Control.ClientID。出于某种原因,客户端 __doPostBack 函数不能与母版页一起工作;变通方法是调用按钮单击事件。参见列表 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>
我注意到,单击滑块会触发 DragEnd 函数并导致不必要的发布回退。为了解决这个问题,请比较先前选定的范围值与当前选定的范围值;如果它们不相等,则允许调用客户端 __doPostBack 函数。参见列表 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", "");
    }
}
图表在我的本地计算机上显示正常,但在托管环境中显示一系列奇怪的字符。经过一些研究,我发现我没有在存储文件夹上设置适当的权限,并且页面指令上的 EnableSessionState 被设置为 false。ASP.NET 图表存储模式的列表可以在这里找到:here。
新更新 1
创建一个名为 arrRange 的客户端 Array 对象,循环遍历 lstSliderRange 对象中的每一行,并将年份和月份的值添加到其中。将 CreateArray() 方法放在 !Page.IsPostBack 块内。参见列表 11。
void CreateArray()
{
    foreach (Range r in lstSliderRange)
    {
        Page.ClientScript.RegisterArrayDeclaration("arrRange", 
             "'"+r.Year +"--" + r.Month+"'");
    }
}
不要使用 PageMethods 来查找 RowNumber 文本,而是调用客户端 GetSliderText 函数。参见列表 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;
}
结论
如果您发现任何错误或不同意内容,请给我留言,我会与您合作进行纠正。我建议下载演示并进行探索,以充分理解其概念,因为我可能遗漏了一些有用的信息。

资源
- ASP.NET 图表 - 存储模式
- MultiHandleSlider 演示
- ScriptManager..::.EnablePageMethods 属性
- 在 ASP.NET 应用程序中使用 Microsoft Chart Controls:渲染图表
- 处理客户端脚本
历史
- 2010/03/07 - 首次发布。
- 2010/03/09 - 将 PageMethods 替换为客户端 JavaScript Array。
我收到一位 CodeProject 成员的反馈,他说:“问题是,拖动会向服务器发送许多请求,最后一个会执行,但这同样意味着数据库需要额外工作,IIS 也需要。” 我认为他说得有道理,所以我决定将每个 RowNumber 的年份和月份数据存储在客户端 JavaScript 数组中。这将允许通过客户端更新滑块的范围标签。参见新更新 1 部分。


