In my previous post, I discussed separating drag-and-drop logic from view components as another tool to help achieve the cleanliness of single-responsibility classes. In this post, I’m going a step further by examining how the drag-and-drop functionality itself can be decoupled to achieve encapsulated components.
This post focuses on a scenario where objects are being dragged from one component to another (as opposed to simply rearranging objects within a single component). Further, these components may be quite different from each other (as opposed to being separate instances of the same component — although that scenario can benefit, too).
For example, one component may display a pool of objects that are “available” to be dragged somewhere. We can think of this as the “source” component (the “source” of the objects that can be dragged). In addition to this source component, we may have several “target” components. Perhaps the source/pool holds objects named “penny”, “nickel”, “dime”, and “quarter”. Then, we would have (at least) four “target” components displaying an image of each type of coin (and a place to drop a single, dragged object).
Across clients and projects, I see a tendency for drag-and-drop functionality to be lumped into a single, large view class. In the example discussed above, all of the logic for the source and target components, as well as their drag-and-drop logic, would be coupled (quadrupled?) together. This creates a lot of complexity, making it difficult to follow the flow. The method that handles the drag_drop logic, for example, would have to have conditional logic to determine whether the object was dropped on a target (and which one) … or dropped back on the source component (where do I need to add this object?). The same complexity typically exists for every drag-handler method. And, it often ends up a lot more complex if you start casting to different objects or implementing one-off behaviors for each component type. The root-cause of this complexity is the fact that each drag-handler method is responsible for multiple objects (the one where the drag started and the one where the object was dropped).
We tend to think of the happy-path drag-drop procedure progressing like this:
- drag start
- drag enter
- drag drop
- drag complete
This is simple and irresistibly logical. And, it is fine if all of your drag actions are occurring on a single component. However, if you are creating custom drag-and-drop functionality for separate components using this style of thinking, then the complexity and confusion discussed above can start to creep in.
It would be cleaner if each drag-handler was specific to a given component. But, if the source and target component logic is truly separated into separate components, what does this mean for the drag events (which events are called on which objects?)?
Once you separate these components, it eventually becomes clear that the events are separated like this:
- On the component that the item is being dragged AWAY FROM:
- drag start
- drag complete
- On the component that the item is being dragged TO:
- drag enter / over / exit
- drag drop
And, since the components are now separated, your drag-and-drop logic can only be for that object. This can really simplify your drag-and-drop logic. First of all, you can get rid of the conditional logic. Second, you can now build decoupled, encapsulated components that are not cluttered by the logic of other components.
You can couple this technique of drag-and-drop component separation with the previous post’s technique of keeping the drag-and-drop logic separate from the component. This can go a long way toward achieving clean, clear, single-responsibility classes. Instead of having all of this logic in a single, confusing, monolithic view class, the logic would now be split into four (or more) small, targeted classes (source component, target component, drag logic for source component, drag logic for target component). It can be quite refreshing to maintain code that is focused on a single-responsibility, rather than cluttered by logic that you are not interested in at the moment.
Below is some sample code to get you started. It shows the basic drag handlers for a source component and a target component. It does not show the components themselves, or refer to interfaces, etc. that you might use. But, hopefully, it’ll give you the basic idea.
*** Drag Logic for Source Component ***
public class DragSourceBehavior {
private var component:MyDragSourceUIComponent;
public function DragSourceBehavior(_component:MyDragSourceUIComponent) {
component = _component;
component.addEventListener(DragEvent.DRAG_ENTER, onDragEnter, false, 0, true);
component.addEventListener(DragEvent.DRAG_DROP, onDragDrop, false, 0, true);
}
private function onDragStart(event:MouseEvent):void {
if (DragManager.isDragging)
return;
var dragSource:DragSource = new DragSource();
DragManager.doDrag(event.currentTarget, dragSource, event);
component.removeObject(event.currentTarget);
}
private function onDragComplete(event:DragEvent):void {
var dragObject:UIComponent = event.currentTarget as UIComponent;
dragObject.dispatchEvent(new MouseEvent(MouseEvent.MOUSE_UP));
if (droppedOnDifferentObject(event))
removeDragEvents(event.currentTarget);
}
private function onDragEnter(event:DragEvent):void {
DragManager.acceptDragDrop(event.currentTarget as IUIComponent);
}
public function onDragDrop(event:DragEvent):void {
component.addObject(event.dragInitiator);
addDragEvents(event.dragInitiator);
}
public function addDragEvents(dragObject:UIComponent):void {
dragObject.addEventListener(MouseEvent.MOUSE_DOWN, onDragStart);
dragObject.addEventListener(DragEvent.DRAG_COMPLETE, onDragComplete);
}
public function removeDragEvents(dragObject:UIComponent):void {
dragObject.removeEventListener(MouseEvent.MOUSE_DOWN, onDragStart);
dragObject.removeEventListener(DragEvent.DRAG_COMPLETE, onDragComplete);
}
private function droppedOnDifferentObject(event:DragEvent):Boolean {
var droppedOnAnotherObject:Boolean = false;
if (event.relatedObject != component)
droppedOnAnotherObject = true;
return droppedOnAnotherObject;
}
}
*** Drag Logic for Target Component ***
public class MyDragTargetBehavior extends EventDispatcher {
private var component:MyDragTargetUIComponent;
public function MyDragTargetBehavior(_component:MyDragTargetUIComponent) {
component = _component;
component.addEventListener(DragEvent.DRAG_ENTER, onDragEnter, false, 0, true);
component.addEventListener(DragEvent.DRAG_DROP, onDragDrop, false, 0, true);
}
private function onDragStart(event:MouseEvent):void {
if (DragManager.isDragging)
return;
var dragSource:DragSource = new DragSource();
DragManager.doDrag(event.currentTarget, dragSource, event);
component.removeObject(event.currentTarget);
}
private function onDragComplete(event:DragEvent):void {
var dragObject:UIComponent = event.currentTarget as UIComponent;
if (droppedOnDifferentObject(event))
removeDragEvents(dragObject);
}
private function onDragEnter(event:DragEvent):void {
DragManager.acceptDragDrop(event.currentTarget as IUIComponent);
}
public function onDragDrop(event:DragEvent):void {
component.addObject(event.dragInitiator);
dispatchEvent(new CustomDragEvent(CustomDragEvent.OBJECT_DROPPED, event.dragInitiator, component));
}
private function droppedOnDifferentObject(event:DragEvent):Boolean {
var droppedOnAnotherObject:Boolean = false;
if (event.relatedObject != component)
droppedOnAnotherObject = true;
return droppedOnAnotherObject;
}
public function addDragEvents(dragObject:UIComponent):void {
dragObject.addEventListener(MouseEvent.MOUSE_DOWN, onDragStart);
dragObject.addEventListener(DragEvent.DRAG_COMPLETE, onDragComplete);
}
public function removeDragEvents(dragObject:UIComponent):void {
dragObject.removeEventListener(MouseEvent.MOUSE_DOWN, onDragStart);
dragObject.removeEventListener(DragEvent.DRAG_COMPLETE, onDragComplete);
}
}