To solve dragging, we need to look very closely at the dragging sequence. First, the mouse is positioned over a control (causing it to go “hot”). Next, the left mouse button is held down. Then, the mouse is moved until some part of the control intersects the drop area. Finally, the left mouse button is released. This is a drag with perfect user procedure. What if the user very energetically clicks the left mouse button, causing the mouse to shift a few pixels before the button is released. Internally this may look like a drag, but it’s really a click. To avoid the problem of inadvertent dragging, we need to debounce the start of the drag. This can be done quite easily by not starting the drag until we have moved a few pixels.
Another scenario: the user begins a drag, but then clicks the right mouse button before dropping. What does this mean? For our purposes, we will interpret a right mouse click as a plea for help. Dragging can be dangerous (have you ever lost a folder in the Windows Explorer?), and to make our user feel comfortable with dragging we need to support their right to cancel the drag at any time. A right-click during a drag does this quite nicely.
The Drag Cycle
Let’s synthesize all of this into a target sequence: the Drag Cycle. The Drag Cycle will consist of:
- Start (left mouse down and mouse moved more than a few pixels).
- Drag (mouse moving with left mouse down).
- End (left mouse up).
- Cancel (right mouse down while dragging).
In addition to isolating the exact sequence of a drag, we need to recognize the different kinds of dragging. First, we have “factory” dragging, where a copy of the dragged item is made and then moved around in the form until it is dropped. Here the conceptual model for the user is: drag to create something. A second type of drag is the “move” drag, where no copy is made and the item itself moves during the drag. Here we are selling the drag as a way to rearrange items; nothing new is created. Both types of drags should be supported.
ApplicationZero and some tests
From our previous article, we have ApplicationZero:
The solid rectangle can be dragged over the frame and dropped. This is a move drag. Covering the full Drag Cycle, we have these tests:
- Position mouse over solid rectangle (goes hot).
- Position mouse over frame (nothing happens).
- Drag solid rectangle over frame (frame goes hot).
- Drag solid rectangle over the frame and drop (solid moves).
- Drag one pixel to the left (nothing happens due to debouncing).
- Drag one rectangle width to the left (dragging starts).
- Drag one rectangle width to the left and right click (dragging is cancelled).
- Drag over the frame and right click (dragging is cancelled).
You may have noticed that we have moved incomplete cases from the previous article into this case set. It seemed more appropriate for these cases to appear with the drag test sets.
The drag infrastructure
Enhancing our implementation we have:
WindowsForm - a standard
ControlSystem – the heart of the platform, routes form events to handling objects.
FormOverlay – ancestor for controls which fill the client area of the form.
ControlOverlay – ultimate parent for all standard controls, behind all other overlays.
DragOverlay – ultimate parent for controls involved in dragging, in front of
Mouse – uses a
HitTester to locate the hot control, calls
MouseTrap methods when one exists for the hot control.
HitTester – tunnels through the control composite, finding the front-most control.
MouseTrap – handles mouse events for a given control.
DropSite – defines a target area in the form where compatible DragBots can be dropped.
DragBot – defines a source area in the form from which dragging can commence.
DragManifest – houses DragBots and DropSites, facilitates dragging.
ControlSystem class hooks the mouse events of a standard Windows form, calling corresponding methods on
Mouse uses a
HitTester to find the front-most control for a given mouse event. When a control is found,
Mouse searches its collection of
MouseTraps, looking for a trap which corresponds to the control. If a
MouseTrap is found, then the corresponding mouse method is called. This is the basic flow of mouse handling.
Dragging is achieved by creating a
DropSite, and adding them to the
DragBot deploys a
DragTrap into the
DragTrap contains the debounce implementation (it doesn’t start the drag until the mouse has moved more than three pixels with the left mouse down).
For ApplicationZero, a
DragBot is created for the solid rectangle and a
DropSite is created for the frame. When the
DragBot begins moving the solid rectangle, checking the
DragManifest for any compatible
DropSites. The moment the solid rectangle intersects the frame, the manifest returns a
DropSite. At this point the
DropSite changes the appearance of the frame, indicating a legal drop. If the solid rectangle is dragged away, the
DropSite changes the frame back to its “cold” appearance. When the solid rectangle is released over the frame, the
DragBot requests a drop location from the
DropSite and then positions the solid rectangle accordingly. This creates the “snap-to” effect at the end of the drag.
With this infrastructure in place, the first eight tests now pass:
By creating a different
FactoryDragBot), we can change the behavior of ApplicationZero to that of a factory drag. Here the solid rectangle is cloned when the drag begins, and the clone is dragged. Tests are easily created for this behavior by copying the move drag tests and changing the declaration of the
DragBot. The greens for factory dragging:
Tweaks: Constraining the drag
The most basic of dragging behaviors are now in place. Let’s add one more thing before finishing up: axis-locking. Axis-locking makes it easy to constrain a drag to a vertical or horizontal line. The lock itself does something quite simple: it takes the delta (expressed as a point) of the drag operation and modifies it to conform to some rule. To constrict the drag to a horizontal or vertical line, we simply zero out the X or Y portion of the drag delta. The
DragBot is responsible for animating the drag sequence, and seems a pretty good place to install the constraint (which we will call a
Tweak.) To install a
Tweak we would code this:
MoveDragBot dragBot = new MoveDragBot(solidRectangle);
dragBot.Tweak = new HorizontalDragTweak();
We could design any number of drag tweaks, implementing snap-to type behaviors, etc. For now, let’s stick with vertical and horizontal. Our tests:
- MoveDrag with horizontal
- MoveDrag with vertical
- FactoryDrag with horizontal
- FactoryDrag with vertical
Just to help us manually check tweaks, we’ll paint a line on the form indicating the valid direction of the drag.
After checking the results of each case, we master, verify and move forward under full green.
We’ll, that’s it for ApplicationZero. In the next article we will use threading to animate these same tests. This technique will help us emulate a "real" user, making all future UI testing more lifelike...
As you look over the drag infrastructure, you may be thinking: Is all this really necessary? – It looks like a lot of over-engineering. To this we say: You may be right. Remember, our goal is to create a UI library that allows you to combine elements and create cutting edge UI as necessary. This is a very high goal indeed. As stated in the pioneering Design patterns work, "Designing object-oriented software is hard, and designing reusable object-oriented software is even harder". While this statement could not be more correct, we try to boil down any given approach to structure and strategy. That is, we are either defining something, or we are manipulating something.
When defining structure, we try to declare minimalist ancestors and interfaces in those areas where it is clear that extension will be necessary. Where it is not clear that extension will be necessary, we generally lock it down, as there is always a performance hit for extensibility. When defining strategy, we lean more towards pluggability. Ancestors and interfaces can work well for strategies, but they can also be a real nightmare. This is often due to the timing involved in strategies. A heavy ancestor which does “half” of the work for you, with little pinhole abstract methods that let you do things at "just the right time" seems to break down quickly as the system evolves. For example, see the
Tweak class introduced above. We like this class because it is focused in its mission and the ancestor does nothing. When you implement and plug a tweak, you are taking full control. This is how we try to fashion strategy classes.
But again, you may be right. As we move forward, implementing various dragging capabilities, we will see how well this infrastructure holds up.