65.9K
CodeProject 正在变化。 阅读更多。
Home

高级 AJAX ListBox 组件 v0.1

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.68/5 (9投票s)

2007 年 6 月 15 日

13分钟阅读

viewsIcon

118431

downloadIcon

912

如何构建一个更直观的、具有 ASP.NET AJAX 客户端功能的 ListBox 服务器控件。

Screenshot - ListBoxComponent01.gif

引言

正如 Evyatar Ben-Shitrit 在他的文章 ASP.NET 2.0 的可滚动 ListBox 自定义控件 的引言中所说,我也曾搜索过一个支持水平滚动条的 ASP.NET ListBox 控件。像他一样,我也找到了几个解决方案和代码片段(包括他的),但它们并没有完全满足我的需求。

起初,我以为我只是想要一个可以水平滚动的 ListBox。然后我想要一个 ListBox,当使用向上和向下箭头键在项目之间循环时,它也能调整其滚动位置。然后我想要一个 ListBox,当用户使用其他键(如字母和数字)在项目之间循环时,它也能调整其滚动位置。当满足了这些要求后,我想要一个 ListBox,它能够记住其垂直和水平滚动位置,并在回发(异步或同步)后重新调整它们。最后,我希望这个 ListBox 默认情况下能够作为一个普通的 Microsoft System.Web.UI.WebControls.ListBox 存在,这样控件本身就可以使用其 WidthBorderXyzStyle 属性来确定自己的渲染策略。由此产生的控件就是本文要介绍的内容。

背景

我开始搜索,很快就找到了 lintom 的文章 ASP.NET 中终于有横向滚动条的 ListBox 了!。通过那篇文章,我了解了将 ListBox 包装在 DIV 中并将其 OVERFLOW 样式设置为 AUTO 的策略。这使得获得所需的水平滚动条成为可能。然而,当使用键盘在 ListBox 中的项目之间导航时,DIV 不会自动滚动。当用户选择了一个不在滚动窗口中的项目时,他们就束手无策了。我的下一次搜索找到了 Shiby Chacko 的文章 带有水平滚动条的组合框或列表框,并具有上下箭头键按下功能。Shiby 有助于展示如何使用客户端脚本来克服 lintom 文章中的问题。但 Shiby 文章中最棒的部分是用户 zivros 在其底部留下的一条 评论。它包含了客户端脚本,可用于在任何按键事件(不仅是向上和向下箭头键)发生时,即使 DIV 元素接收到按键事件而不是 ListBox,也能使 DIV 调整其滚动位置。zivros 的贡献非常大,感谢你的发布。最后,Evyatar 的文章 解决了通过覆盖自定义服务器控件中的渲染方法,将 WidthBorderXyz 和其他属性从 ListBox 传递到其包含的 DIV 的想法。

我能为这一系列对横向滚动 ListBox 的改进带来的,就是我熟悉使用 ASP.NET AJAX 扩展为服务器控件添加客户端功能。如果你从未实现过 System.Web.UI.IScriptControl 接口或为 ASP.NET 2.0 服务器控件创建过客户端脚本原型,别担心,我们会涵盖这些内容。如果你跟不上,这两篇文章帮助我很好地掌握了核心概念

一个全面的解决方案

首先,我们需要创建一个新的服务器控件,它继承自 Microsoft 的 System.Web.UI.WebControls.ListBox 控件并实现 System.Web.UI.IScriptControl 接口。这很简单

using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace DanLudwig.Controls.Web
{
    public class ListBox : System.Web.UI.WebControls.ListBox,
        System.Web.UI.IScriptControl
    {
        // all the server code we need will go right here
    }
}

至于可配置属性,我们的 ListBox 控件将有三个:HorizontalScrollEnabledScrollTopScrollLeft。请注意,HorizontalScrollEnabled 属性的默认值是 false,这意味着为了利用我们将创建的新功能,必须在 ASPX 页面中显式启用它。ScrollTopScrollLeft 属性在这里是为了在服务器控件和相应的客户端脚本之间提供简化的连接,以便 ListBox 在回发之间记住其滚动位置。另外请注意,如果控件或其父控件上的 EnableViewState 为 false,滚动属性将不会在回发之间保留。我还添加了一个 get 访问器,以便统一访问 ScriptManager,稍后将需要它。

