高级AJAX列表框组件 v0.2

分享于 

31分钟阅读

Web开发

  繁體

Screenshot - ListBoxComponent02.gif

介绍

中,我的上一篇文章我们创建了一个启用了客户端事件的ASP.NET ajax启用 ListBox 控件。 然而,这种控制仍然不能很长的时间发布和生产要求。

背景

我们已经讨论过的一些缺失部分包括:

  • 使用SHIFT和控制键一次选择多个项是困难或者不可以能的,因为它们的onkeydown 事件会根据选定的第一项。 我们应该读取事件的keyCode,并绕过"元"按键的滚动代码,如下所示。
  • 使用鼠标滚轮滚动项目的能力取决于用于加载页面的浏览器和版本。 我们想让鼠标滚动一个可以配置的选项,支持所有目标浏览器。
  • 当控件属性的HorizontalScrollEnabled 显式设置为 true 时,垂直和水平滚动条位置仅在回发之间保留。 我们应该能够保留滚动状态,即使 HorizontalScrollEnabledfalse
  • 由于更改滚动位置的用户交互不会执行 _onContainerScroll 事件处理程序,所以我们的事件处理策略不完全支持 Firefox 1.0. 这里外,如果你尝试调试这里处理程序,你将注意到 IE 可以在单击鼠标单击或者击击时执行最多四次。 拖动滚动条会使我的CPU短暂跳转到 30%或者更高,具体取决于滚动速度。 更新隐藏字段多次会在计算机的客户端处理器上放置不必要的负载。

除了这些问题,你还可以能发现,使用FireBug运行这个控件会导致大量的"事件未定义"错误。 在 ListBox.js 文件中,我们将在将这里控件提升为 0.2版本之前,使用一些糟糕的编码实践。

一次一件事

评估这四个障碍中哪一个,首先可以节省我们大量的时间和。 从可用性角度看最大的问题是 #1,,但这不是我们应该根据我们的决定来决定的。 对客户机和服务器代码都有最大影响的是 #3. 在 #2, 文件中,我们必须为添加一个新的服务器控件属性,而 #1 和 #4 可以完全被处理。 但是 #3 将要求在这两个文件中进行最大的改变,因这里我们将在本文中解决。 首先,让我们消除FireBug中那些讨厌的红色消息,我们将?

内存泄漏( 在我的头脑中)

为了在开发 0.1版本时获得工作控制,我忽略了几个很好的编码实践。 我在文件中复制并粘贴了很多Zivros代码,因为它在IE7中工作,所以理所当然地接受了它。 但是,window.event 对象(。在客户端处理程序中简单地引用为 event ) 在 Firefox 中不可用。 相反,Firefox 使用传递给处理程序的" e"参数。 如果你曾经在 跨浏览器 脚本中处理客户端事件,那么你可能比较 window.eventundefined,以促进一致性。 幸运的是,微软已经想到了这一点,并给我们一个一致的方法来处理客户控制。 我只是。呃。忘记了。

在这里,我们不正确地使用了客户端控件中的event 对象。 在 _onKeyDown 处理程序中执行了一次,然后再次在 _onChange 处理程序中,以防止 ListBox 将它的事件冒泡到 DIV 容器中。 使用 ASP.NET AJAX扩展框架的正确方法是调用传入处理函数的事件参数" e"的stopPropagation() 方法,如下所示:

_onKeyDown : function(e)
{
 if (this.get_element() &&!this.get_element().disabled)
 {
 // cancel bubble to prevent listbox from re-scrolling// back to the tope.stopPropagation();returntrue;
 }
}
,
_onChange : function(e)
{
 if (this.get_element() &&!this.get_element().disabled)
 {
 updateListBoxScrollPosition(
 this.get_elementContainer(), this.get_element(), null);
 e.stopPropagation();returntrue;
 }
}
,

