Introduction
All developers in some
moment in their lives have faced (or will be facing) the need of create a
graphic editor, an ER model editor, a flowchart editor, whatever... but a
common problem is that every time we start a project like these, we start
without a common way to implement that, there are patterns like MVC, MVP, MVVM
and others M-something. These patterns are useful but in the most of cases we
find a slight line in implementation where every looks like the same,
separating responsibilities is sometimes quite tricky and the more user
interaction is added, the more complicated is to support these changes in
developments, usually because we must to modify many components in the
implementation. Well in this article we discuss a way (maybe not the
best) to solve the user interaction problems. In general is not only
useful for graphic editors but these are good examples for this article.
I hope you like it.
Background
Let's suppose that we must
to create a shapes editor, then we start creating a class model but the user
interaction behavior for every shape is different, however, the device actions
set performed by users are the same for every shape. A user can click,
move, release its mouse, and is the same for other devices, the device actions
are finite, but ¿why is so difficult to understand the device inputs and give a
meaning to these actions? Well the answer is that the meaning for some action
depends on the 'context' it is raised. Let see an example.
A user press a mouse
button, if there is no shape in the mouse position a selection task must start,
but if there is a shape in that position we could think that user wants to
select or drag that shape, we can code this in an lazy way, but, what if this
action produces different meanings depending on the shape functionality for
example, if we have a box with an expand-button? or maybe a shape with an
internal “draggable” element? We must to manage all these conditions in
the editor but without a well-structured way to do that all becomes a
mess.
What I propose in the title
of this article is not to manage the context for those actions directly, but
let the context itself decide which must be the consequences for a specific
action. Working in that way, every time a new meaning is needed or
appears for a specific context is easier to maintain the code, every
time a new context appears in the development is easier to create and connect
with the existing development. The class diagram shown bellow
display a general idea about the code posted for this article and it is
organized to read from left to right and from top to bottom.
The first class we see is
the DashboardBase class
which is a user control and its responsibility is to catch the
control inputs for all devices (mouse, keyboard, pencil, whatever). This
class uses a Renderizer to
print the view state in the control's client area Graphics. Additionally the
dashboard references an InteractionManager which
processes all InteractionEvent instances built
by the dashboard control depending on the event raised. The InteractionManager class uses the View class and
its responsibilities are:
1. Manage the context for
the view.
2. Adapt the diagram
information (for example managing coordinates spaces) to the control depending
on some properties like zoom, offset.
The Selection class
is only a property used by the view to store the selected shapes and it is not
important in order to show the idea. The Diagram class is the model
we are creating or editing, this class contains for this example Layers and
Shapes. To keep it simple we only define a BoxShape and a LineShape. When
the view detects that an action have been performed, it analyzes whether exists
a shape capable to solve the context, if a shape is found that shape must solve
the context depending on its features. In that way, if the shape is
a BoxShape it can define the context depending on a drag area, a
rotation glyph or a re-size corner, all these with different
consequences. OK now we have the context, so, how it is used?
That's the wonderful part, every context share a common
interface called IInteractionContext. This
interface contains basically three methods.
Begin: When the context is
activated a transaction is started.
ProcessEvent; This method
filters all the user interactions that this context can manage, including
events available to commit or rollback the transaction modifying the view for
the specified event.
Commit: This method defines
in every context, the way all data must be applied to the diagram model or how
must be modified the view (in the Selection case for example).
As you can see, we define
in the top classes implementing the IInteractionContext. These classes
decide for every context which is the meaning for all the interaction events
available for the context. We can observe the following
contexts:
ShapeResizeContext: Context used to re-size a BoxShape.
SelectionMoveContext: Context used to move a shape in the Diagram.
MoveLineVertexContext: Context used to move the start point and end point for a
line.
SelectRectangleContext: Context used to select all shapes in a rectangle.