protected virtual ScriptManager ScriptManager
{
    get { return ScriptManager.GetCurrent(Page); }
}

public virtual bool HorizontalScrollEnabled
{
    set { this.ViewState["HorizontalScrollEnabled"] = value; }
    get
    {
        object output = this.ViewState["HorizontalScrollEnabled"];
        if (output == null)
            output = false;
        return (bool)output;
    }
}

public virtual int ScrollTop
{
    set { this.ViewState["ScrollTop"] = value; }
    get
    {
        object output = this.ViewState["ScrollTop"];
        if (output == null)
            output = 0;
        return (int)output;
    }
}

public virtual int ScrollLeft
{
    set { this.ViewState["ScrollLeft"] = value; }
    get
    {
        object output = this.ViewState["ScrollLeft"];
        if (output == null)
            output = 0;
        return (int)output;
    }
}
    

此时,我读过的大多数其他 AJAX 扩展入门文章都会讨论如何在扩展控件中实现 System.Web.UI.IScriptControl 接口。本文将改为先讨论客户端脚本文件。如果你正在跟随,你已经在你的 App_Code 目录中创建了一个 ListBox.cs 文件。在你的网站项目的根目录下,创建一个名为 ListBox.js 的 JScript 文件。只要它包含一个 ScriptManager,当渲染 ASPX 页面时,这个文件将自动包含我们控件所需的所有客户端脚本。无需在 ASPX 页面中手动包含任何脚本标签,也无需编写任何 ASPX 代码隐藏文件。不过,我们必须仔细遵循 Microsoft 的规范,这需要以下主要步骤

  1. 注册客户端控件的命名空间(1行代码)。
  2. 定义客户端控件的类。
  3. 定义类的原型。
    • 重写/实现 Sys.UI.Controlinitialize 方法。
    • 重写/实现 Sys.UI.Controldispose 方法。
    • 定义事件处理程序。
    • 定义属性 getset 方法。
  4. 可选启用 JSON 序列化(非必需)。
  5. 将客户端控件注册为继承自 Sys.UI.Control 的类型(1行代码)。
  6. 通知 ScriptManager 此脚本已加载(1行代码)。
//
// 1.)
// Register the client control's namespace
//
Type.registerNamespace('DanLudwig.Controls.Client');

// 2.)
// Define the client control's class
//
DanLudwig.Controls.Client.ListBox = function(element)
{
    // initialize base (Sys.UI.Control)
    DanLudwig.Controls.Client.ListBox.initializeBase(this, [element]);

    // declare fields for use by properties
    this._horizontalScrollEnabled = null;
    this._scrollTop = null;
    this._scrollLeft = null;
}

// 3.)
// Define the class prototype
//
DanLudwig.Controls.Client.ListBox.prototype =
{
    // 3a)
    // Override / implement the initialize method
    //
    initialize : function()
    {
        // call base initialize
        DanLudwig.Controls.Client.ListBox.callBaseMethod(this, 'initialize');
        // more code will go here later
    }
,
    // 3b)
    // Override / implement the dispose method
    //
    dispose : function()
    {
        // more code will go here later
        // call base dispose
        DanLudwig.Controls.Client.ListBox.callBaseMethod(this, 'dispose');
    }
,
    // 3c)
    // Define the event handlers (this will be done later)
    //
,
    // 3d)
    // Define the property get and set methods.
    //
    set_horizontalScrollEnabled : function(value)
    {
        if (this._horizontalScrollEnabled !== value)
        {
            this._horizontalScrollEnabled = value;
            this.raisePropertyChanged('_horizontalScrollEnabled');
        }
    }
,
    get_horizontalScrollEnabled : function()
    {
        return this._horizontalScrollEnabled;
    }
,
    set_scrollTop : function(value)
    {
        if (this._scrollTop !== value)
        {
            this._scrollTop = value;
            this.raisePropertyChanged('_scrollTop');
        }
    }
,
    get_scrollTop : function()
    {
        return this._scrollTop;
    }
,
    set_scrollLeft : function(value)
    {
        if (this._scrollLeft !== value)
        {
            this._scrollLeft = value;
            this.raisePropertyChanged('_scrollLeft');
        }
    }
,
    get_scrollLeft : function()
    {
        return this._scrollLeft;
    }

} // end prototype declaration

