|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionMy open source AJAX Web Portal, www.dropthings.com, has an ASP.NET AJAX Extender which provides multi-column drag and drop for widgets. You can see similar drag and drop behavior in commercial AJAX Start Pages like Pageflakes. The Extender allows reordering of content on the same column and drag and drop content between columns. It also supports client-side notification so that you can call a web service and store the position of the content behind the scene without producing (async) postback. BackgroundDrag and Drop is very popular in AJAX websites. You can rearrange content on a website as you like, and this gives you some level of personalization. However, free form drag drop is a problem because the content gets messy as you drag things around on the page, and there's no logical organization. So, a popular choice for drag and drop is to use column-wise content flow where you can drag and drop content within a column or across columns. This ASP.NET AJAX Extender allows you to do that very easily. I first thought of going for a plain vanilla JavaScript based solution for drag and drop. It requires less code, less architectural complexity, and provides better speed. Another reason was the high learning curve for making Extenders the proper way in ASP.NET AJAX. However, writing a proper extender which pushes ASP.NET AJAX to the limit is a very good way to learn under-the-hood secrets of the ASP.NET AJAX framework itself. So, the two extenders I will introduce here will tell you almost everything you need to know about ASP.NET AJAX Extenders. The AJAX Control Toolkit (ACT) comes with a
The next challenge is with the
So, I have made a How to use the extenderHere's how you can attach this extender to any 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" />
The 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>
When a widget is dropped on the 1: function pageLoad( sender, e ) {
2:
3: var extender1 = $get("CustomDragDropExtender1");
4: extender1.add_onDrop( onDrop );
5:
6: }
you can do this: 1: <cdd:CustomDragDropExtender ID="CustomDragDropExtender1"
2: runat="server"
3: OnClientDrop="onDrop" />
When the event is raised, the function named When the event is fired, it sends the container, the widget, and the position of the widget where the widget is dropped. 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: }
The widget location is updated by calling
The server-side class CustomDragDropExtender.cs has the following code: 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: }
Most of the code in the extender defines the property. The important part is the declaration of the class: [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
{
The extender class inherits from The The The challenge is to make the client-side JavaScript for the extender. On the js file, there's a JavaScript pseudo class: 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: }
During initialize, it hooks on the 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: },
After initializing the
Discovering and initializing floating behavior for the draggable items is the challenging work: 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: },
Here's the algorithm:
The 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: },
When the user drags an item over the 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: },
Here, we deal with the drop cue. The challenging work is to find out the right position for the drop cue.
We need to find out where we should show the drop cue based on where the user is dragging the item. The idea is to find out the widget which is immediately below the dragged item. The item is pushed down by one position and the drop cue takes its place. While dragging, the position of the drag item can be found easily. Based on that, I locate the widget below the drag item: 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: },
This function returns the widget which is immediately under the dragged item. Now, I add the drop cue immediately above the widget: 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: },
One exception to consider here is that there can be no widget immediately below the dragged item. It happens when the user is trying to drop the widget at the bottom of a column. In that case, the drop cue is shown at the bottom of the column. When the user releases the widget, it drops right on top of the drop cue and the drop cue disappears. After the drop, the 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: }
Generally, you can make events in extenders by adding two functions in the 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: },
But this does not give you the support for defining the event listener name in the ASP.NET declaration: 1: <cdd:CustomDragDropExtender ID="CustomDragDropExtender1"
2: runat="server"
3: OnClientDrop="onDrop" />
The declaration only allows properties. In order to support such declarative assignment of events, we need to first introduce a property named 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: },
Raising the event is same as the basic AJAX events: 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: },
This is all about the 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: }
There’s only one property – This extender has a dependency on Besides the designer class, there’s one more class which 1: [ClientScriptResource(null, "CustomDragDrop.CustomFloatingBehavior.js")]
2: public static class CustomFloatingBehaviorScript
3: {
4: }
This class can be used inside the The client-side JavaScript is the same as the 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: }
Setting When the drag completes, the original 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: }
ConclusionYou can use the extender as it is. It should require no change in the code. It has been tested on IE 6, 7, Firefox 1.5, 2.0. Other browsers may not work. There's been a lot of discussion about this extender at my blog post. You are welcome to participate there.
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||