高级 AJAX 列表框组件 v0.5
在目标浏览器中强制实现鼠标滚轮行为。
引言
在我上一篇文章中,我们修改了列表框,允许使用元键(SHIFT 和 CONTROL)来选择列表框中的多个项目。在本文中,我们将继续关注用户,并处理一种不同的输入场景:强大的鼠标滚轮。
背景
我们再次回顾一下在第二篇文章中列出的需求清单。
很难或不可能使用 SHIFT 和 CONTROL 键一次选择多个不连续的项目,因为它们的onkeydown
事件会根据选定的第一个项目触发滚动条的重新定位。我们应该读取事件的keyCode
并绕过此类“元”键按下的滚动代码。- 使用鼠标滚轮滚动项目的功能取决于浏览器以及加载页面的版本。我们希望将鼠标滚动设置为所有目标浏览器都支持的可配置选项。
只有当控件的HorizontalScrollEnabled
属性明确设置为true
时,垂直和水平滚动条位置才会在回发时保留。即使HorizontalScrollEnabled
为false
,我们也应该能够保留滚动状态事件。- 我们的事件处理策略不能完全支持 Firefox 1.0,因为一些改变滚动位置的用户交互不会执行
_onContainerScroll
事件处理程序。此外,如果你尝试调试此处理程序,你会注意到 IE 可以对单个鼠标点击或击键执行多达四次。拖动滚动条会导致我的 CPU 短暂跳到 30% 或更高,具体取决于我滚动的速度。如此多次更新隐藏字段会给客户端计算机的处理器带来不必要的负担。
现在应该很明显接下来要解决哪个需求。当我们甚至还没有处理所有必需的事件时,优化事件代码并不是一个好主意!
兼容性,嘘!
鼠标滚轮事件的默认效果取决于派生的 RequiresContainerScroll
属性、浏览器以及您正在使用的版本。以下是它目前行为的细分。
默认行为 | RequiresContainerScroll == true |
RequiresContainerScroll == false |
IE7 | 仅当鼠标位于滚动条上方时才滚动。 | 默认情况下滚动列表框。 |
IE6 | 默认情况下滚动 DIV 容器。 |
不适用 |
FF1 | 默认情况下阻止滚动。 | 当列表框获得焦点时,默认情况下滚动列表框。 |
FF1.5 | 默认情况下滚动列表框。 | 默认情况下滚动列表框。 |
FF2 | 默认情况下滚动列表框。 | 默认情况下滚动列表框。 |
Opera9 | 默认情况下滚动列表框。 | 不适用 |
如果能够控制鼠标滚轮,以便我们可以默认为上述行为矩阵,阻止滚轮滚动或强制执行它,那就太好了。如果这个属性可以在服务器控件上设置,无论是声明性地还是编程性地,那就更好了。我喜欢拥有好东西,所以我们就那样做吧。
鼠标滚轮 101
好消息是,我们目前已支持这六种目标浏览器(除了 Safari 2 之外,这涵盖了微软 ASP.NET AJAX 扩展框架支持的大多数浏览器)。不过,我们必须解决一个一致性问题。
Opera 和 IE 都有一个 onmousewheel
客户端事件,可以使用我们正在利用的事件框架进行处理。然而,Firefox 没有。它有一个我们可以监听的 DOMMouseScroll
事件。两者之间有许多不同之处,最重要的是 onmousewheel
事件将附加到控件原型,而 DOMMouseScroll
事件将附加到 ListBox(或 DIV
)元素。最终结果是,在事件处理程序期间无法引用某些客户端控件属性,因为对于 FF,this
引用将指向 ListBox,而不是原型。也许有办法将其附加到原型,但如果真有,我肯定不知道如何做到……所以我又写了一个 HACK :P
痛彻心扉的熟悉
// 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._mouseWheelScroll = null;
this._requiresContainerScroll = null;
this._scrollStateEnabled = null;
this._horizontalScrollEnabled = null;
this._scrollTop = null;
this._scrollLeft = null;
}
// 3d)
// Define the property get and set methods.
//
set_mouseWheelScroll : function(value)
{
if (this._mouseWheelScroll !== value)
{
this._mouseWheelScroll = value;
this.raisePropertyChanged('_mouseWheelScroll');
}
}
,
get_mouseWheelScroll : function()
{
return this._mouseWheelScroll;
}
,
现在你应该可以在睡梦中添加这样的属性了。我们稍后会编写服务器控件属性,但让我提醒你,它不会是一个布尔值。客户端控件的 _mouseWheelScroll
字段可以等于 true
、false
或 null
。Null 值将不会尝试处理鼠标滚轮的任何响应。当为 false
时,应该阻止滚轮滚动。当为 true
时,应该强制滚轮滚动。知道了这些,我们就有足够的信息来注册正确的事件了。
准备……开始……HACK!
_initializeEvents : function()
{
// handle mouse wheel events separately from all others
if (this.get_mouseWheelScroll() != null)
{
// IE and Opera have an onmousewheel event
this._onmousewheelHandler = Function.createDelegate(
this, this._onMouseWheel);
$addHandlers(this.get_element(),
{
'mousewheel' : this._onMouseWheel
}, this);
// also register the container's mouse wheel event
if (this.get_requiresContainerScroll())
{
$addHandlers(this.get_elementContainer(),
{
'mousewheel' : this._onMouseWheel
}, this);
}
// FF doesn't have an onmousewheel event
if (this.get_element().onmousewheel === undefined)
this.get_element().addEventListener('DOMMouseScroll',
this._onMouseWheel, false);
}
// rest of the event initialization code stays the same
}
,
// 3c)
// Define the event handlers
//
_onMouseWheel : function(e)
{
if (this._mouseWheelScroll == false)
{
e.preventDefault();
return false;
}
}
,
当 _mouseWheelScroll
为 false
时,这段代码足以阻止鼠标滚轮滚动……在 IE 和 Opera 中。但是请记住,由于 FF 使用 DOMMouseScroll
注册 _onMouseWheel
处理程序,它附加到 ListBox 而不是原型。因此,对于 FF,this._mouseWheelScroll
未定义。好消息是,我们可以将此属性添加到 ListBox,就像我们在 _initializeUI()
中向 DIV
添加 originalListBoxSize
属性一样。
但在那之前,让我们停下来思考一下。我们还需要访问其他属性吗?也许吧。与其仅仅添加 _mouseWheelScroll
字段,不如直接将整个原型添加为一个字段。我们同时也会给 DIV
添加相同的字段,因为在下一篇文章中我们需要它。
_initializeUI : function()
{
var listBox = this.get_element();
var container = this.get_elementContainer();
// hack to support mouse wheel scrolling
if (this.get_mouseWheelScroll() != null)
{
listBox._thisPrototype = this;
container._thisPrototype = this;
}
// rest of code stays the same
}
,
哪个是反向的?
现在,我们拥有处理 _mouseWheelScroll
等于 true
的情况所需的一切。在 IE 和 Opera 中,e
参数在其 rawEvent
中包含一个 wheelDelta
属性,该属性指示鼠标滚轮移动的方向和幅度。我们不关心幅度,所以让我们关注方向。向前滚动鼠标滚轮会产生一个正整数,而向后滚动滚轮会产生一个负整数。这意味着对于正值,我们希望减小 scrollTop
,对于负值,我们希望增加它。因此,当 wheelDelta
为负时,我们将方向设置为 1,当 wheelDelta
为正时,设置为 -1。
当然,Firefox 表现出相反的行为。当使用 DOMMouseWheel
事件时,我们可以使用 e.detail
访问一个类似 wheelDelta
的变量。向前旋转滚轮会产生负值,而向下移动会产生正值。因此,我们希望以与处理 wheelDelta
相反的方式处理方向。有了所有这些信息,设置正确元素的 scrollTop
属性并覆盖默认浏览器行为就轻而易举了。
// 3c)
// Define the event handlers
//
_onMouseWheel : function(e)
{
var _this = this._thisPrototype
if (_this === undefined)
_this = this;
// stop the mouse wheel from scrolling
if (_this.get_mouseWheelScroll() == false)
{
e.preventDefault();
return false;
}
// enforce mouse wheel scrolling
else if (_this.get_mouseWheelScroll() == true)
{
var listBox = _this.get_element();
var container = _this.get_elementContainer();
var direction, scrollingElement;
if (this._thisPrototype === undefined) // IE & Opera
{
// negative wheelDelta should increase scrollTop,
// positive wheelDelta should decrease the scrollTop.
direction = (e.rawEvent.wheelDelta > 1) ? -1 : 1;
}
else
{
// detail's direction is opposite of wheelDelta
direction = (e.detail > 1) ? 1 : -1;
}
// scroll the correct element
if (_this.get_requiresContainerScroll())
scrollingElement = container;
else
scrollingElement = listBox;
// scroll the ListBox by the height of one item in the correct direction.
var stepSize = scrollingElement.scrollHeight / listBox.options.length;
var newScrollTop = scrollingElement.scrollTop + (stepSize * direction);
scrollingElement.scrollTop = newScrollTop;
// tell the browser we're taking care of the mouse wheel.
e.preventDefault();
return false;
}
}
,
就到这里吧
目前,测试这个的唯一方法是手动更改客户端控件构造函数中的 _mouseWheelScroll
字段。下一步是将此客户端控件属性连接到可配置的服务器控件属性。不过,这次会有一个额外的步骤,因为 _mouseWheelScroll
不是一个普通的布尔属性。理想情况下,我们希望有一个枚举来赋予这三个值更多意义。
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
}
public enum ListBoxMouseWheelScrollSetting
{
NotSet,
Enforce,
Prevent
}
}
然而,我们的客户端控件属性不会识别此枚举。因此,我们需要两个服务器控件属性:一个用于设置值,另一个用于将其转换为客户端控件可以使用的值。
public virtual ListBoxMouseWheelScrollSetting MouseWheelScroll
{
set { this.ViewState["MouseWheelScroll"] = value; }
get
{
object output = this.ViewState["MouseWheelScroll"];
if (output == null)
output = ListBoxMouseWheelScrollSetting.NotSet;
return (ListBoxMouseWheelScrollSetting)output;
}
}
protected virtual bool? MouseWheelScrollClientValue
{
get
{
if (MouseWheelScroll.Equals(
ListBoxMouseWheelScrollSetting.Enforce))
return true;
else if (MouseWheelScroll.Equals(
ListBoxMouseWheelScrollSetting.Prevent))
return false;
return null;
}
}
我们现在可以在 ASPX 页面中声明性地或在代码隐藏文件(或其他服务器端代码)中编程性地配置服务器控件的 MouseWheelScroll
属性。然而,在 GetScriptDescriptors()
期间,我们希望将派生属性(一个可空的 bool
)传递给客户端控件,而不是枚举值。
protected virtual IEnumerable<ScriptDescriptor> GetScriptDescriptors()
{
ScriptControlDescriptor descriptor = new ScriptControlDescriptor(
"DanLudwig.Controls.Client.ListBox", this.ClientID);
descriptor.AddProperty("mouseWheelScroll",
this.MouseWheelScrollClientValue);
descriptor.AddProperty("requiresContainerScroll",
this.RequiresContainerScroll);
descriptor.AddProperty("scrollStateEnabled", this.ScrollStateEnabled);
descriptor.AddProperty("horizontalScrollEnabled",
this.HorizontalScrollEnabled);
descriptor.AddProperty("scrollTop", this.ScrollTop);
descriptor.AddProperty("scrollLeft", this.ScrollLeft);
return new ScriptDescriptor[] { descriptor };
}
哦不,又来了!
这足以满足大多数浏览器中的大多数用户输入场景,但仍然存在一些不一致之处。看看你下次能否找出需要修复的问题……