// 4.)
// Optionally enable JSON Serialization
//
DanLudwig.Controls.Client.ListBox.descriptor =
{
    properties: [
         {name: '_horizontalScrollEnabled', type: Boolean}
        ,{name: '_scrollTop', type: Number }
        ,{name: '_scrollLeft', type: Number }
    ]
}

// 5.)
// Register the client control as a type that inherits from Sys.UI.Control.
//
DanLudwig.Controls.Client.ListBox.registerClass(
    'DanLudwig.Controls.Client.ListBox', Sys.UI.Control);

// 6.)
// Notify the ScriptManager that this script is loaded.
//
if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();    

这个文件目前还没有提供任何实际功能,但它定义了必要的组件,以便客户端控件(仅针对我们的 ListBox 控件的客户端 JavaScript 代码)能够与 ScriptManager 进行交互。现在,如果我们实现了服务器控件上的 System.Web.UI.IScriptControl 方法,并重写了其 OnPreRenderRender 方法,我们就可以最终将服务器控件的属性与其对应的客户端控件属性绑定起来了。

protected virtual IEnumerable<ScriptReference> GetScriptReferences()
{
    ScriptReference reference = new ScriptReference();

    // use this line when debugging the control in a web site project
    reference.Path = ResolveClientUrl("ListBox.js");

    // use these lines when the control is released with an embedded
    // js resource
    //reference.Assembly = "DanLudwig.Controls.AspAjax.ListBox";
    //reference.Name = "DanLudwig.Controls.Client.ListBox.js";

    return new ScriptReference[] { reference };
}
IEnumerable<ScriptReference> IScriptControl.GetScriptReferences()
{
    return GetScriptReferences();
}

protected virtual IEnumerable<ScriptDescriptor> GetScriptDescriptors()
{
    ScriptControlDescriptor descriptor = new ScriptControlDescriptor(
        "DanLudwig.Controls.Client.ListBox", this.ClientID);
    descriptor.AddProperty("horizontalScrollEnabled",
        this.HorizontalScrollEnabled);
    descriptor.AddProperty("scrollTop", this.ScrollTop);
    descriptor.AddProperty("scrollLeft", this.ScrollLeft);
    return new ScriptDescriptor[] { descriptor };
}
IEnumerable<ScriptDescriptor> IScriptControl.GetScriptDescriptors()
{
    return GetScriptDescriptors();
}

protected override void OnPreRender(EventArgs e)
{
    if (!this.DesignMode)
    {
        if (ScriptManager == null)
            throw new HttpException(
                "A ScriptManager control must exist on the current page.");

        ScriptManager.RegisterScriptControl(this);
    }

    base.OnPreRender(e);
}

protected override void Render(HtmlTextWriter writer)
{
    if (!this.DesignMode)
        ScriptManager.RegisterScriptDescriptors(this);

    // more code will go here later
    base.Render(writer);
    // more code will go here later
}        

这应该会消除那些烦人的编译器错误,告诉你实现你之前为类声明的接口。如果你正在使用我们自己的网站项目,到目前为止讨论的代码此时应该可以编译并正常渲染(尽管还没有什么实际功能)。服务器控件会设置客户端控件的属性,但我们仍然需要将它们付诸使用。不过请注意,从现在开始,在 ASPX 页面中测试控件可能会产生奇怪的结果,直到我们完成本文。

为 ListBox 渲染一个 DIV... 或者不渲染

到目前为止,我们已经完成了创建新的 AJAX 启用服务器控件并带客户端事件时所需的常见工作。客户端事件将是你的控件特有的。对于我们的 ListBox,我们希望处理并响应的客户端特定事件是当用户滚动将包含 ListBoxDIV 元素时,或者当他们使用按键来导航项目时。但是这个 DIV 容器在哪里?我们需要在 ASPX 页面中包含一个 DIV 元素或一个 ASP Panel 控件来包裹我们的 ListBox 吗?理想情况下,控件应该使用其 HorizontalScrollEnabled 属性来决定是否应该在自身周围渲染一个 DIV。我们还需要渲染一个隐藏的表单字段,以便在回发时将客户端状态数据传回服务器控件(以维护滚动状态)。

