Wicked Code
Drag and Drop with ASP.NET AJAX
Jeff Prosise
Code download available at:WickedCode2008_01.exe(296 KB)
Contents
Drag-Drop in Action
DragDropManager
Building Drag Sources
Building Drop Targets
Creating Drag Sources and Drop Targets
Beyond Drag and Drop
AJAX has revolutionized Web user interfaces, and ASP.NET AJAX has made AJAX available to the Visual Studio® users. It comes in three separate downloads: ASP.NET AJAX Extensions (asp.net/ajax/downloads), which provides the core, fully tested set of AJAX functionality; ASP.NET AJAX Futures (asp.net/downloads/futures), which contains experimental features on which the product group wants feedback; and the ASP.NET AJAX Control Toolkit (ajaxcontroltoolkit.codeplex.com/), which provides a grab bag of AJAX controls as well as an SDK for building controls of your own.
Of the three, the Futures release has garnered the least attention from the developer community. That's unfortunate because, more than providing a glimpse into what future versions of ASP.NET AJAX might look like, the Futures Community Technology Preview (CTP) is chock full of features that can be used to build cutting-edge Web apps today. A case in point is drag-and-drop.
Hidden away inside the Futures PreviewDragDrop.js file lies support for rich, browser-based drag-and-drop user interfaces. The model it uses is patterned after the old OLE drag-drop model, in which drag sources implement the IDragSource interface, drop targets implement the IDropTarget interface, and the system provides a drag-drop manager to connect drag sources to drop targets. The Futures drag-drop manager is an instance of a JavaScript class named Sys.Preview.UI._DragDropManager, which is automatically instantiated and made available through a global variable named Sys.Preview.UI.DragDropManager.
For months now, I've been meaning to write a sample showing how to use PreviewDragDrop.js to implement real drag-drop, featuring custom drag sourcing and custom drop targeting. I finally got around to it, and the results are pretty cool. I learned quite a lot about DragDropManager in the process, including how to enhance it by adding support for custom drag visuals. Once you're familiar with the model (and comfortable with the concept of deriving classes and implementing interfaces in JavaScript), DragDropManager opens up a whole new world of possibilities for Web UIs.
Drag-Drop in Action
Before I dive into the code, take a moment to download the sample project that accompanies this article and give it a quick run-through. Open it using the Visual Studio Open Web Site command and then view the home page, DragDropDemo.aspx, in your browser. To run this sample, you must have ASP.NET AJAX Extensions installed. You don't have to download and install the Futures CTP because it's already in the site's Bin folder.
You'll see five color swatches at the top of the page and an empty box labeled "Drop It Here" at the bottom (see Figure 1). The color swatches are drag sources, and the empty box is the drop target. Grab one of the color swatches with your mouse and drag it down to the box. Observe how the box changes color from white to light gray when the cursor enters it, a feature known as drop-target highlighting. Now drop the color swatch over the box. The box changes color to match the swatch.
Figure 1** DragDropDemo.aspx in Action **(Click the image for a larger view)
Grab another color swatch and move it around the screen. Notice that the swatch can only be dropped over the drop target, and that the cursor indicates whether a drop would be accepted if it occurred right now. Also notice that as you drag a color swatch, a partially transparent rendition of the swatch—a "drag visual"—follows the cursor so you can see what you're dragging. As you'll see shortly, you write the code to create the drag visual, but ASP.NET AJAX handles everything else, including moving the drag visual with the cursor and updating the cursor with each mouse movement.
The custom logic that drives this page is located in a script file named ColorDragDrop.js in the Web site's Scripts folder. ColorDragDrop.js is a custom script file that's loaded by ScriptManager. In fact, open DragDropDemo.aspx in source view and you'll see that ScriptManager loads three script files: PreviewScript.js, which contains a key ASP.NET AJAX base class used in this demo; PreviewDragDrop.js, which contains the drag-drop support used by ColorDragDrop.js; and ColorDragDrop.js itself.
DragDropManager
The first step in building rich drag-drop user interfaces with ASP.NET AJAX is getting acquainted with the Sys.Preview.UI._DragDropManager class (hereafter referred to as DragDropManager). Under the hood, it delegates most operations to an instance of Sys.Preview.UI.IEDragDropManager or Sys.Preview.UI.GenericDragDropManager, depending on the host browser type. Its public interface consists of about a dozen methods, three of which play a vital role in drag-drop implementations:
- startDragDrop—initiates a drag-drop operation
- registerDropTarget—registers an object as a drop target
- unregisterDropTarget—unregisters a drop target
DragDropManager's primary job, once its startDragDrop method has been called, is to monitor ongoing mouse events, fire pertinent notifications to the drag source and the drop target (if any) beneath the cursor, and shape the cursor to provide a visual indication of what would happen if a drop occurred at the current location. To do this, it registers its own handlers for key events such as mousemove and mouseup. From these event handlers, it calls IDragSource methods on the source being dragged and IDropTarget methods on any drop targets below.
When a cursor carrying a payload moves within the boundaries of a drop target, for example, DragDropManager calls the drag source's get_dragDataType, get_dragMode, and getDragData methods to get information about the payload. Then it passes the results to the drop target's canDrop method to determine whether the drag source and drop target are compatible. If a drop occurs, DragDropManager notifies the drop target by calling its drop method, once more passing in data obtained from the drag source.
DragDropManager can determine whether the cursor is over a drop target because it maintains an internal array of references to objects registered as drop targets. Objects are added to the array by calling DragDropManager's registerDropTarget method and removed by calling unregisterDropTarget. It's important to unregister the drop targets you register so DragDropManager can unregister the associated event handlers it registers on behalf of those drop targets. Failure to do so can result in memory leaks.
DragDropManager is the glue that binds drag sources and drop targets together. Without it, you'd be writing a ton of code to make drag-and-drop user interfaces work—especially if they had to work in a variety of browsers.
Building Drag Sources
Before you can begin dragging, you must have something to drag—that is, a drag source. To ASP.NET AJAX, a drag source is simply an object that implements the IDragSource interface defined in PreviewDragDrop.js. Figure 2 lists the methods that belong to that interface.
Figure 2 IDragSource Methods
Method | Description |
---|---|
get_dragDataType | Returns the type of data being dragged. |
getDragData | Returns the data being dragged. |
get_dragMode | Returns the drag mode (move or copy). |
onDragStart | Called when dragging of the drag source begins. |
onDrag | Called repeatedly as the drag source is dragged. |
onDragEnd | Called when dragging ends. |
It may feel strange to implement interfaces in JavaScript given that JavaScript doesn't explicitly support interfaces. Nor, for that matter, does it explicitly support classes, inheritance, namespaces, and other tenets of object-oriented programming (OOP). And yet the client half of ASP.NET AJAX, the Microsoft® AJAX Library, is a class library implemented entirely in JavaScript. It fakes OOP using popular JavaScript programming techniques, which you can use to create classes of your own and even to implement interfaces in those classes and derive one class from another.
ColorDragDrop.js contains a pair of JavaScript classes named ColorDragSourceBehavior and ColorDropTargetBehavior. The former implements IDragSource, and both belong to a namespace named Custom.UI.
To create a JavaScript class in the style of the Microsoft AJAX Library, you begin by defining a function whose name is the same as the class name. This function serves as the class constructor:
Custom.UI.ColorDragSourceBehavior = function(element, color) { Custom.UI.ColorDragSourceBehavior.initializeBase(this, [element]); this._mouseDownHandler = Function.createDelegate(this, this.mouseDownHandler); this._color = color; this._visual = null; }
You then use the JavaScript prototype property to define methods to be included in every instance of the class:
Custom.UI.ColorDragSourceBehavior.prototype = { // Methods here }
JavaScript doesn't support strong typing or type reflection, so the Microsoft AJAX Library does it for you. Methods of the Type class (found in MicrosoftAjax.js, which is part of the ASP.NET AJAX core) such as registerClass and registerNamespace allow you to register the classes you build and assign them to namespaces. Later, you can use methods such as getTypeName, which is added to JavaScript's built-in Object type by the Microsoft AJAX Library, to reflect upon the types you registered.
Another essential piece of infrastructure provided by the Type class is a mechanism for calling a base class method from the corresponding method in a derived class. From a class constructor, you call initializeBase to call the base class's constructor, and from a method override in a derived class, you call callBaseMethod to invoke the equivalent method in the base class.
The ColorDragSourceBehavior class shown in Figure 3 uses all these tools to encapsulate the logic needed to convert an ordinary DOM element into a drag source in one easy-to-use class. From the call to registerClass, you can see that ColorDragSourceBehavior derives from the IDragSource interface as well as from an ASP.NET AJAX base class named Sys.UI.Behavior, which is defined in MicrosoftAjax.js. The former allows ColorDragSourceBehavior to function as a drag source; the latter enables instances of ColorDragSourceBehavior to be attached to DOM elements to modify their behavior.
Figure 3 ColorDragSourceBehavior
Custom.UI.ColorDragSourceBehavior = function(element, color) { Custom.UI.ColorDragSourceBehavior.initializeBase(this, [element]); this._mouseDownHandler = Function.createDelegate(this, this.mouseDownHandler); this._color = color; this._visual = null; } Custom.UI.ColorDragSourceBehavior.prototype = { // IDragSource methods get_dragDataType: function() { return 'DragDropColor'; }, getDragData: function(context) { return this._color; }, get_dragMode: function() { return Sys.Preview.UI.DragMode.Copy; }, onDragStart: function() {}, onDrag: function() {}, onDragEnd: function(canceled) { if (this._visual) this.get_element().parentNode.removeChild(this._visual); }, // Other methods initialize: function() { Custom.UI.ColorDragSourceBehavior.callBaseMethod(this, 'initialize'); $addHandler(this.get_element(), 'mousedown', this._mouseDownHandler) }, mouseDownHandler: function(ev) { window._event = ev; // Needed internally by _DragDropManager this._visual = this.get_element().cloneNode(true); this._visual.style.opacity = '0.4'; this._visual.style.filter = 'progid:DXImageTransform.Microsoft.BasicImage(opacity=0.4)'; this._visual.style.zIndex = 99999; this.get_element().parentNode.appendChild(this._visual); var location = Sys.UI.DomElement.getLocation(this.get_element()); Sys.UI.DomElement.setLocation(this._visual, location.x, location.y); Sys.Preview.UI.DragDropManager.startDragDrop(this, this._visual, null); }, dispose: function() { if (this._mouseDownHandler) $removeHandler(this.get_element(), 'mousedown', this._mouseDownHandler); this._mouseDownHandler = null; Custom.UI.ColorDragSourceBehavior.callBaseMethod(this, 'dispose'); } } Custom.UI.ColorDragSourceBehavior.registerClass ('Custom.UI.ColorDragSourceBehavior', Sys.UI.Behavior, Sys.Preview.UI.IDragSource);
Most of the methods that ColorDragSourceBehavior implements—get_dragDataType, getDragData, and so on—are members of the IDragSource interface. But ColorDragSourceBehavior implements a few methods that don't come from IDragSource. The initialize method, for example, registers a handler for mousedown events so the drag source can call DragDropManager.startDragDrop to begin a drag-drop operation. The dispose method deregisters the event handler to prevent memory leaks.
The IDragSource methods themselves are rather straightforward. get_dragDataType returns the string "DragDropColor" to identify the type of data that ColorDragSourceBehavior offers to drop targets—in this case, color data. getDragData returns a string defining the color represented by the drag source. get_dragMode returns Sys.Preview.UI.DragMode.Copy, which indicates that if a drop occurred right now, the drag source would be copied rather than moved. (The alternative is Sys.Preview.UI.DragMode.Move, which is defined along with Sys.Preview.UI.DragMode.Copy in PreviewDragDrop.js.)
Another item to note in ColorDragSourceBehavior is how it handles drag visuals. The mousedown event handler initiates a drag-drop operation by calling startDragDrop on DragDropManager. Before it calls startDragDrop, however, the handler calls cloneNode to clone the DOM element represented by the drag source. Then it sets the cloned node's opacity to 40 percent, inserts the node into the browser DOM, and passes a reference to the new node to DragDropManager as a parameter to startDragDrop. DragDropManager now assumes responsibility for the drag visual, animating its movement so that it follows the cursor.
Building Drop Targets
The ColorDropTargetBehavior class in ColorDragDrop.js (included in the code download) encapsulates the logic that converts DOM elements into drop targets. It derives from Sys.UI.Behavior, so it can be attached to DOM elements, and it implements the IDropTarget interface, so it can function as a drop target. Figure 4 lists the members of IDropTarget.
Figure 4 IDropTarget Methods
Method | Description |
---|---|
get_dropTargetElement | Returns a reference to the DOM element that is the drop target. |
canDrop | Returns true or false indicating whether a drop would be accepted. |
drop | Called when a drop occurs over the drop target. |
onDragEnterTarget | Called when a cursor carrying a payload enters the drop target. |
onDragInTarget | Called when a cursor carrying a payload moves within the drop target. |
onDragLeaveTarget | Called when a cursor carrying a payload leaves the drop target. |
ColorDropTargetBehavior's onDragEnterTarget and onDragLeaveTarget methods perform the drop-target highlighting demonstrated earlier. When a cursor carrying a payload enters the drop target, DragDropManager calls onDragEnterTarget. If the payload is of the correct type (DragDropColor), onDragEnterTarget saves the target's current background color in a field and changes the background color to light gray. If the cursor leaves the drop target still carrying a payload, DragDropManager calls the drop target's onDragLeaveTarget method. ColorDropTargetBehavior's implementation of that method restores the drop target's original background color.
Each time a cursor carrying a payload moves over the drop target, DragDropManager calls the drop target's canDrop method to determine whether the target would accept a drop. ColorDropTargetBehavior.canDrop checks the payload type and returns true if it's DragDropColor and false if it's not. This process allows the DragDropManager class to provide the necessary visual feedback to the user via the cursor.
If a drop occurs, the drop target's drop method is called. The ColorDropTargetBehavior.drop method extracts the color from the drag source and sets its own background color accordingly (actually, the color of the DOM element that serves as the drop target).
The dragMode parameter that is passed to the drop method specifies the drag mode, which is either move or copy. Ultimately, it is up to the drop target to manipulate the browser's DOM to honor the drag mode. DragDropDemo.aspx only supports copy mode; therefore the drop target "copies" the drag source by setting its own background color to the color contained in the drag source. If move mode were supported also, the drop target would need to check the drag mode following the completion of a successful drop and would then have to delete (or reposition) the DOM element associated with the drag source if dragMode were set to Sys.Preview.UI.DragMode.Move.
Creating Drag Sources and Drop Targets
Once ColorDragSourceBehavior and ColorDropTargetBehavior are implemented, building pages containing drag sources and drop targets is easy. Figure 5 shows the relevant markup and code from DragDropDemo.aspx.
Figure 5 Excerpt from DragDropDemo.aspx
<asp:ScriptManager ID="ScriptManager1" runat="server"> <Scripts> <asp:ScriptReference Name="PreviewScript.js" Assembly="Microsoft.Web.Preview" /> <asp:ScriptReference Name="PreviewDragDrop.js" Assembly="Microsoft.Web.Preview" /> <asp:ScriptReference Path="~/Scripts/ColorDragDrop.js" /> </Scripts> </asp:ScriptManager> <fieldset> <legend>Pick a Color</legend> <table cellpadding="8"> <tr> <!-- Drag sources --> <td><div id="RedDragSource" style="width: 32px; height: 32px; background-color: red" /></td> <td><div id="YellowDragSource" style="width: 32px; height: 32px; background-color: yellow" /></td> <td><div id="GreenDragSource" style="width: 32px; height: 32px; background-color: green" /></td> <td><div id="MagentaDragSource" style="width: 32px; height: 32px; background-color: magenta" /></td> <td><div id="BlueDragSource" style="width: 32px; height: 32px; background-color: blue" /></td> </tr> </table> </fieldset> <div style="width: 100%; height: 128px"> </div> <fieldset> <legend>Drop It Here</legend> <!-- Drop target --> <div id="DropTarget" style="width: 100%; height: 196px"> </div> </fieldset> <script type="text/javascript"> function pageLoad() { // // Make the "DragSource" DIVs drag sources. // var source1 = new Custom.UI.ColorDragSourceBehavior ($get('RedDragSource'), 'red'); var source2 = new Custom.UI.ColorDragSourceBehavior ($get('YellowDragSource'), 'yellow'); var source3 = new Custom.UI.ColorDragSourceBehavior ($get('GreenDragSource'), 'green'); var source4 = new Custom.UI.ColorDragSourceBehavior ($get('MagentaDragSource'), 'magenta'); var source5 = new Custom.UI.ColorDragSourceBehavior ($get('BlueDragSource'), 'blue'); source1.initialize(); source2.initialize(); source3.initialize(); source4.initialize(); source5.initialize(); // // Make the "DropTarget" DIV a drop target. // var target = new Custom.UI.ColorDropTargetBehavior ($get('DropTarget')); target.initialize(); } </script>
The color swatches that serve as drag sources are nothing more than DIVs with their background colors set to red, yellow, green, and so on. The JavaScript pageLoad function converts the DIVs into drag sources by instantiating five ColorDragSourceBehavior objects and assigning each to one of the DIVs. The calls to the drag source's initialize methods provide each drag source the opportunity to register its mousedown event handler.
The drop target, too, is a DIV. One statement instantiates a ColorDropTargetBehavior object and associates it with the DIV to convert the DIV into a drop target. A second statement calls the ColorDropTargetBehavior's initialize method so it can register as a drop target with DragDropManager.
ColorDragSourceBehavior and ColorDropTargetBehavior are specifically built to enable colors to be dragged and dropped. You can build classes like them to support other drag-drop scenarios. For example, imagine you were building a photo-sharing site and wanted to allow users to organize photos by dragging them between folders. You could use DIVs to represent the folders and write a PhotoDropTargetBehavior class to convert the DIVs into drop targets. Correspondingly, you could build a PhotoDragSourceBehavior class to turn DOM image elements into draggable photos. The classes you build tend to be very application-specific, but they all follow the same pattern exemplified by the drag-source and drop-target classes presented in this column.
Beyond Drag and Drop
ASP.NET AJAX is a wonderful tool for adding AJAX magic to Web pages, and the ASP.NET AJAX Futures release adds useful enhancements to the core platform. Support for drag-and-drop user interfaces is one example; other examples include animations, client-side data binding, and other handy DOM abstractions. Regardless of whether these features make it into core someday, they're available for use today. Consider using them in your next ASP.NET AJAX application to make a good app even better.
For another cool example of how to add fancy animations to Web pages by using the classes in the Futures PreviewGlitz.js file, see my blog post titled "Creating Sophisticated Animations with the Microsoft AJAX Library" at wintellect.com/cs/blogs/jprosise/archive/2007/04/04/Creating-Sophisticated-Animations-with-the-Microsoft-AJAX-Library.aspx.
Send your questions and comments for Jeff to wicked@microsoft.com.
Jeff Prosise is a contributing editor to MSDN Magazine and the author of several books, including Programming Microsoft .NET (Microsoft Press, 2002). He's also a cofounder of Wintellect (www.wintellect.com), a software consulting and education firm that specializes in the Microsoft .NET Framework. Contact Jeff at wicked@microsoft.com.