Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Professional Drag and Drop Manager Control for Silverlight 2

0.00/5 (No votes)
12 Feb 2009 1  
An article on a flexible Silverlight 2 Drag and Drop Manager and TemplateControl

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">
                    
                    <!-- State manager -->
                    <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>
                    
                    <!-- Content -->
                    <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)
{
    // Only initialize dragging mode if mouse was not captured
    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();
    // Send drag started event
    CallEvent_DragStarted();
}

private void CloneDragDropItem()
{
    if (this.CloningEnabled)
    {
        // --> Extension method: SilverlightExtensions.CloneObject(this);
        var dragClone = this.DragAndDropElement.Clone();
        CallEvent_Cloned(dragClone);
        ((Panel)this.DragAndDropElement.Parent).Children.Add(dragClone);
        // Disable cloning from future cloning?
        if (this.CloningOnlyOnce)
        {
            this.CloningEnabled = false;
            this.removeWhenDropCancelled = true;
        }
    }
}

private void SetDragDropElementLayoutToDefaultAndStore()
{
    // Store any kind of settings which might change during a drag drop operation
    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;
    // Ignore any kind of canvas settings, margins and alignment
    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;
    // Get object transform except translating
    this.dragInitTransformExcludeTranslate = 
        this.DragAndDropElement.RenderTransform.ExcludeTransform(
            typeof(TranslateTransform));
}

private void InitDragAndDropElementLayout(MouseButtonEventArgs e)
{
    // Get highest parent panel
    Panel highestPanel = 
      SilverlightExtensions.GetFirstParentPanel(this.DragAndDropElement);
    // Move this object to highest panel in order to be moved anywhere
    MoveDragDropElementToPanel(highestPanel);
    // Set correct X and Y position
    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()
{
    // DragAndDrop manager should not require a Control derived type 
    // but supports Focusing
    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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here