在渲染时我们还应该做的另一件事是将 ListBox 的某些特性传递给 DIV 容器。例如,如果宽度或边框样式是从 ASPX 页面应用到我们的 ListBox 上的,我们希望将它们传递给 DIV 元素。这可能有点棘手,而且我可能没有完全做到,但以下代码可以处理我在应用程序中应用于此 ListBox 的所有样式。阅读代码注释可以确切地了解我们在渲染控件时发生了什么。

public const string ContainerClientIdSuffix = "__LISTBOXHSCROLLCONTAINER";
public const string ScrollStateClientIdSuffix = "__LISTBOXHSCROLLSTATE";

protected override void Render(HtmlTextWriter writer)
{
    if (!this.DesignMode)
        ScriptManager.RegisterScriptDescriptors(this);

    if (this.HorizontalScrollEnabled)
    {
        // wrap this control in a DIV container
        this.AddContainerAttributesToRender(writer);
        writer.RenderBeginTag(HtmlTextWriterTag.Div);
    }
    base.Render(writer);
    if (this.HorizontalScrollEnabled)
    {
        // add a hidden field to store client scroll state
        // and close the container
        this.AddScrollStateAttributesToRender(writer);
        writer.RenderBeginTag(HtmlTextWriterTag.Input);
        writer.RenderEndTag();
        writer.RenderEndTag();
    }
}

protected virtual void AddContainerAttributesToRender(HtmlTextWriter writer)
{
    // when horizontal scrolling is enabled, width and border styles 
    // should be delegated to the container
    if (HorizontalScrollEnabled)
    {
        // add required container attributes
        writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID
            + ContainerClientIdSuffix);
        writer.AddStyleAttribute(HtmlTextWriterStyle.Overflow, "auto");
        writer.AddStyleAttribute(HtmlTextWriterStyle.Width,
            this.Width.ToString());

        // add optional container attributes
        Color borderColor = this.BorderColor;
        if (!borderColor.Equals(Color.Empty))
            writer.AddStyleAttribute(HtmlTextWriterStyle.BorderColor,
                string.Format("#{0}", borderColor.ToArgb().ToString("x")
                .Substring(2)));

        BorderStyle borderStyle = this.BorderStyle;
        if (!borderStyle.Equals(BorderStyle.NotSet))
            writer.AddStyleAttribute(HtmlTextWriterStyle.BorderStyle,
                borderStyle.ToString());

        Unit borderWidth = this.BorderWidth;
        if (!borderWidth.Equals(Unit.Empty))
            writer.AddStyleAttribute(HtmlTextWriterStyle.BorderWidth,
                borderWidth.ToString());

        // move style declarations from the Style attribute into the DIV
        // container.
        foreach (string key in this.Style.Keys)
        {
            writer.AddStyleAttribute(key, this.Style[key]);
        }
        this.Style.Remove(HtmlTextWriterStyle.Width);
        this.Style.Remove("width");
        this.Style.Remove(HtmlTextWriterStyle.BorderWidth);
        this.Style.Remove(HtmlTextWriterStyle.BorderStyle);
        this.Style.Remove(HtmlTextWriterStyle.BorderColor);
        this.Style.Remove("border");
    }
}

protected override void AddAttributesToRender(HtmlTextWriter writer)
{
    if (HorizontalScrollEnabled)
    {
        // take advantage of the base method by clearing the properties
        // we don't want rendered, adding the attributes, then restoring
        // the properties to their original values. BTW, why is there no
        // writer method to remove attributes? Or did I miss it???

        Unit originalWidth = this.Width;
        Unit originalBorderWidth = this.BorderWidth;
        BorderStyle originalBorderStyle = this.BorderStyle;
        Color originalBorderColor = this.BorderColor;

        this.Width = Unit.Empty;
        this.BorderWidth = Unit.Empty;
        this.BorderStyle = BorderStyle.NotSet;
        this.BorderColor = Color.Empty;

        base.AddAttributesToRender(writer);

        // get rid of default firefox border
        writer.AddStyleAttribute("border", "0px none");

        this.Width = originalWidth;
        this.BorderWidth = originalBorderWidth;
        this.BorderStyle = originalBorderStyle;
        this.BorderColor = originalBorderColor;
    }
    else
    {
        base.AddAttributesToRender(writer);
    }
}

