Introduction
Silverlight 2 ships with a lot of controls. Unfortunately, none of them supports drag and drop functionality needed by a lot of business applications. This article comes along with an implementation of a Drag and Drop Manager, which should be sufficient enough for most business application needs.
Features:
- Support for different drag-drop modes
- Moving mode
- Cloning mode
- Cloning-Once mode
- Full zooming/scaling support via a separate interface, or embedded scale transformations
- Visual feedback during dragging operation (including zooming/scaling support)
- Support of any
Panel
as a potential drop target
- Support of drag and drop for any type derived from
FrameworkElement
- Style support via a template control
- Support of composition and inheritance of drag and drop behavior
- Integrated z-index and focus (only for
Control
derived elements) handling
- Event handling for drag and drop elements and drop targets
- Full Expression Blend support (dependency properties/animations states)
- Keyboard support (Escape key for cancelling)
Background
Silverlight 2 offers a very nice development environment for .NET developers. Unfortunately, some controls/features are missing which are required in most Enterprise applications. One of these missing features is drag and drop handling.
The way drag and drop is implemented in most Silverlight applications today, is quite a direct approach, handling the necessary mouse events like MouseMove
and MouseLeftButtonUp
directly. Unfortunately, a lot of handy work needs to be done in order to get it setup in most scenarios.
Therefore, this article will introduce a Drag and Drop Manager, which can be used in various solutions, that handles most of the uncomfortable tasks like z-index mapping or panel-to-panel movements. Furthermore, the Drag and Drop Manager supports refactoring scenarios through the composition of classes (Decorator Pattern) and the creation of new drag and drop enabled styled template controls.
Before I explain the usage of this control, I'd like to summarize the main naming conventions of the objects in this component:
- Surface describes the drop target. This can be any object derived from
Panel
. The two interfaces IDropSurface
and IZoomAbleSurface
can be implemented by any surface.
DragAndDropElement
describes the dragging element in a drag and drop operation.
- The enumeration
DragAndDropMode
represents what is happening with a DragAndDropElement
when it is dragged.
Class Diagram of the DragDropManager Component
Creation of the Drag and Drop Manager
The demonstration program is divided in to two different sections. The upper half creates the Drag and Drop Manager through composition. The other half uses the DragAndDropControl
which forms a template control with style and animation support.
Creation of Drag and Drop Manager via composition
In a lot of scenarios, the refactoring of already created control elements might be required. In these cases, the usage of the embedded DragAndDropBuilder
might be the most convenient way to accomplish this. This builder will create the DragAndDropManager
object and hook into the appropriate events. The enumerable of Panel
objects (allowedDropSurfaces
) represent the Panel
objects where the DragAndDropElement
can be dropped on. If startDraggingOnMouseClick
is true
, it will bind additionally the MouseLeftButtonDown
event of the DragAndDropElement
with the dragging start.
Please note that the builder does not support the usage of the DragAndDropMode CloningOnce
setting in conjunction with startDraggingOnMouseClick
. If you need to compose a DragAndDropElement
with the DragAndDropMode CloningOnce
, set startDraggingOnMouseClick
to false
and write the necessary code in your application. As an example, please see the implementation of DragAndDropControl
.
static public class DragAndDropBuilder
{
public static DragAndDropManager Create(
FrameworkElement dragAndDropElement,
IEnumerable allowedDropSurfaces,
DragAndDropMode mode,
bool startDraggingOnMouseClick)
{
bool cloningEnabled = false;
bool cloningOnlyOnce = false;
switch (mode)
{
case DragAndDropMode.Moving:
break;
case DragAndDropMode.CloningOnce:
cloningEnabled = true;
cloningOnlyOnce = true;
break;
case DragAndDropMode.Cloning:
cloningEnabled = true;
break;
}
var dadm = new DragAndDropManager(
dragAndDropElement,
allowedDropSurfaces,
cloningEnabled,
cloningOnlyOnce);
Debug.Assert(
!(mode == DragAndDropMode.CloningOnce && startDraggingOnMouseClick),
"Unfortunately there is no support " +
"of the CloningOnce mode in combination with an " +
"automatical dragging handler. " +
"Set startDraggingOnMouseClick to false.");
if (startDraggingOnMouseClick)
{
dragAndDropElement.MouseLeftButtonDown +=
new MouseButtonEventHandler(dadm.StartDragging);
if (mode == DragAndDropMode.Cloning || mode == DragAndDropMode.CloningOnce)
dadm.ClonedDragDropElement += new
EventHandler<clonedeventargs<frameworkelement>
(dadm_ClonedDragDropElement);
}
return dadm;
}
static void dadm_ClonedDragDropElement(object sender,
ClonedEventArgs<frameworkelement /> e)
{
e.ClonedElement.MouseLeftButtonDown +=
new MouseButtonEventHandler(((DragAndDropManager)sender).StartDragging);
}
...
}
Usage in the main app (Page.xaml.cs):
var allowedSurfaces_1 = new Panel[] { this.CanvasDandD_1 };
DragAndDropBuilder.Create(
this.EllipseMoving,
allowedSurfaces_1,
DragAndDropMode.Moving,
true);
Creation of the Drag and Drop Manager through DragAndDropControl
When using the DragAndDropControl
, the creation of the DragAndDropManager
occurs completely behind the scenes. Therefore, this control will be most likely used when building a drag and drop supporting application from scratch, or when implementing additional drag and drop enabled controls. DragAndDropControl
is built as a template control, and supports various visual states (used by animations) and dependency properties. Therefore, it supports full customization in Expression Blend.
Control template (XAML code):
<Style TargetType="local:DragAndDropControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DragAndDropControl">
<Grid Background="{TemplateBinding Background}" x:Name="LayoutRoot">
-->
<vsm:VisualStateManager.VisualStateGroups>
<vsm:VisualStateGroup x:Name="CommonStates">
<vsm:VisualStateGroup.Transitions>
</vsm:VisualStateGroup.Transitions>
<vsm:VisualState x:Name="Normal">
<Storyboard />
</vsm:VisualState>
<vsm:VisualState x:Name="MouseOver">
<Storyboard />
</vsm:VisualState>
<vsm:VisualState x:Name="Disabled">
<Storyboard />
</vsm:VisualState>
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="DraggingStates">
<vsm:VisualStateGroup.Transitions>
</vsm:VisualStateGroup.Transitions>
<vsm:VisualState x:Name="DragStarted">
<Storyboard />
</vsm:VisualState>
<vsm:VisualState x:Name="Dropped">
<Storyboard />
</vsm:VisualState>
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="SurfaceStates">
<vsm:VisualState x:Name="SurfaceEnter">
<Storyboard />
</vsm:VisualState>
<vsm:VisualState x:Name="SurfaceLeave">
<Storyboard />
</vsm:VisualState>
</vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>
-->
<Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}">
<ContentPresenter
x:Name="contentPresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
VerticalAlignment="{TemplateBinding
VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding
HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}"/>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The demonstration program shows a very simple example of how to customize the style of the DragAndDropControl
. Afterwards the style can be used and the content can be set as in the following code snippet.
<DragDropManager:DragAndDropControl BorderBrush="#FF000000"
Foreground="#FF000000" Height="50"
Margin="30,61,225,90" Width="100" BorderThickness="2,2,2,2"
Style="{StaticResource DragAndDropControlStyle}"
d:LayoutOverrides="HorizontalAlignment, VerticalAlignment"
x:Name="ControlMoving" Padding="2,2,2,2"
Content="Moving">
The only part which will probably be done in code is the assignment of valid surfaces:
var allowedSurfaces_2 = new Panel[] { this.GridDandD_2, this.StackPanelDandD_3 };
this.ControlMoving.AllowedDropSurfaces = allowedSurfaces_2;
DragAndDropMode
The enumeration DragAndDropMode
is used to setup the different initial states of the DragAndDropManager
object.
Moving
: The DragAndDropElement
gets moved from one place to another.
Cloning
: The DragAndDropElement
gets cloned before the element can be moved somewhere else. The clone inherits the cloning behavior.
CloningOnce
: The DragAndDropElement
gets cloned only the first time. Any cloned element can only be moved afterwards.
Surface Interfaces (Zooming Behavior and Notifications)
The component supports that any Panel
object which is used as a potential target can inherit two related, optional interfaces. The first interface IZoomableSurface
implements a member ZoomFactor
, and represents the zooming factor of this object. This zooming factor is used when the control is hovered about the surface and scales the DragAndDropElement
accordingly. Alternatively of using this interface, the surface can use any kind of scale transform. This scale transform will be recognized by the DragAndDropElement
during its dragging operation as well. In most cases, the interface IZoomableSurface
might be only used if the surface is transformed through a non-supported matrix transform, or if the scaling of the surface element needs to be different from the scaling of a DragAndDropElement
.
The second interface enables surface elements to react to various drag and drop states (e.g., OnDropped
, OnDragStart
, etc.). Please see the following code as an easy example of how to use these interfaces.
public class MyDropStackPanel : StackPanel, IDropSurface, IZoomAbleSurface
{
public static TextBlock NotificationTextBlock { get; set; }
public MyDropStackPanel()
{
this.ZoomFactor = 1.5d;
}
#region IDropSurface Members
public void OnDropping(FrameworkElement droppingObject)
{
droppingObject.Margin = new Thickness(0.0d);
}
public void OnDropped(FrameworkElement droppedObject)
{
droppedObject.RenderTransform = null;
droppedObject.VerticalAlignment = VerticalAlignment.Center;
}
public void OnDragStart(FrameworkElement draggedObject)
{
NotificationTextBlock.Text = "Dragging started";
}
public void OnDragMouseOver(FrameworkElement draggedObject)
{
NotificationTextBlock.Text = "Mouse moved while element is dragged.
Dragged element name: " + draggedObject.Name;
}
public void OnElementSurfaceEnter(FrameworkElement enterObject)
{
NotificationTextBlock.Text = "Mouse entered surface while element is dragged.
Dragged element name: " + enterObject.Name;
}
public void OnElementSurfaceLeave(FrameworkElement leaveObject)
{
NotificationTextBlock.Text = "Mouse left surface while element is dragged.
Dragged element name: " + leaveObject.Name;
}
#endregion
#region IZoomAbleSurface Members
public double ZoomFactor
{
get; set;
}
#endregion
}
DragDropManager and DragAndDropControl Events
Another way of how to react to certain states is via the DragAndDropManager
events. Please note that the template control DragAndDropControl
implements exactly the same events. These events and their usage should be quite self-explanatory. Therefore, I dispense from further comments. However, a working example is implemented inside the DragAndDropControl
itself.
public event EventHandler<ClonedEventArgs<FrameworkElement>> ClonedDragDropElement;
public event EventHandler<DroppedEventArgs> Dropped;
public event EventHandler<DragStartedEventArgs> DragStarted;
public event EventHandler<DraggingEventArgs> Dragging;
public event EventHandler<SurfaceEnterLeaveEventArgs<FrameworkElement>> SurfaceEnter;
public event EventHandler<SurfaceEnterLeaveEventArgs<FrameworkElement>> SurfaceLeave;
Behind the Scenes
In the following part, I discuss some of the internals of the DragAndDropManager
object. When developing drag and drop capable applications in Silverlight, we first need to identify the best general way on how to move an object on the screen. In general, I believe there exist two different options for that. The first option is to set the Canvas.Left
and Canvas.Top
dependency properties. This solution has the negative impact that drag and drop only works on Canvas
derived objects. The second option is to use a TranslateTransform
. This option offers the advantage that it can be used for any kind of surface which derives from a Panel
. Therefore, the second option was chosen for this component.
The following code is used inside the DragAndDropManager
object, and starts the dragging behavior. I'd like to mention a specific point about the function SetDragDropElementLayoutToDefaultAndStore()
, which sets the default layout behavior for the element DragAndDropManager
. It ignores Canvas
and Margin
settings, and sets the default orientation to left and top. This allows on the one hand side, that interface designers can layout the initial positioning quite freely. On the other hand side, it forces a certain layout behavior in the target region. If there are some needs to customize this behavior, it should be implemented inside the appropriate DragAndDropManager
event, or via implementing the interface IDropSurface
in the drop targets.
[SuppressMessageAttribute(
"Microsoft.Security",
"CA2109",
Justification = "Can be used by external components " +
"in order to start dragging on mouse clicks")]
public void StartDragging(object sender, MouseButtonEventArgs e)
{
if (!this.StartDragging(e))
throw new InvalidOperationException("Unfortunately " +
"the dragging could not be initialized successfully.");
}
public bool StartDragging(MouseButtonEventArgs e)
{
if (this.mouseCaptured)
return false;
InitDraggingMode(e);
return true;
}
private void InitDraggingMode(MouseButtonEventArgs e)
{
CloneDragDropItem();
SetDragDropElementLayoutToDefaultAndStore();
CallIDropSurfaceEvent_OnDragStart();
InitDragAndDropElementLayout(e);
InitMouseForDragging();
this.initialZIndex =
SilverlightExtensions.GetZIndexAndSetMaxZIndex(this.DragAndDropElement);
Debug.Assert(
this.initialZIndex < short.MaxValue - 1,
"draggingInitialZIndex is supposed to have a smaller z value");
FocusDragAndDropElement();
CallEvent_DragStarted();
}
private void CloneDragDropItem()
{
if (this.CloningEnabled)
{
var dragClone = this.DragAndDropElement.Clone();
CallEvent_Cloned(dragClone);
((Panel)this.DragAndDropElement.Parent).Children.Add(dragClone);
if (this.CloningOnlyOnce)
{
this.CloningEnabled = false;
this.removeWhenDropCancelled = true;
}
}
}
private void SetDragDropElementLayoutToDefaultAndStore()
{
this.dragInitPos = new Point(
Canvas.GetLeft(this.DragAndDropElement),
Canvas.GetTop(this.DragAndDropElement));
this.dragInitMargin = this.DragAndDropElement.Margin;
this.dragInitHorizontalAlignment = this.DragAndDropElement.HorizontalAlignment;
this.dragInitVerticalAlignment = this.DragAndDropElement.VerticalAlignment;
this.dragInitRenderTransform = this.DragAndDropElement.RenderTransform;
this.dragInitRenderTransformOrigin = this.DragAndDropElement.RenderTransformOrigin;
Canvas.SetLeft(this.DragAndDropElement, 0.0);
Canvas.SetTop(this.DragAndDropElement, 0.0);
this.DragAndDropElement.Margin = new Thickness(0.0);
this.DragAndDropElement.HorizontalAlignment = HorizontalAlignment.Left;
this.DragAndDropElement.VerticalAlignment = VerticalAlignment.Top;
this.DragAndDropElement.RenderTransformOrigin = new Point(0.5, 0.5);
this.dragStartPanel = (Panel)this.DragAndDropElement.Parent;
this.dragInitTransformExcludeTranslate =
this.DragAndDropElement.RenderTransform.ExcludeTransform(
typeof(TranslateTransform));
}
private void InitDragAndDropElementLayout(MouseButtonEventArgs e)
{
Panel highestPanel =
SilverlightExtensions.GetFirstParentPanel(this.DragAndDropElement);
MoveDragDropElementToPanel(highestPanel);
var transform =
this.CombineTransformations(
e.GetPosition(highestPanel),
null,
1.0,
1.0);
this.DragAndDropElement.RenderTransform = transform;
}
private void MoveDragDropElementToPanel(Panel highestPanel)
{
SilverlightExtensions.RemoveElementFromParentPanel(this.DragAndDropElement);
if (highestPanel != null)
highestPanel.Children.Add(this.DragAndDropElement);
}
private void InitMouseForDragging()
{
this.mouseCaptured = true;
if (!this.DragAndDropElement.CaptureMouse())
throw new InvalidOperationException("Mouse could not be captured");
this.DragAndDropElement.Cursor = Cursors.Hand;
}
private void FocusDragAndDropElement()
{
if (this.DragAndDropElement.GetType().GetAllBaseTypes().Contains(typeof(Control)))
((Control)this.DragAndDropElement).Focus();
}
The other code inside DragAndDropManager
deals with different kind of transformations (scaling, positioning, and combinations), the support of the mentioned interfaces (via Reflection), cancelling of drag and drop operations, and event notifications. I'd be very keen on getting some feedback for these implementations. However, I think it's getting long-winded explaining all these small features yet. Therefore, I'd like to refer to the DragAndDropManager
class itself at this point.
Known Limitations / Potential Issues
Although I tried to minimize most shortcomings, I'd like to explain known potential conformities.
- The exception with the message: "Value does not fall within the expected range" is thrown, if names of the objects are not unique. When two objects have the same name (if they have a name) inside the same
Panel
, Silverlight throws this very self-explanatory exception. This can happen very easily in drag and drop scenarios.
- Bindings need to be valid in all potential surfaces. If you create e.g. a user control for one special surface and set the
DataContext
property of this user control in order to bind the dependency properties of a DragAndDropElement
, this binding won't be valid in the dragging mode or after dropping the element to another surface. Therefore, your binding strategy for a DragAndDropElement
should incorporate different potential parents.
- Surfaces need a background color. Otherwise, they are not exposed for hit-testing (https://silverlight.net/forums/p/34201/104012.aspx).
- A
DragAndDropElement
should have a fixed width and height at the moment dragging starts.
- The
DragAndDropBuilder
does not support the full creation of a CloningOnce
enabled DragAndDropElement
.
- A
Panel
derived object should be the "main" parent. Out of the box, this is usually a Grid
called "LayoutRoot
".
- Matrix transformations are not supported yet in the
DragAndDropElement
.
Conclusion
I have used this component for now in two Silverlight projects. One of them is some kind of graphical template editor with drag and drop support. It worked quite nicely inside these projects.
Because I have no further conclusions at this time, I'd like to mention that I appreciate any kind of comments, ideas, and criticisms about this control and/or this article.
References
History
- 12.2.2009 - Just published version 1.0