ASP.NET AJAX Extender 实现多列拖放






4.74/5 (25投票s)
一个 Extender,允许内容在列内和跨列之间拖放。支持按列进行内容流和重组。
引言
我的开源 AJAX Web Portal,www.dropthings.com,包含一个 ASP.NET AJAX Extender,它提供了多列拖放小部件的功能。您可以在商业 AJAX 开始页(如 Pageflakes)中看到类似拖放行为。该 Extender 允许在同一列中重排内容,以及在不同列之间拖放内容。它还支持客户端通知,这样您就可以调用 Web 服务并后台存储内容的位置,而无需进行(异步)回发。
背景
拖放功能在 AJAX 网站中非常流行。您可以根据自己的喜好重新排列网站上的内容,这提供了一定程度的个性化。然而,自由形式的拖放存在问题,因为当您在页面上拖动元素时,内容会变得混乱,缺乏逻辑组织。因此,一个流行的拖放选择是使用按列的内容流,您可以在列内或跨列拖放内容。这个 ASP.NET AJAX Extender 可以让您非常轻松地做到这一点。
我最初考虑使用纯 JavaScript 解决方案来实现拖放。它需要更少的代码,更低的架构复杂性,并提供更好的速度。另一个原因是,按照正确的方式在 ASP.NET AJAX 中创建 Extender 具有很高的学习曲线。但是,编写一个能充分发挥 ASP.NET AJAX 框架潜力的 Extender,是学习 ASP.NET AJAX 框架底层秘密的好方法。因此,我将在这里介绍的两个 Extender 将告诉您关于 ASP.NET AJAX Extender 的几乎所有知识。
AJAX Control Toolkit (ACT) 提供了 `DragPanel` Extender,我可以使用它为面板提供拖放支持。它还有一个 `ReorderList` 控件,我可以用它来提供单个列表中项目的重排功能。小部件本质上是垂直流在每一列中的面板。因此,我可能可以在每一列中创建一个 `ReorderList`,并使用 `DragPanel` 来拖动小部件。但是,我无法使用 `ReorderList`,因为
- `ReorderList` 严格使用 HTML 表格来渲染其在列中的项目。我的列中没有表格。`UpdatePanel` 中只有一个 `Panel`。
- `ReorderList` 接受一个拖动句柄模板,并在运行时为每个项目创建一个拖动句柄。我已经在小部件内部创建了一个拖动句柄,即小部件的标题。因此,我无法允许 `ReorderList` 创建另一个拖动句柄。
- 我需要在拖放时进行客户端回调,以便我能够进行 AJAX 调用并持久化小部件的位置。回调必须提供小部件被放置的 `Panel`、被拖动的小部件以及它被放置的位置。
下一个挑战是 `DragPanel` Extender。AJAX Control Toolkit 中拖放的默认实现存在一些问题
- 当您开始拖动时,项目会变为绝对定位,但当您释放它时,它不会变为静态定位。需要一个小的技巧来将原始位置恢复为“static”。
- 它不会将拖动项置于所有项目之上。因此,当您开始拖动时,您会看到被拖动的项目位于其他项目下方,这会导致拖动卡住,尤其是在存在 `IFRAME` 的情况下。
因此,我创建了一个 `CustomDragDropExtender` 和一个 `CustomFloatingExtender`。`CustomDragDropExtender` 用于放置小部件的列容器。它提供重排功能。您可以将此 Extender 附加到任何 `Panel` 控件。
如何使用 Extender
这是如何将此 Extender 附加到任何 `Panel` 并使其支持小部件拖放的方法
1: <asp:Panel ID="LeftPanel" runat="server" class="widget_holder" columnNo="0">
2: <div id="DropCue1" class="widget_dropcue">
3: </div>
4: </asp:Panel>
5:
6: <cdd:CustomDragDropExtender ID="CustomDragDropExtender1"
7: runat="server"
8: TargetControlID="LeftPanel"
9: DragItemClass="widget"
10: DragItemHandleClass="widget_header"
11: DropCueID="DropCue1"
12: OnClientDrop="onDrop" />
<cdd:CustomDragDropExtender>
提供以下属性
TargetControlID
- 将成为放置区的 `Panel` 的 IDDragItemClass
- `Panel` 内所有具有此类的子元素将可拖动。例如,小部件的 `DIV` 具有此类,因此它可以被拖动。DragItemHandleClass
- 可拖动元素内的任何具有此类的子元素将成为可拖动元素的拖动句柄。例如,小部件的 `Header` 区域具有此类,因此它充当小部件的拖动句柄。DropCueID
- `Panel` 内的一个元素 ID,该元素充当放置提示。OnClientDrop
- 在小部件被放置在 `Panel` 上时调用的 JavaScript 函数的名称。
`LeftPanel` 成为一个小部件容器,允许将小部件放置在其上并进行重排。Extender 上的 `DragItemClass` 属性定义了可重排的项目。这可以防止非小部件的 HTML Div
被重排。只有具有“widget”类的 `DIV` 才会被重排。假设有五个 `DIV` 具有名为“widget”的 class
。它将只允许重排这五个 `DIV`。
1: <div id="LeftPanel" class="widget_holder" >
2: <div class="widget"> ... </div>
3: <div class="widget"> ... </div>
4:
5: <div class="widget"> ... </div>
6: <div class="widget"> ... </div>
7: <div class="widget"> ... </div>
8:
9: <div>This DIV will not move</div>
10: <div id="DropCue1" class="widget_dropcue"></div>
11: </div>
当一个小部件被放置在 `Panel` 上时,Extender 会触发 `OnClientDrop` 中指定的函数。它提供了标准的 AJAX 事件。但是,与基本的 AJAX 事件不同,您必须以编程方式绑定到事件,而在这里,您可以定义一个属性并指定要调用的函数名。因此,而不是这样做
1: function pageLoad( sender, e ) {
2:
3: var extender1 = $get("CustomDragDropExtender1");
4: extender1.add_onDrop( onDrop );
5:
6: }
您可以这样做
1: <cdd:CustomDragDropExtender ID="CustomDragDropExtender1"
2: runat="server"
3: OnClientDrop="onDrop" />
当事件被触发时,名为 `onDrop` 的函数会被调用。这是通过 AJAX Control Toolkit 项目中提供的一些便捷库实现的。
当事件被触发时,它会发送容器、小部件以及小部件被放置的位置。
1: function onDrop( sender, e )
2: {
3: var container = e.get_container();
4: var item = e.get_droppedItem();
5: var position = e.get_position();
6:
7: //alert( String.format( "Container: {0}, Item: {1}, Position: {2}",
// container.id, item.id, position ) );
8:
9: var instanceId = parseInt(item.getAttribute("InstanceId"));
10: var columnNo = parseInt(container.getAttribute("columnNo"));
11: var row = position;
12:
13: WidgetService.MoveWidgetInstance( instanceId, columnNo, row );
14: }
通过调用 `WidgetService.MoveWidgetInstance` 来更新小部件的位置。
`CustomDragDropExtender` 包含三个文件
- CustomDragDropExtender.cs - 服务器端 Extender 实现
- CustomDragDropDesigner.cs - Extender 的设计器类
- CustomDragDropExtender.js - Extender 的客户端脚本
服务器端类 `CustomDragDropExtender.cs` 包含以下代码
1: [assembly: System.Web.UI.WebResource("CustomDragDrop.CustomDragDropBehavior.js",
"text/javascript")]
2:
3: namespace CustomDragDrop
4: {
5: [Designer(typeof(CustomDragDropDesigner))]
6: [ClientScriptResource("CustomDragDrop.CustomDragDropBehavior",
"CustomDragDrop.CustomDragDropBehavior.js")]
7: [TargetControlType(typeof(WebControl))]
8: [RequiredScript(typeof(CustomFloatingBehaviorScript))]
9: [RequiredScript(typeof(DragDropScripts))]
10: public class CustomDragDropExtender : ExtenderControlBase
11: {
12: // TODO: Add your property accessors here.
13: //
14: [ExtenderControlProperty]
15: public string DragItemClass
16: {
17: get
18: {
19: return GetPropertyValue<String>("DragItemClass", string.Empty);
20: }
21: set
22: {
23: SetPropertyValue<String>("DragItemClass", value);
24: }
25: }
26:
27: [ExtenderControlProperty]
28: public string DragItemHandleClass
29: {
30: get
31: {
32: return GetPropertyValue<String>("DragItemHandleClass", string.Empty);
33: }
34: set
35: {
36: SetPropertyValue<String>("DragItemHandleClass", value);
37: }
38: }
39:
40: [ExtenderControlProperty]
41: [IDReferenceProperty(typeof(WebControl))]
42: public string DropCueID
43: {
44: get
45: {
46: return GetPropertyValue<String>("DropCueID", string.Empty);
47: }
48: set
49: {
50: SetPropertyValue<String>("DropCueID", value);
51: }
52: }
53:
54: [ExtenderControlProperty()]
55: [DefaultValue("")]
56: [ClientPropertyName("onDrop")]
57: public string OnClientDrop
58: {
59: get
60: {
61: return GetPropertyValue<String>("OnClientDrop", string.Empty);
62: }
63: set
64: {
65: SetPropertyValue<String>("OnClientDrop", value);
66: }
67: }
68:
69: }
70: }
Extender 中的大部分代码用于定义属性。重要部分是类的声明
[assembly: System.Web.UI.WebResource("CustomDragDrop.CustomDragDropBehavior.js",
"text/javascript")]
namespace CustomDragDrop
{
[Designer(typeof(CustomDragDropDesigner))]
[ClientScriptResource("CustomDragDrop.CustomDragDropBehavior",
"CustomDragDrop.CustomDragDropBehavior.js")]
[TargetControlType(typeof(WebControl))]
[RequiredScript(typeof(CommonToolkitScripts))]
[RequiredScript(typeof(TimerScript))]
[RequiredScript(typeof(FloatingBehaviorScript))]
[RequiredScript(typeof(DragDropScripts))]
[RequiredScript(typeof(DragPanelExtender))]
[RequiredScript(typeof(CustomFloatingBehaviorScript))]
public class CustomDragDropExtender : ExtenderControlBase
{
Extender 类继承自 AJAX Control Toolkit (ACT) 项目中定义的 `ExtenderControlBase`。这个基类比 AJAX 运行时提供的 `Extender` 基类具有更多功能。它允许您定义 `RequiredScript` 属性,该属性可确保在下载和初始化 Extender 脚本之前下载所有必需的脚本。此 Extender 依赖于另一个名为 `CustomFloatingBehavior` 的 Extender。它还依赖于 ACT 的 `DragDropManager`。因此,`RequiredScript` 属性确保在下载此 Extender 脚本之前已下载这些脚本。`ExtenderControlBase` 是一个非常大的类,为我们做了很多工作。它包含发现 Extender 的所有脚本文件并正确渲染它们的默认实现。
`[assembly:System.Web.UI.WebResource]` 属性定义了包含 Extender 脚本的脚本文件。脚本文件是一个嵌入式资源文件。
`[ClientScriptResource]` 属性定义了 Extender 所需的脚本。此类也定义在 ACT 中。`ExtenderControlBase` 使用此属性来确定哪些 `.js` 文件为 Extender 工作,并正确渲染它们。
挑战在于创建 Extender 的客户端 JavaScript。在 `.js` 文件中,有一个 JavaScript 伪类
1: Type.registerNamespace('CustomDragDrop');
2:
3: CustomDragDrop.CustomDragDropBehavior = function(element) {
4:
5: CustomDragDrop.CustomDragDropBehavior.initializeBase(this, [element]);
6:
7: this._DragItemClassValue = null;
8: this._DragItemHandleClassValue = null;
9: this._DropCueIDValue = null;
10: this._dropCue = null;
11: this._floatingBehaviors = [];
12: }
在初始化时,它会附加到 Extender 所附加的 `Panel` 以及在 `Panel` 上拖放时显示的放置提示。
1: CustomDragDrop.CustomDragDropBehavior.prototype = {
2:
3: initialize : function() {
4: // Register ourselves as a drop target.
5: AjaxControlToolkit.DragDropManager.registerDropTarget(this);
6: //Sys.Preview.UI.DragDropManager.registerDropTarget(this);
7:
8: // Initialize drag behavior after a while
9: window.setTimeout( Function.createDelegate( this,
this._initializeDraggableItems ), 3000 );
10:
11: this._dropCue = $get(this.get_DropCueID());
12: },
在初始化 `DragDropManager` 并将 `Panel` 标记为放置目标后,它会启动一个计时器来查找 `Panel` 内的可拖动项目,并为它们创建浮动行为。浮动行为是使 `DIV` 可拖动的那个。
`FloatingBehavior` 使 `DIV` 可以在页面上自由拖动。但它不提供放置功能。`DragDropBehavior` 提供放置功能,它允许一个自由移动的 `DIV` 停留在固定位置。
发现和初始化可拖动项目的浮动行为是具有挑战性的工作。
1: // Find all items with the drag item class and make each item
2: // draggable
3: _initializeDraggableItems : function()
4: {
5: this._clearFloatingBehaviors();
6:
7: var el = this.get_element();
8:
9: var child = el.firstChild;
10: while( child != null )
11: {
12: if( child.className == this._DragItemClassValue && child != this._dropCue)
13: {
14: var handle = this._findChildByClass(child,
this._DragItemHandleClassValue);
15: if( handle )
16: {
17: var handleId = handle.id;
18: var behaviorId = child.id + "_WidgetFloatingBehavior";
19:
20: // make the item draggable by adding floating behaviour to it
21: var floatingBehavior =
$create(CustomDragDrop.CustomFloatingBehavior,
22: {"DragHandleID":handleId, "id":behaviorId,
"name": behaviorId}, {}, {}, child);
23:
24: Array.add( this._floatingBehaviors, floatingBehavior );
25: }
26: }
27: child = child.nextSibling;
28: }
29: },
算法如下
- 遍历 Extender 所附加的控件的所有直接子元素
- 如果子项具有可拖动项目的类,则
- 在子项下查找任何具有拖动句柄类的元素
- 如果找到这样的项,则将 `CustomFloatingBehavior` 附加到子项
`_findChildByClass` 函数会递归地遍历所有子元素,并查找具有指定类的元素。这是一个耗时的函数。因此,拖动句柄离可拖动元素越近越好。
1: _findChildByClass : function(item, className)
2: {
3: // First check all immediate child items
4: var child = item.firstChild;
5: while( child != null )
6: {
7: if( child.className == className ) return child;
8: child = child.nextSibling;
9: }
10:
11: // Not found, recursively check all child items
12: child = item.firstChild;
13: while( child != null )
14: {
15: var found = this._findChildByClass( child, className );
16: if( found != null ) return found;
17: child = child.nextSibling;
18: }
19: },
当用户将项目拖动到 Extender 所附加的 `Panel` 上时,`DragDropManager` 会触发以下事件
1: onDragEnterTarget : function(dragMode, type, data) {
2: this._showDropCue(data);
3: },
4:
5: onDragLeaveTarget : function(dragMode, type, data) {
6: this._hideDropCue(data);
7: },
8:
9: onDragInTarget : function(dragMode, type, data) {
10: this._repositionDropCue(data);
11: },
在这里,我们处理放置提示。具有挑战性的工作是找到放置提示的正确位置。
我们需要根据用户拖动项目的位置来确定在哪里显示放置提示。想法是找到紧挨在被拖动项目下方的那个小部件。该项目被向下推一个位置,放置提示取而代之。在拖动过程中,可以轻松找到拖动项的位置。基于此,我定位了拖动项下方的那个小部件。
1: _findItemAt : function(x,y, item)
2: {
3: var el = this.get_element();
4:
5: var child = el.firstChild;
6: while( child != null )
7: {
8: if( child.className == this._DragItemClassValue &&
child != this._dropCue && child != item )
9: {
10: var pos = Sys.UI.DomElement.getLocation(child);
11:
12: if( y <= pos.y )
13: {
14: return child;
15: }
16: }
17: child = child.nextSibling;
18: }
19:
20: return null;
21: },
此函数返回紧挨在被拖动项下方的那个小部件。现在,我在小部件的上方立即添加放置提示。
1: _repositionDropCue : function(data)
2: {
3: var location = Sys.UI.DomElement.getLocation(data.item);
4: var nearestChild = this._findItemAt(location.x, location.y, data.item);
5:
6: var el = this.get_element();
7:
8: if( null == nearestChild )
9: {
10: if( el.lastChild != this._dropCue )
11: {
12: el.removeChild(this._dropCue);
13: el.appendChild(this._dropCue);
14: }
15: }
16: else
17: {
18: if( nearestChild.previousSibling != this._dropCue )
19: {
20: el.removeChild(this._dropCue);
21: el.insertBefore(this._dropCue, nearestChild);
22: }
23: }
24: },
这里需要考虑的一个例外是,可能没有紧挨在被拖动项下方的小部件。当用户尝试将小部件放置在列的底部时,就会发生这种情况。在这种情况下,放置提示会显示在列的底部。
当用户释放小部件时,它会正好落在放置提示的上方,放置提示消失。放置后,会触发 `onDrop` 事件,以通知小部件被放置的位置。
1: _placeItem : function(data)
2: {
3: var el = this.get_element();
4:
5: data.item.parentNode.removeChild( data.item );
6: el.insertBefore( data.item, this._dropCue );
7:
8: // Find the position of the dropped item
9: var position = 0;
10: var item = el.firstChild;
11: while( item != data.item )
12: {
13: if( item.className == this._DragItemClassValue ) position++;
14: item = item.nextSibling;
15: }
16: this._raiseDropEvent( /* Container */ el,
/* droped item */ data.item,
/* position */ position );
17: }
通常,您可以通过在 Extender 中添加两个函数来创建 Extender 中的事件
1: add_onDrop : function(handler) {
2: this.get_events().addHandler("onDrop", handler);
3: },
4:
5: remove_onDrop : function(handler) {
6: this.get_events().removeHandler("onDrop", handler);
7: },
但这不支持在 ASP.NET 声明中定义事件监听器名称。
1: <cdd:CustomDragDropExtender ID="CustomDragDropExtender1"
2: runat="server"
3: OnClientDrop="onDrop" />
声明只允许属性。为了支持这种声明式的事件分配,我们首先需要在 Extender 中引入一个名为 `OnClientDrop` 的属性。然后,在属性赋值时,我们需要找出其中指定的函数,并在该函数上附加事件通知。从函数名解析函数是由 ACT 项目中可用的 `CommonToolkitScripts.resolveFunction` 完成的。
1: // onDrop property maps to onDrop event
2: get_onDrop : function() {
3: return this.get_events().getHandler("onDrop");
4: },
5:
6: set_onDrop : function(value) {
7: if (value && (0 < value.length)) {
8: var func = CommonToolkitScripts.resolveFunction(value);
9: if (func) {
10: this.add_onDrop(func);
11: } else {
12: throw Error.argumentType('value', typeof(value), 'Function',
'resize handler not a function,' +
' function name, or function text.');
13: }
14: }
15: },
触发事件与基本的 AJAX 事件相同。
1: _raiseEvent : function( eventName, eventArgs ) {
2: var handler = this.get_events().getHandler(eventName);
3: if( handler ) {
4: if( !eventArgs ) eventArgs = Sys.EventArgs.Empty;
5: handler(this, eventArgs);
6: }
7: },
以上就是 `CustomDragDropExtender` 的全部内容。下一个挑战是创建 `CustomFloatingBehavior`。服务器端类声明如下
1: [assembly: System.Web.UI.WebResource("CustomDragDrop.CustomFloatingBehavior.js",
"text/javascript")]
2:
3: namespace CustomDragDrop
4: {
5: [Designer(typeof(CustomFloatingBehaviorDesigner))]
6: [ClientScriptResource("CustomDragDrop.CustomFloatingBehavior",
"CustomDragDrop.CustomFloatingBehavior.js")]
7: [TargetControlType(typeof(WebControl))]
8: [RequiredScript(typeof(DragDropScripts))]
9: public class CustomFloatingBehaviorExtender : ExtenderControlBase
10: {
11: [ExtenderControlProperty]
12: [IDReferenceProperty(typeof(WebControl))]
13: public string DragHandleID
14: {
15: get
16: {
17: return GetPropertyValue<String>("DragHandleID", string.Empty);
18: }
19: set
20: {
21: SetPropertyValue<String>("DragHandleID", value);
22: }
23: }
24: }
25: }
只有一个属性——`DragHandleID`。小部件的标题充当拖动句柄。因此,标题 ID 在此处指定。
此 Extender 依赖于 `DragDropManager`,因此存在 `[RequiredScript(typeof(DragDropScripts))]` 属性。
除了设计器类,`CustomDragDropExtender` 还需要另一个类才能指定其对该浮动行为的依赖关系。
1: [ClientScriptResource(null, "CustomDragDrop.CustomFloatingBehavior.js")]
2: public static class CustomFloatingBehaviorScript
3: {
4: }
此类可以在 `RequiredScript` 属性中使用。它只定义哪个脚本文件包含 Extender 的客户端代码。
客户端 JavaScript 与 ACT 附带的 `FloatingBehavior` 相同。唯一的区别是在拖动开始时的一些技巧。`DragDropManager` 在调用 `startDragDrop` 时,不会将被拖动的项目恢复到静态位置。它也不会增加项目的 `zIndex`。如果拖动项不是最顶层的项目,在拖动时,它会显示在页面上其他元素之下。因此,我在行为的 `mouseDownHandler` 中做了一些修改,以添加这些功能。
1: function mouseDownHandler(ev) {
2: window._event = ev;
3: var el = this.get_element();
4:
5: if (!this.checkCanDrag(ev.target)) return;
6:
7: // Get the location before making the element absolute
8: _location = Sys.UI.DomElement.getLocation(el);
9:
10: // Make the element absolute
11: el.style.width = el.offsetWidth + "px";
12: el.style.height = el.offsetHeight + "px";
13: Sys.UI.DomElement.setLocation(el, _location.x, _location.y);
14:
15: _dragStartLocation = Sys.UI.DomElement.getLocation(el);
16:
17: ev.preventDefault();
18:
19: this.startDragDrop(el);
20:
21: // Hack for restoring position to static
22: el.originalPosition = "static";
23: el.originalZIndex = el.style.zIndex;
24: el.style.zIndex = "60000";
25: }
设置 `el.originalPosition = static` 修复了 `DragDropManager` 中的一个错误。在调用 `startDragDrop` 时,它错误地将 `originalPosition` 存储为绝对值。因此,在调用此函数后,我将其设置为正确的 `originalPosition`,即 `static`。
拖动完成后,会恢复原始的 `zIndex`,并清除 left、top、width 和 height 属性。`DragDropManager` 会将项目位置设置为静态,但它不会清除 left、top、width 和 height 属性。这会导致元素远离其放置的位置。此错误在 `onDragEnd` 事件中得到修复。
1: this.onDragEnd = function(canceled) {
2: if (!canceled) {
3: var handler = this.get_events().getHandler('move');
4: if(handler) {
5: var cancelArgs = new Sys.CancelEventArgs();
6: handler(this, cancelArgs);
7: canceled = cancelArgs.get_cancel();
8: }
9: }
10:
11: var el = this.get_element();
12: el.style.width = el.style.height = el.style.left = el.style.top = "";
13: el.style.zIndex = el.originalZIndex;
14: }
结论
您可以直接使用此 Extender。它应该不需要更改代码。它已在 IE 6、7、Firefox 1.5、2.0 上进行了测试。其他浏览器可能无法正常工作。关于此 Extender 的大量讨论可以在 我的博客文章 中找到。欢迎您参与讨论。