protected virtual void AddScrollStateAttributesToRender(
    HtmlTextWriter writer)
{
    // the hidden field should have an id, name, type, and default value
    string fieldId = this.ClientID + ScrollStateClientIdSuffix;
    writer.AddAttribute(HtmlTextWriterAttribute.Id, fieldId);
    writer.AddAttribute(HtmlTextWriterAttribute.Name, fieldId);
    writer.AddAttribute(HtmlTextWriterAttribute.Type, "hidden");
    writer.AddAttribute(HtmlTextWriterAttribute.Value, string.Format(
        "{0}:{1}", this.ScrollTop, this.ScrollLeft));
}        

这几乎是我们服务器控件所需的所有代码了。唯一缺少的是服务器控件在回发期间接收来自客户端的新滚动位置信息时所做的事情。事实证明,我们只需要提取隐藏字段传递过来的数据,并将其值应用到服务器控件中相应的 ScrollTopScrollLeft 属性。然后,当 IScriptControl.GetScriptDescriptors() 被执行时,它将在回发后再次渲染客户端控件时将这些值传回客户端控件。我通过重写服务器控件的 OnLoad 事件来处理这个问题,因为它发生在生命周期的一个节点,此时可以访问 Request.Form 集合,但在 IScriptControl.GetScriptDescriptors() 被调用之前。

public static readonly char[] ClientStateSeparator = new char[] { ':' };

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);

    // update the server control's scroll position state
    string scrollStateClientId = this.ClientID + ScrollStateClientIdSuffix;
    object state = Page.Request.Form[scrollStateClientId];

    if (state != null)
    {
        //the state will be formatted with the pattern "scrollTop:scrollLeft"
        string[] scrollState = state.ToString().Split(
            ClientStateSeparator,2);
        int scrollTop = 0;
        if (scrollState[0] != null && int.TryParse(scrollState[0],
            out scrollTop))
            this.ScrollTop = scrollTop;

        int scrollLeft = 0;
        if (scrollState[1] != null && int.TryParse(scrollState[1],
            out scrollLeft))
            this.ScrollLeft = scrollLeft;
    }
}    

处理客户端事件以添加 ListBox 功能...或者不添加

我们需要编写的其余代码都在 ListBox.js 文件中。所有用户与 ListBox 的交互都将在客户端处理。这就是 zivros 发布的代码派上用场的地方。他的代码处理的 3 个客户端事件是 DIV 容器的 onkeydown 事件,以及 ListBoxonkeydownonchange 事件。我们将处理这 3 个事件,但为了更新我们渲染的隐藏字段,我们还将处理容器的 onscoll 事件。

为了处理这些事件,我们首先需要在重写的 initializedispose 函数中添加一些代码。(请注意,在原型定义内部,所有函数都用逗号分隔。)