不要感谢我,感谢微软。 事实证明 stopPropagation() 是正式的W3C方法,所以在他们开始遵循某些行业标准时,最好看到他们开始遵循某些行业标准。 在处理这些事件时,FireBug应该显示一个漂亮的绿色复选标记,这意味着我们的脚本不会导致 Firefox ( for ) 中的任何。

向用户承诺他们想要什么。

等一下,把你自己放在设计师的page上。 假设你不关心这里控件是如何工作的,只要它有效。 你只需要在控制标记的服务器XML上设置几个简单属性,并让它以 跨浏览器 方式处理自己。 如果网页设计者想要的那就是他们应该得到的。 让我们先给他们一个控制属性。

publicvirtualbool ScrollStateEnabled
{
 set {
 this.ViewState["ScrollStateEnabled"] = value;
 }
 get {
 object output = this.ViewState["ScrollStateEnabled"];
 if (output == null)
 output = false;
 return (bool)output;
 }
}protectedvirtual 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);
 descriptor.AddProperty("scrollStateEnabled",this.ScrollStateEnabled);returnnew ScriptDescriptor[] { descriptor };
}

好了现在回到现实 ! 我们的控件当前处理 4种可能情况的:

  • HorizontalScrollEnabled==<code>true && ScrollStateEnabled == true </代码/>
  • <code><code>HorizontalScrollEnabled == false && ScrollStateEnabled == false </code/> </code/> ;

由于我们将这些属性视为版本 0.1中的相同选项,所以这些属性具有相同的值。 现在,我们必须重构服务器和客户机代码以适应其他 2种可能的配置:

  • <code><code>HorizontalScrollEnabled == true && ScrollStateEnabled == false </code/> </code/> ;
  • <code><code>HorizontalScrollEnabled == false && ScrollStateEnabled == true </code/> </code/> ;

我们要问自己的第一个问题是,DIV 容器应该什么时候呈现? 当 HorizontalScrollEnabled 等于 true 时,它绝对需要,但当 ScrollStateEnabled 等于 true 时也很好。 通过这种方式,我们可以很容易地在客户端代码中找到隐藏表单字段,而不用知道客户端的控件,我们可以在的DIVchildNodes 中搜索它,比如当前的实现。 实际上,只有当这两个属性都设置为 false 时,我们才会呈现 DIV

当页面通过网络时,有条件地呈现隐藏表单域也会节省我们的带宽。 已经授予了,只有美元的钱,但每个字节数。 通过忽略向编写器添加属性所需的步骤并呈现 HTML,我们也可以跳过服务器上的几个CPU周期。