// 3.)
// Define the class prototype
//
DanLudwig.Controls.Client.ListBox.prototype =
{
    // 3a)
    // Override / implement the initialize method
    //
    initialize : function()
    {
        // call base initialize
        DanLudwig.Controls.Client.ListBox.callBaseMethod(this, 'initialize');

        // only create event handlers and delegates if horizontal
        // scrolling is enabled
        if (this.get_horizontalScrollEnabled())
        {
            // create event delegates
            this._onkeydownHandler = Function.createDelegate(
                this, this._onKeyDown);
            this._onchangeHandler = Function.createDelegate(
                this, this._onChange);
            this._onkeydownContainerHandler = Function.createDelegate(
                this, this._onContainerKeyDown);
            this._onscrollContainerHandler = Function.createDelegate(
                this, this._onContainerScroll);

            // add event handlers for the ListBox
            $addHandlers(this.get_element(),
            {
                 'keydown' : this._onKeyDown
                ,'change' : this._onChange
            }, this);

            // add event handlers for the ListBox's DIV container
            $addHandlers(this.get_elementContainer(),
            {
                 'keydown' : this._onContainerKeyDown
                ,'scroll' : this._onContainerScroll
            }, this);
        }

        // when horizontal scrolling is enabled, initialize the control
        if (this.get_horizontalScrollEnabled())
        {
            var listBox = this.get_element();
            var container = this.get_elementContainer();

            // before changing the listbox's size, store the original
            // size in the container.
            container.originalListBoxSize = listBox.size;

            if (this.get_element().options.length > this.get_element().size)
            {
                // change the listbox's size to eliminate internal scrolling
                listBox.size = listBox.options.length;

                // set the height of the container based on the
                // original listbox's size
                // (add 2 pixels of padding to prevent clipping)
                container.style.height
                    = ((container.scrollHeight / listBox.size)
                        * (container.originalListBoxSize)) + 2 + 'px';
            }

            // set the scroll position based on server state
            container.scrollTop = this.get_scrollTop();
            container.scrollLeft = this.get_scrollLeft();

            // if the ListBox is too narrow, expand it to fill the DIV
            // container
            if (container.scrollWidth <= parseInt(container.style.width))
            {
                listBox.style.width = '100%';
                listBox.style.height = container.scrollHeight + 'px';
                container.style.height
                    = ((container.scrollHeight / listBox.size)
                        * (container.originalListBoxSize)) + 'px';

                // there is a known bug in some FF versions that renders
                // 'XX' in empty selects. To overcome this issue, you could
                // add an empty item to empty ListBoxes
                //if (listBox.options.length < 1)
                //{
                //    listBox.options[0] = new Option('','');
                //}
            }
        }
    }
,
    // 3b)
    // Override / implement the dispose method
    //
    dispose : function()
    {
        // clear event handlers from the ListBox
        $clearHandlers(this.get_element());

        // can only clear event handlers from the DIV container if it exists
        if (this.get_elementContainer() != null)
        {
            $clearHandlers(this.get_elementContainer());
        }

        // call base dispose
        DanLudwig.Controls.Client.ListBox.callBaseMethod(this, 'dispose');
    }
,
    // helper method for retrieving the ListBox's DIV container
    get_elementContainer : function()
    {
        // only return the container if horizontal scrolling is enabled.
        if (this.get_horizontalScrollEnabled()
            && this.get_element().parentNode != null)
        {
            return this.get_element().parentNode;
        }
        else
        {
            return null;
        }
    }
,

initialize 方法中,我们创建并添加了 4 个新的事件处理程序。由于 ListBox 是直接连接到此客户端控件的服务器控件,因此我们使用 this.get_element() 访问它。我创建了一个名为 get_elementContainer() 的辅助方法,它可以方便地为我们检索 DIV 客户端元素。创建好处理程序后,我们分别使用 $addHandlers 快捷方式将它们添加到 ListBoxDIV。其余代码遵循 zivros 的模式,即 初始化客户端元素,并做了一些我自己的调整。页面加载后,当我们的 4 个客户端事件中的任何一个被触发时,客户端将执行下面定义的事件特定代码。

    // 3c)
    // Define the event handlers
    //
    _onKeyDown : function(e)
    {
        if (this.get_element() && !this.get_element().disabled)
        {
            // cancel bubble to prevent listbox from re-scrolling
            // back to the top
            event.cancelBubble = true;
            return true;
        }
    }
,
    _onChange : function(e)
    {
        if (this.get_element() && !this.get_element().disabled)
        {
            // update the scroll position when the user changes the
            // item selection
            updateListBoxScrollPosition(this.get_elementContainer(),
                this.get_element(), null);
            event.cancelBubble = true;
            return true;
        }
    }
,
    // when keypresses are received by the container
    // (not bubbled up from the listbox),
    // they should be passed to the listbox.
    _onContainerKeyDown : function(e)
    {
        // setting focus on the listbox scrolls back to the top
        this.get_element().focus();

        // re-position the container scollbars after the focus()
        // method scrolled to the top
        setTimeout("updateListBoxScrollPosition("
            + "document.getElementById('"
            + this.get_elementContainer().id
            + "'), document.getElementById('"
            + this.get_element().id
            + "'), "
            + this.get_elementContainer().scrollTop
            + ")", "5");
    }
,
    _onContainerScroll : function(e)
    {
        // when the container is scrolled, update this control
        this.set_scrollState();
    }
,
    // set this property when the DIV container is scrolled
    set_scrollState : function()
    {
        // first of all, make sure the scroll properties are set
        this.set_scrollTop(this.get_elementContainer().scrollTop);
        this.set_scrollLeft(this.get_elementContainer().scrollLeft);

        // server control expects the state to be in the format
        // "scrollTop:scrollLeft"
        var stateValue = this.get_scrollTop() + ':' + this.get_scrollLeft();

        // save the state data in the hidden field
        this.get_elementState().value = stateValue;

        this.raisePropertyChanged('_scrollState');
    }
,
    get_elementState : function()
    {
        // function locates and returns the hidden form field which
        // stores the scroll state data.
        if (this.get_horizontalScrollEnabled()
            && this.get_element().parentNode != null)
        {
            // the second child node in the DIV container with a valid
            // VALUE property is the hidden field.
            // must find the hidden field this way because IE and FF have
            // differing childNodes collections
            var childNodeIndex = -1;
            for (i = 0; i<this.get_elementContainer().childNodes.length; i++)
            {
                if (this.get_elementContainer().childNodes[i].value != null)
                {
                    childNodeIndex++;
                    if (childNodeIndex > 0)
                    {
                        childNodeIndex = i;
                        break;
                    }
                }
            }
            return this.get_elementContainer().childNodes[childNodeIndex];
        }
        else
        {
            return null;
        }
    }
,

如果你在看到文章的这一点之前就试图让这些函数工作,你可能已经非常沮丧了。我们还需要添加一个客户端函数,这可能是最重要的。再次感谢 zivros 提供的将所有内容联系起来并实际更新 DIV 元素滚动位置的最后一部分。将 updateListBoxScrollPosition 函数放在调用 Sys.Application.notifyScriptLoaded() 的最后一行代码之前。

// 5.)
// Register the client control as a type that inherits from Sys.UI.Control.
//
DanLudwig.Controls.Client.ListBox.registerClass(
    'DanLudwig.Controls.Client.ListBox', Sys.UI.Control);

// static function called by the client control(s)
function updateListBoxScrollPosition(container, listBox, realScrollTop)
{
    // realScrollTop defaults to zero when it is not set
    if (realScrollTop == null)
        realScrollTop = container.scrollTop;

    // determine the size of a single item in the ListBox
    var scrollStepHeight = container.scrollHeight / listBox.size;

    //find out what are the visible top & bottom items in the ListBox
    var minVisibleIdx = Math.round(realScrollTop / scrollStepHeight);
    var maxVisibleIdx = minVisibleIdx + container.originalListBoxSize - 2;

    // handle the case where a user is scrolling down...
    if (listBox.selectedIndex >= maxVisibleIdx)
    {
        container.scrollTop
            = (listBox.selectedIndex - container.originalListBoxSize + 2)
            * scrollStepHeight;
    }

    // handle the case where a user is scrolling up...
    else if (listBox.selectedIndex < minVisibleIdx)
    {
        container.scrollTop = listBox.selectedIndex * scrollStepHeight;
    }

    // in all other cases, set the vertical scroll to the realScrollTop
    // parameter.
    else
    {
        container.scrollTop = realScrollTop;
    }
}

// 6.)
// Notify the ScriptManager that this script is loaded.
//
if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();
    

搞定!

这里有很多重叠的代码,所以如果你直接从这篇文章复制粘贴到你自己的文件中,你可能会遇到令人讨厌的“DanLudwig is undefined”客户端错误消息。这条错误消息和其他许多错误一样,可能具有误导性。我向你保证,我的定义非常明确。但是,ListBox.js 文件中即使最轻微的语法错误也会导致 DanLudwig 命名空间未能成功注册。有时是因为原型声明的各个部分之间缺少必需的逗号,有时是因为客户端脚本文件中有更晦涩的错误。如果你在复制粘贴时遇到问题,请使用本文附带的源文件(但请记住,如果你要在网站项目中使用它进行测试,请更改 GetScriptReferences() 方法中的引用)。

完美主义是一种疾病

现在我们有了一个相当直观的 ListBox,它可以容纳比我们可用屏幕空间更宽的文本。作为一个额外的功能,它还将记住其最后的垂直和水平滚动位置,并在回发后恢复它们,以保留客户端状态。但仍有更多功能需要添加!例如,如果你的 ListBoxSelectionMode 设置为 Multiple,用户应该能够使用 CONTROL 和 SHIFT 键同时选择多个项目。试试这个:选择一个项目,然后滚动离开,使其不再可见。现在,尝试使用 CONTROL 或 SHIFT 键选择更多项目。一旦你按下键,ListBox 就会滚动回你最初选择的项目。我不是用户,但如果我是,这可能会让我恼火。

此控件的另一个缺点可以通过在 IE7 和 FireFox 1.5 中查看来演示。在 FireFox 中,当鼠标悬停在 ListBox 上时,可以使用鼠标滚轮滚动项目。但在 IE7 中,只有当鼠标悬停在滚动条之一上时,此功能才有效。同样,如果我是用户,我可能会抱怨这一点。更有趣的是,当你在 IE6 和 FireFox 1.0 中查看控件时会发生什么……你会得到相反的行为!在这些浏览器的旧版本中,IE 默认滚动,而 FireFox 则不滚动。理想情况下,我们应该为鼠标滚轮进行编码,以便能够从 ASPX 页面打开/关闭这种行为。一些用户可能更喜欢使用鼠标滚轮,而另一些用户可能认为它过多地干扰了页面滚动。如果我们使其可配置,我们至少可以为自己提供可能性,让用户通过 ProfileCommon 打开/关闭此功能。

你可能还注意到,当 HorizontalScrollingEnabled 设置为 false(或未设置)时,我们的 ListBox 不会记住滚动位置。两者应该分开配置并独立运行。另外,我们还可以重塑我们响应事件的方式,以减少客户端计算机上的 CPU 周期。这会特别有用,因为 FireFox 1.0 不响应我们附加到 DIV 容器的 onscroll 事件。可能还有更多我还没想到的事情,但这篇文章已经太长了。在我的下一篇,篇幅短得多的文章中,我将开始讨论如何满足这些额外的需求。

关注点

这是我写过的第二篇代码文章。第一篇,现在已经过时了 Flash 5 中更多的滚动功能,我早在 2000 年就写了。计算滚动位置所需的一些数学原理是相似的,但即使是我自己也觉得奇怪的是,我写过的两篇文章都涉及到这个相当平凡但对 UI 至关重要的必需品。也许当浏览器发展到能够原生横向滚动 ListBox 的成熟水平时,这个 ListBox 控件也会过时。

为你提供的练习

我知道这个控件并不完美,尽管我认为它被正确打包后非常可重用,而且在下一篇文章中我们会让它变得更好。如果你发现这个控件未能满足你的要求,这里有一些你可以考虑的事情。

  • 即使是异步回发,在浏览器渲染 ListBox 和客户端代码初始化保存的滚动位置之间,有时也会出现屏幕闪烁。因为默认情况下,它的高度由其“size”属性(对应于服务器控件中的“Rows”属性/值)决定,所以在 JavaScript 以编程方式设置 ListBox 的大小之前,很难设置 DIV 的滚动位置。对我来说,屏幕闪烁不是一个 100% 可重现的问题,几乎不明显,而且在大多数情况下可以忽略。但是你能找到完全消除它的方法吗?
  • 尽管此控件与 FireFox 兼容,但即使 ListBox 包含的项目少于其可见项,它也始终会渲染一个垂直滚动条。你能找到摆脱它的方法吗?
  • 创建服务器属性以向 DIV 滚动条添加 CSS 样式。
  • 创建其他属性,将更多 CSS 样式控制从 ListBox 传递给其 DIV 容器。
  • 清理我的 C# 代码(我实际上拥有 Java 和 J2EE 认证,而不是 ASP.NET 或 C# 认证)。
  • 我想不到其他了。我可能遗漏了什么?
© . All rights reserved.