protectedoverridevoid Render(HtmlTextWriter writer)
{
 if (!this.DesignMode)
 ScriptManager.RegisterScriptDescriptors(this);
 if (this.HorizontalScrollEnabled || this.ScrollStateEnabled) {
 // wrap this control in a DIV container }
 base.Render(writer);
 if (this.HorizontalScrollEnabled || this.ScrollStateEnabled) {
 if (this.ScrollStateEnabled) {
 // add a hidden field to store client scroll state }
 // close the container }
}

然而,我们已经编写的大多数属性呈现代码都不会改变。 重写的AddAttributesToRender() 方法根本不会改变,我们只需要将不同的代码块封装为其他两个方法的不同条件检查:

protectedvirtualvoid AddContainerAttributesToRender(HtmlTextWriter writer)
{
 // the container now depends on 2 different property valuesif (this.HorizontalScrollEnabled || this.ScrollStateEnabled) {
 // add required container attributesif (this.HorizontalScrollEnabled)
 {
 // add optional container attributes// move style declarations from the Style attribute// into the DIV container. }
 }
}protectedvirtualvoid AddScrollStateAttributesToRender(HtmlTextWriter writer)
{
 if (ScrollStateEnabled) 
 {
 // the hidden field should have an id,// name, type, and default value }
}

再次,我们几乎完成了服务器控制代码。 然后,我们可以将以前的方法包装在条件 block 中,以节省不必要的服务器指令。

protectedoverridevoid OnLoad(EventArgs e)
{
 base.OnLoad(e);
 if (this.ScrollStateEnabled) {
 // update the server control's scroll position state }
}

。然后再计算细节

在你的测试ASPX页面中设置 ScrollStateEnabledtrue 并尝试运行该示例,你会发现我们已经在代码中找到了。 如果打开了 IE 脚本错误通知,你将看到一个大错误,告诉我们我们试图 register 属性不存在。 我们创建一个属性部分,我们还需要更新用于检索容器和隐藏字段的属性。

// 2.)// Define the client control's class//DanLudwig.Controls.Client.ListBox = function(element)
{
 // initialize base (Sys.UI.Control)// declare fields for use by propertiesthis._scrollStateEnabled = null;// declare our original 3 fields}
// 3d)// Define the property get and set methods.//set_scrollStateEnabled : function(value)
{
 if (this._scrollStateEnabled!== value)
 {
 this._scrollStateEnabled = value;
 this.raisePropertyChanged('_scrollStateEnabled');
 }
}
,
get_scrollStateEnabled : function()
{
 returnthis._scrollStateEnabled;
}
,// helper method for retrieving the ListBox's DIV containerget_elementContainer : function()
{
 // only return the container if it has been renderedif (this.get_horizontalScrollEnabled()
 || this.get_scrollStateEnabled()) {
 returnthis.get_element().parentNode;
 }
 else {
 returnnull;
 }
}
,
get_elementState : function()
{
 // function locates and returns the hidden form field which// stores the scroll state data.if (this.get_scrollStateEnabled()) {
 // same v0.1 logic used to locate and return the// hidden form field }
 else {
 returnnull;
 }
}
,

我们的控件再次应该处于工作阶段,但它仍然仅当 HorizontalScrollEnabledScrollStateEnabled 等于( 也就是说,当它们都是 true 或者两个 false 时) 时才行。 为了处理它们不同的两个场景,我们必须评估事件和客户端逻辑。

如果你仔细看我们的客户代码,有一部分"应该应"就像一个痛苦的拇指: set_scrollState() 你注意到有什么奇怪的东西? 当然,它没有 get 方法,这通常表明代码设计策略很糟糕。 仔细观察,你会发现它并没有参数。 所以它根本不是一个属性,它只是一个 helper 函数。 此外,还记得我添加到版本 0.1演示的代码段中的Page_Load 代码Fragment? 由于 Firefox 与服务器状态失去同步,我禁用了页面缓存。 这是因为我们把 set_scrollTop()set_scrollLeft() inside 称为 set_scrollState()。 我们真的不需要从客户机代码中设置这些属性。 它们仅用于从服务器控件捕获滚动位置,以恢复以前的客户端状态,而不是将它的设置为。 如果这些属性可以更新服务器上的相应属性,我们将不需要隐藏字段。

如果 HorizontalScrollEnabledfalse 是,那么这个函数将不能正常工作,因为我们需要访问 ListBoxscrollTopscrollLeft 属性,而不是 DIV的。 无论你看什么,这段代码都需要一些重构。 下面是它应该看起来像的内容:

_onContainerScroll : function(e)
{
 // when the container is scrolled, update this control//OBSOLETE this.set_scrollState();this._updateScrollState();}
,// return the client scroll state data that will go in the hidden fieldget_scrollState : function()
{
 var scrollingElement = this.get_elementContainer();
 if (!this.get_horizontalScrollEnabled())
 scrollingElement = this.get_element();
 return scrollingElement.scrollTop + ':' + scrollingElement.scrollLeft;
}
,// set the hidden field if it is out of sync with the current client state_updateScrollState : function()
{
 var scrollState = this.get_scrollState();
 var hiddenField = this.get_elementState();
 if (hiddenField.value!== scrollState)
 hiddenField.value = scrollState;
}
,

就像这样这可以可重用。 我们现在需要处理事件的ListBoxonscroll,当 HorizontalScrollEnabledfalse 时。 让我们先将所有事件和用户界面初始化逻辑合并到单独的helper 方法中:

// 3a)// Override/implement the initialize method//initialize : function()
{
 // call base initialize DanLudwig.Controls.Client.ListBox.callBaseMethod(this, 'initialize');
 // initialize the eventsthis._initializeEvents();// initialize the control user interfacethis._initializeUI();}
,
_initializeEvents : function()
{
 // when horizontal scroll is enabled, use 3 zivros eventsif (this.get_horizontalScrollEnabled())
 {
 // create & add ListBox eventsthis._onkeydownHandler = Function.createDelegate(
 this, this._onKeyDown);
 this._onchangeHandler = Function.createDelegate(
 this, this._onChange);
 $addHandlers(this.get_element(),
 {
 'keydown' : this._onKeyDown
, 'change' : this._onChange
 }, this);
 // create & add DIV container event(s)this._onkeydownContainerHandler = Function.createDelegate(
 this, this._onContainerKeyDown);
 $addHandlers(this.get_elementContainer(),
 {
 'keydown' : this._onContainerKeyDown
 }, this);
 // need event to update hidden field when DIV is usedif (this.get_scrollStateEnabled())
 {
 this._onscrollContainerHandler = Function.createDelegate(
 this, this._onContainerScroll);
 $addHandlers(this.get_elementContainer(),
 {
 'scroll' : this._onContainerScroll
 }, this);
 }
 }
 // from here, it's safe to assert that horizontal scrolling// is not enabledelseif (this.get_scrollStateEnabled())
 {
 this._onscrollHandler = Function.createDelegate(
 this, this._onScroll);
 $addHandlers(this.get_element(),
 {
 'scroll' : this._onScroll
 }, this);
 }
}
,
// 3c)// Define the event handlers//_onScroll : function(e)
{
 if (this.get_element() &&!this.get_element().disabled)
 {
 this._updateScrollState();
 e.stopPropagation();
 returntrue;
 }
}
,

注意用于初始化事件的策略。 你可能已经注意到,在设置字段的隐藏值之前,我们创建的新 _updateScrollState() 方法没有检查是否启用了滚动状态。 这是因为在添加实际调用 _updateScrollState()的事件处理程序之前,我们要进行所有的属性。 当我们添加更多事件处理程序或者使用现有的事件处理程序执行其他任务时,可能需要在以后的一篇文章中。 但要记住,一次一件事 ! 下面是如何初始化事件的表格形式的表示形式。 "粗体"中的事件将更新隐藏字段,而"斜体"中的事件则需要从 DIV 容器中滚动。

ScrollStateEnabledHorizontalScrollEnabled
Truefalse
True
  • ListBox.onKeyDown
  • ListBox.onChange
  • DIV.onKeyDown
  • DIV.onScroll
  • ListBox.onScroll
false
  • ListBox.onKeyDown
  • ListBox.onChange
  • DIV.onKeyDown
  • 不注册任何事件。

所有这些都是好的,但是用这些变化测试它仍然会出错,因为我们尚未编写 _initializeUI() 方法。 我们不得不在写它时去掉我们使用的一些黑客,但是我们实际上需要"添加"来处理它。 在一次处理一个问题时,让我们看看使用这个代码时会发生什么:

_initializeUI : function()
{
 var listBox = this.get_element();
 var container = this.get_elementContainer();
 if (this.get_horizontalScrollEnabled())
 {
 // before changing the listbox's size,// store the original size in the container. container.originalListBoxSize = listBox.size;
 // this should be done regardless of how many items there are container.style.height = this.get_correctContainerHeight() + 'px';
 if (this.get_element().options.length> this.get_element().size)
 {
 // change the listbox's size to eliminate internal scrolling listBox.size = listBox.options.length;
 }
 }
 if (this.get_scrollStateEnabled())
 {
 this._restoreScrollState();
 }
 if (this.get_horizontalScrollEnabled())
 {
 if (container.scrollWidth <= parseInt(container.style.width))
 {
 listBox.style.width = '100%';
 listBox.style.height = container.scrollHeight + 'px';
 container.style.height = this.get_correctContainerHeight()
 + 'px';
 // the Firefox hack discussed in the previous article// should not be dealt with here. If an application// wants to hack a FF bug, it should be done on the// server before this control is rendered. }
 }
}
,
get_correctContainerHeight : function()
{
 var container = this.get_elementContainer();
 var listBox = this.get_element();
 var itemHeight = container.scrollHeight/listBox.size;
 var correctHeight = itemHeight * container.originalListBoxSize;
 return correctHeight;
}
,
_restoreScrollState : function()
{
 var scrollingElement = this.get_elementContainer();
 if (!this.get_horizontalScrollEnabled())
 scrollingElement = this.get_element();
 scrollingElement.scrollTop = this.get_scrollTop();
 scrollingElement.scrollLeft = this.get_scrollLeft();
}
,

尽管这个代码 block 仍然不必要的臃肿,但实际上我们改进了它。 至少可以在 postback 之后正确设置滚动位置,独立于 HorizontalScrollEnabled 属性。 对吧先试试吧。 在 Firefox 1.5中这段代码很好,看起来在IE7中可以很好地工作。 你可以滚动,启动 postback,并且 scrollTop 属性将被正确还原。 但是,尝试在 postback 后再次滚动 ListBox 会导致 IE 自动滚动到顶部。 Buggers !

结果是,这实际上是因为 IE 尝试帮助用户尽可以能多地帮助用户。 在我的演示中,有 4个 postback LinkButton。 对于 postback,其中任何一个都将 ListItem.Selected 属性设置为每个 ListBox 中的所有项的false。 在客户端上,这将导致 selectedIndex 设置为 -1。 我们在之后恢复了 ListBoxscrollTop的属性,并使用 selectedIndex vetos来设置自己的滚动位置。 幸好,知道这一点让我们可以轻松地解决它。

我们需要回滚并将注意力转回导致这里 Bug的_onScroll 事件处理程序。 从该块中,( 不,很容易) 可以计算与当前 scrollTop 值匹配的selectedIndex 值。 这样做,我们可以利用( 不,hack )的IE 尝试来弥补我们在这方面所做的一切。 如果我们将 selectedIndex 设置为特定计算,那么将它设置回 -1,IE7将提交给我们的权限。 记住,IE 只在 selectedIndex 等于 -1 时挑战我们。 如果有任何存在,我们不想混乱我们选择的用户项,因这里只需在 selectedIndex 等于 -1 时执行这里 hack。

在一个浏览器中修复,在另一个浏览器中中断

然而,这样做会使 Firefox 愤怒。 将 selectedIndex 设置回 -1 现在将导致它一直滚动到顶部。 而且,只要 selectedIndex 保持等于 -1,它就会阻止任何滚动。 如果我们在设置 selectedIndex 等于 -1 之前和之后比较 ListBoxscrollTop,我们会发现无论在什么情况下,它总是零。 我们不能简单地重置 scrollTop 值,因为这会导致事件处理程序再次触发,因为 selectedIndex 等于 -1

在某种程度上,我并不认为是JavaScript专家,所以我不确定如何通过更改元素属性值来防止事件处理程序中的代码( 如在场景中更改元素属性值)。 因这里,既然我们已经写了一个 hack,让我们继续写两个:

_onScroll : function(e)
{
 if (this.get_element() &&!this.get_element().disabled  &&!this._supressNextScroll)
 {
 this._updateScrollState();
 e.stopPropagation();
 //TODO -- what an ugly hack!!!!!var listBox = this.get_element();
 var itemSize = listBox.scrollHeight/listBox.options.length;
 if (listBox.selectedIndex == -1)
 {
 var oldScrollTop = listBox.scrollTop;
 listBox.selectedIndex = Math.round(
 listBox.scrollTop/itemSize);
 this._supressNextScroll = true;
 listBox.selectedIndex = -1;
 listBox.scrollTop = oldScrollTop;
 }
 returntrue;
 }
 else {
 this._supressNextScroll = false;
 }
}
,

好消息,坏消息

好消息是,第一次,我们可以说我们已经实现了我们最初为这里升级的行为。 在两个最常见的浏览器中。 幸运的是,由于 onscroll 在两个浏览器中单击了多次,因这里不会损害我们。 我们可以从 document.all 或者 window.event 测试,但这会比我们现在要更糟的是一个更糟的hack。

错误的消息是,这个代码不能在nautilus或者 in 1.0中工作得非常好。 在IE6中,我最初认为这是因为事件的ListBoxonscroll 没有被触发。 不是,但这是我们的问题,因为of总是返回零作为我们的ListBoxscrollTop的值。 这意味着我们甚至无法确定发送回服务器的正确滚动状态。 IE6总是在隐藏字段中发送" 0: 0"。 此时,使滚动状态在IE6中工作的唯一方法是启用水平滚动,因为 DIV 容器将发送正确的值。

通常情况下,在 Firefox 中情况略不明显。 当用户点击滚动条来浏览项目时,我们的控件会正常工作,因为这些操作触发了事件的ListBoxonscroll。 只有当按键和鼠标滚轮用于导航项目时,才会不正确地记录滚动状态。 在另一篇文章中我们将尝试处理鼠标滚轮,但是我们可以在事件的ListBoxonchange 中执行 _onScroll 处理程序(。_onContainerScroll 处理程序) 时,通过执行处理程序解决按键问题。

// from here, it's safe to assert that horizontal scrolling// is not enabledelseif (this.get_scrollStateEnabled())
 {
 this._onscrollHandler = Function.createDelegate(
 this, this._onScroll);
 $addHandlers(this.get_element(),
 {
 'scroll' : this._onScroll
 ,'change' : this._onScroll }, this);
 }
_onChange : function(e)
{
 if (this.get_element() &&!this.get_element().disabled)
 {
 updateListBoxScrollPosition(
 this.get_elementContainer(), this.get_element(), null);
 e.stopPropagation();
 if (this.get_scrollStateEnabled()){this._onContainerScroll(e);}returntrue;
 }
}
,

由于 ListBox 强制 selectedIndex 更改,所以在我们将它附加到 onchange 事件时,可以使用 _onScroll 处理程序( 或者 _onContainerScroll )。 尽管现在可以说 ListBox 支持 Firefox 1.0,但是本文的演示使用了 UpdatePanel 和异步回发。 不幸的是,微软表示,ASP.NET 不支持FF协议。 其实我已经成功了。 一台机器上的FF将处理演示罚款,而另一台机器上的FF会抱怨 UpdatePanel的。 你可以尝试注释演示密码中的Page_Init 处理程序,但不管 UpdatePanel s,我们的ListBox 绝对工作 in 1.0.

外出时外出

我知道我在这个系列的文章中保证了很短的时间,但是我确实是在我保证我们已经完成了。 我发现 ListBox的版本 0.2比 Safari 2.0.4 ( Mac OS X )的预期版本要好得多。 用户可以一边滚动一边查看长文本,当 HorizontalScrollEnabledtrue 时。 但是不幸的是,在postbacks中,滚动状态似乎没有保留。 我愿意使用这种方法,尤它的是在 Safari ( 版本 2.0.4至少) 中另一个奇怪的情况下,使用字母或者数字键。 如果Safari用户可以使用这个,那么他们必须得到一个滚动的硬方式。 对吧?

我还在 Safari public 测试版 3.0.1上对这个进行了简单测试。 这个版本的Safari允许它的余的键盘导航 ListBox 项目,但这个特性仍然不适合我们的ListBox。 - 为什么- 我很高兴你问。 因为键盘是用来改变 ListBox 选择的,所以它的onchange 事件不会被触发 ! 因为这样,我不会尝试使这个控制工作的任何beta版浏览器,如 Safari。 我们将在Safari发布产品版本后再次讨论。 在下一篇文章中,我们将选择离开的位置,并深入了解如何实施浏览器兼容性。


COM  lis  列表  component  列表框  
相关文章