Click here to Skip to main content
14,984,032 members
Articles / Desktop Programming / WPF
Article
Posted 9 Sep 2018

Stats

8.1K views
9 bookmarked

WPF Drag and Drop using NP.Visuals Library

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
9 Sep 2018CPOL13 min read
Drag and drop using NP.Visuals package

Introduction

What This Article is About

I am building several libraries that can help C# and WPF developers. The libraries are fully open source, available on GITHUB and also as NuGet packages.

In this article, I am going to describe the drag and drop functionality available as part of NP.Visuals.

NP.Visuals contains basic generic visual utilities, converters, behaviors and controls that can be of great help for WPF developers. There is nothing in this library that suggests any specific business logic - all of its classes can be used for any visual WPF project. NP.Visuals depends on two other libraries:

  1. NP.Utilities - a library containing some basic non visual utilities and extension classes
  2. NP.Concepts - a non-visual library containing more complex classes and behaviors

Behaviors

All of the drag and drop logic described in this article is behavior based. Because of this, I have provided a refresher of what behaviors are.

AFAIK, the term 'Behavior' was coined by the creators of Microsoft Blend SDK. By this term, they called some (not necessarily visual) classes that can be attached to visual controls and change their behavior by modifying their properties (during the attachment process) and more importantly - providing handlers for their events.

In iew-View Model based WPF and XAML Implementational Patterns, I give a lot of examples of behaviors, both visual and non-visual.

In a sense, behaviors provide a way to modify a behavior of a class non-invasively, without modifying the class itself.

In recent times, I started liking behaviors in the shape of static classes. Such behaviors are singletons, they do not require creating multiple behavior objects in order to attach to different controls. The attached properties defined within such behavior provide non-invasive extending of the classes to which the behavior is attached. The value of such attached property is dependent on the object it is attached to, even though the behavior is a singleton, so that each object can have its own attached property value within the context of the same behavior instance.

DragBehavior and DropBehavior described below provide a perfect illustration for such static behaviors.

Prerequisites

The article assumes that the readers have some basic knowledge of WPF including that of attached properties and bindings, as well as some understanding of the behaviors.

Code Samples

Code Location

All the code on Github at Drag And Drop Article Samples.

NuGet Dependencies and Compilation

The three libraries libraries mentioned above, namely:

  1. NP.Visuals
  2. NP.Concepts
  3. NP.Utilities

Are going to be NuGet'ed into every test project. Remember to make sure that your internet connection is up when you build each project for the first time - in that case, NuGet will automatically download the DLLs from NuGet.org.

Drag Only Samples

Introduction

In this subsection, we are going to present samples demonstrating Drag operation within a certain WPF panel.

Simple Drag, Drag with Boundaries Sample

This sample is located under NP.Tests.SimpleDragTest VS2017 solution.

This project illustrates a way to achieve dragging of an element within some drag container.

Try running this project. You will see a pink window with Blue rectangle at the top left and green circle at the center:

Image 1

Both the rectangle and the circle can be mouse-dragged.

Now let us take a look at the code. There is no any non-trivial C# code in this sample - the only file to look at is MainWindow.xaml:

XML
<Grid x:Name="DragContainer" 
      Background="Pink">
    <Rectangle Width="20"
               Height="20"
               HorizontalAlignment="Left"
               VerticalAlignment="Top" 
               Fill="Blue"
               visuals:DragBehavior.DragContainerElement=
               "{Binding RelativeSource={RelativeSource AncestorType=Panel}}"
               visuals:DragBehavior.DraggedElement=
                      "{Binding RelativeSource={RelativeSource Mode=Self}}"
               visuals:ShiftBehavior.Position="{Binding Path=
               (visuals:DragBehavior.Shift), RelativeSource={RelativeSource Mode=Self}}"/>

    <Ellipse Width="20"
               Height="20"
               HorizontalAlignment="Center"
               VerticalAlignment="Center"
               Fill="Green"
               visuals:DragBehavior.DragContainerElement="
               {Binding RelativeSource={RelativeSource AncestorType=Panel}}"
               visuals:DragBehavior.DraggedElement=
                          "{Binding RelativeSource={RelativeSource Mode=Self}}"
               visuals:ShiftBehavior.Position="{Binding Path=
               (visuals:DragBehavior.Shift), RelativeSource={RelativeSource Mode=Self}}" />
</Grid>  

The two draggable items have almost the same code. In fact, I wanted to show two items instead of one only to demonstrate that the behaviors can work on multiple items at the same time, in spite of being static.

Now, let me explain the significant lines one by one.

XML
  visuals:DragBehavior.DragContainerElement=
    "{Binding RelativeSource={RelativeSource AncestorType=Panel}}"

The line above specifies the Container for the drag behavior, i.e., the panel within which the element can be dragged.

XML
visuals:DragBehavior.DraggedElement="{Binding RelativeSource={RelativeSource Mode=Self}}"  

DraggedElement specifies the element to be dragged. Usually, it is set to 'Self' on the object to which the behavior is attached.

XML
visuals:ShiftBehavior.Position="{Binding Path=(visuals:DragBehavior.Shift),
                                         RelativeSource={RelativeSource Mode=Self}}"

ShiftBehavior.Position specifies the shift from the initial position. The way ShiftBehavior works is by creating a TranslateTransform and binding its X and Y properties to the point passed to it. So the line above is equivalent to:

XML
<FrameworkElement.TranslateTransfor>
 <TranslateTransform X="{Binding Path=(visuals:DragBehavior.Shift).X, 
                                    RelativeSource={RelativeSource Mode=Self}"
                        Y="{Binding Path=(visuals:DragBehavior.Shift).Y, 
                                    RelativeSource={RelativeSource Mode=Self}">
</FrameworkElement.TranslateTransfor>

If you play with the drag, you can see that you can drag both the rectangle and the circle outside of the application on all four sides. Next, we are going to modify the XAML code not to allow the circle to be dragged beyond the top and left sides. Here is the line that needs to be added to the circle's XAML code:

XML
visuals:DragBehavior.StartBoundaryPoint="0, 0"

so that the circle code now looks like:

XML
<Ellipse Width="20"
           Height="20"
           HorizontalAlignment="Center"
           VerticalAlignment="Center"
           Fill="Green"
           visuals:DragBehavior.DragContainerElement=
           "{Binding RelativeSource={RelativeSource AncestorType=Panel}}"
           visuals:DragBehavior.DraggedElement="
           {Binding RelativeSource={RelativeSource Mode=Self}}"
           visuals:ShiftBehavior.Position="{Binding Path=
           (visuals:DragBehavior.Shift), RelativeSource={RelativeSource Mode=Self}}"  
           visuals:DragBehavior.StartBoundaryPoint="0, 0"/>

Verify that you cannot move the circle beyond the left and top borders (though you can still move it beyond bottom and top borders). Also verify that setting the boundary point for the circle did not affect the boundaries for the rectangle - you can still move the rectangle beyond any one of the 4 boundaries.

Similarly, to prevent the circle from moving beyond right and bottom boundaries, one can set visuals:DragBehavior.EndBoundaryPoint to contain the ActionWidth and ActualHeight of the container panel. The best way to do it is to use a MultiValue converter from those values into a point.

Resizing Using DragBehavior

This sample is contained under NP.Tests.DragResizeTest project.

Here is what you see when running the project:

Image 2

A gray rectangle (or square) within a pink panel. The right bottom corner of the rectangle is red. Red square at right bottom corner is an imitation of a resizing thumb. If you drag the thumb, the size of the gray rectangle will change, but you won't be able to make it smaller than 20 by 20 or larger that 100 by 100 squares.

Here is the XAML code that makes it happen:

XML
<Grid x:Name="DragContainer"
      Background="Pink">
    <Grid x:Name="GlyphWithResizing"
          HorizontalAlignment="Left"
          VerticalAlignment="Top"
          Background="Gray"
          visuals:SizeSettingBehavior.InitialSize="50,50">
        <visuals:SizeSettingBehavior.RealSize>
            <MultiBinding Converter="{x:Static visuals:AddPointMultiConverter.Instance}">
                <Binding Path="(visuals:DragBehavior.TotalShiftWithRespectToContainer)"
                         ElementName="ResizingThumb" />
                <Binding Path="(visuals:ActualSizeBehavior.ActualSize)"
                         ElementName="ResizingThumb" />
            </MultiBinding>
        </visuals:SizeSettingBehavior.RealSize>
        <Rectangle x:Name="ResizingThumb"
                   HorizontalAlignment="Right"
                   VerticalAlignment="Bottom"
                   Width="10"
                   Height="10"
                   Fill="Red"
                   Cursor="SizeNWSE"
                   visuals:DragBehavior.StartBoundaryPoint="20,20"
                   visuals:DragBehavior.EndBoundaryPoint="100,100"
                   visuals:DragBehavior.DragContainerElement=
                   "{Binding ElementName=GlyphWithResizing}"
                   visuals:DragBehavior.DraggedElement=
                   "{Binding RelativeSource={RelativeSource Mode=Self}}" />
    </Grid>
</Grid>  

The gray rectangle is represented by a Grid named "GlyphWithResizing" with gray background. In its bottom right corner, it contains the red 10 by 10 Rectangle named "ResizingThumb". This ResizingThumb's DragBehavior is set pretty much like it was shown in the previous subsection:

XML
visuals:DragBehavior.StartBoundaryPoint="20,20"
visuals:DragBehavior.EndBoundaryPoint="100,100"
visuals:DragBehavior.DragContainerElement="{Binding ElementName=GlyphWithResizing}"
visuals:DragBehavior.DraggedElement="{Binding RelativeSource={RelativeSource Mode=Self}}"  

The DragContainerElement is set to the GlyphWithResizing grid. The boundaries of the drag operation are between (20, 20) and (100, 100) which prevents the drag beyond those boundaries of the container.

The interesting part of the code is related to the properties of GlyphWithResizing grid, in particular, the SizeSettingBehavior.

Line...

XML
visuals:SizeSettingBehavior.InitialSize="50,50"

...sets the initial size of the resized grid to be 50 by 50.

The following lines ensure that its size equals to the shift of the thumb with respect to the container plus the actual size of the thumb:

XML
<visuals:SizeSettingBehavior.RealSize>
    <MultiBinding Converter="{x:Static visuals:AddPointMultiConverter.Instance}">
        <Binding Path="(visuals:DragBehavior.TotalShiftWithRespectToContainer)"
                 ElementName="ResizingThumb" />
        <Binding Path="(visuals:ActualSizeBehavior.ActualSize)"
                 ElementName="ResizingThumb" />
    </MultiBinding>
</visuals:SizeSettingBehavior.RealSize>  

SizeSettingBehavior is responsible for controlling the size of an element it is attached to.

Dragging And Resizing Sample

Next sample is located under NP.Tests.DragAndResizeTest solution. It demonstrates a combination of two previous samples - the glyph can be both resized and dragged within its container.

Try running the sample. You will see exactly the same layout as in the previous sample, only now, you cannot only resize the gray square, but also drag it within the pink panel:

Image 3

Here is the code for the sample:

XML
<Grid x:Name="DragContainer"
      Background="Pink">
    <Grid x:Name="GlyphWithResizing"
          HorizontalAlignment="Left"
          VerticalAlignment="Top"
          Background="Gray"
          visuals:DragBehavior.DragContainerElement=
          "{Binding ElementName=DragContainer}"
          visuals:DragBehavior.DraggedElement=
          "{Binding RelativeSource={RelativeSource Mode=Self}}"
          visuals:SizeSettingBehavior.InitialSize="50,50"
          visuals:ShiftBehavior.Position=
          "{Binding Path=(visuals:DragBehavior.Shift), 
                              RelativeSource={RelativeSource Mode=Self}}">
        <visuals:SizeSettingBehavior.RealSize>
            <MultiBinding Converter=
            "{x:Static visuals:AddPointMultiConverter.Instance}">
                <Binding Path=
                "(visuals:DragBehavior.TotalShiftWithRespectToContainer)"
                         ElementName="ResizingThumb" />
                <Binding Path="(visuals:ActualSizeBehavior.ActualSize)"
                         ElementName="ResizingThumb" />
            </MultiBinding>
        </visuals:SizeSettingBehavior.RealSize>
        <Grid x:Name="MouseDownEventConsumingGrid" 
              visuals:EventConsumingBehavior.EventToConsume=
              "{x:Static FrameworkElement.MouseDownEvent}">
            <Rectangle x:Name="ResizingThumb"
                       HorizontalAlignment="Right"
                       VerticalAlignment="Bottom"
                       Width="10"
                       Height="10"
                       Fill="Red"
                       Cursor="SizeNWSE"
                       visuals:DragBehavior.StartBoundaryPoint="20,20"
                       visuals:DragBehavior.EndBoundaryPoint="100,100"
                       visuals:DragBehavior.DragContainerElement=
                       "{Binding ElementName=GlyphWithResizing}"
                       visuals:DragBehavior.DraggedElement=
                       "{Binding RelativeSource={RelativeSource Mode=Self}}" />
        </Grid>
    </Grid>
</Grid>

Here is what was changed In comparison to the previous sample. First, we have added two lines to the GlyphWithResizing:

XML
visuals:DragBehavior.DragContainerElement="{Binding ElementName=DragContainer}"
visuals:DragBehavior.DraggedElement="{Binding RelativeSource={RelativeSource Mode=Self}}" 

These two lines allow dragging the gray rectangle within the pink panel.

Also (and very importantly), the ResizingThumb rectangle is not contained directly by the GlyphWithResizing grid. There is MouseDownEventConsumingGrid that contains it. This Grid has EventConsumingBehavior.EventToConsume attached property set to FrameworkElement.MouseDownEvent:

XML
visuals:EventConsumingBehavior.EventToConsume="{x:Static FrameworkElement.MouseDownEvent}"  

This line ensures that the MouseDown event on the ResizingThumb will not bubble over to GlyphWithResizing grid so that only resizing will take place and not moving of the whole GlyphWithResizing grid.

Drag and Drop Samples

Introduction

In this section, I am going to demonstrate real drag and drop between different parts of an application.

Here are a couple of general differences from pure drag samples:

  1. While pure drag demos did not require any C# code, the drag-drop demos will operate on the view-models so, some C# code will be necessary.
  2. While in pure drag, we moved the object itself, the drag-drop will have a so drag cue, which will indicate the current position of the dragged object and also (possibly) drop cue which will indicate the position where the dropped item will be inserted.

Note that the item can potentially be dragged across multiple windows, so the best drag cue is a Popup (since it is not bound to a single window has properties that allow moving it across the screen and does not require to create a whole new WPF window just for the cue).

Drag from List Drop into Panel Test

This sample is located within NP.Tests.DragDropFromListToPanel project. It demonstrates dragging an item from a list (in fact, not quite a list but an ItemsControl) and dropping it into a Grid so that the location of the item within the Grid is determined by the drop location.

Try running this sample. You will see a list of light green rectangles with numbers from 1 to 5 on the left and a pink panel on the right separated from the list by a light blue column.

You can drag any item from the list and drop it into the pink panel. A glyph with the number written as a word will appear where you drop it:

Image 4

Note that the drag cue is almost transparent until the mouse is directly over the area into which it can be dropped. After that, the drag cue becomes fully opaque.

Let us start discussing the code from the view models. There are three of them:

C#
public class NumberVmWithPostion : NumberVm, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
    }

    Point2D _position = new Point2D();
    public Point2D Position
    {
        get => _position;

        set
        {
            if (_position.Equals(value))
                return;

            _position = value;

            OnPropertyChanged(nameof(Position));
        }
    }

    public NumberVmWithPostion(int n, string str, Point2D position) : base(n, str)
    {
        Position = position;
    }
} 
  1. NumberVm - a view model for an item in the list consists of an integer number and the string representing that number as a word:
    C#
    public class NumberVm
    {
        public int Number { get; }
        public string NumStr { get; }
    
        public NumberVm(int n, string str)
        {
            Number = n;
            NumStr = str;
        }
    }  
  2. NumberVmWithPosition - a view model for each item dropped into the glyph panel. It subclasses NumberVm adding position to it:
  3. TextVm - the full application View Model. It contains a collection of View Models for list items and a collection of View Models for glyphs. It also initializes the list items to View Models for numbers from 1 to 5:
    C#
    public class TestVm 
    {
        public ObservableCollection<NumberVm> ListItems { get; }
        public ObservableCollection<NumberVmWithPostion> Glyphs { get; }
    
        public TestVm()
        {
            ListItems = new ObservableCollection<NumberVm>();
    
            ListItems.Add(new NumberVm(1, "One"));
            ListItems.Add(new NumberVm(2, "Two"));
            ListItems.Add(new NumberVm(3, "Three"));
            ListItems.Add(new NumberVm(4, "Four"));
            ListItems.Add(new NumberVm(5, "Five"));
    
            Glyphs = new ObservableCollection<NumberVmWithPostion>();
        }
    } 

In MainWindow.xaml.cs file, a TestVm object is created and assigned to be the DataContext of the whole window:

C#
public MainWindow()
{
    InitializeComponent();

    DataContext = new TestVm();
} 

There is one more C# class DropIntoGlypPanelOperation. This file defines the operation that happens during the drop:

C#
public class DropIntoGlyphPanelOperation : IDropOperation
{
    // drop method
    public void Drop
    (
        FrameworkElement draggedAndDroppedElement, 
        FrameworkElement dropContainer, 
        Point mousePositionWithRespectToContainer)
    {
        // get the dragged/dropped element's view model
        NumberVm droppedVm = 
            draggedAndDroppedElement.DataContext as NumberVm;

        // get the drop container's view model
        TestVm testVm = 
            dropContainer.DataContext as TestVm;

        // create the inserted glyph's view model
        // note that the last constructor argument
        // specifies the glyph's position within 
        // the drop container. 
        NumberVmWithPostion newGlyphVm = new NumberVmWithPostion
        (
            droppedVm.Number,
            droppedVm.NumStr,
            mousePositionWithRespectToContainer.ToPoint2D());

        // insert the new glyph into the Glyphs collection
        // of the view model 
        testVm.Glyphs.Add(newGlyphVm);
    }
} 

This DropIntoGlyphPanelOperation is attached to the drop behavior via an attached property, as will be shown in XAML.

Now let us switch to XAML code. The XAML code of this sample is more complex than in drag only samples described above, so, I am going to give a high level overview of the code and then will describe each item one by one.

At high level, the XAML code consists of the Grid panel called "TopLevelPanel". The grid has 3 columns - first used for the list of items, the second is used as a vertical separator and the third is used for the glyphs:

XML
<Grid x:Name="TopLevelPanel">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    
    <!--Drag Cue-->
    <Popup x:Name="TheDragCue" .../>

    <!--Number Items (Drag Source)-->
    <ItemsControl x:Name="DragSourceList" .../>

    <!--Vertical Separator-->
    <Grid x:Name="Separator" .../>

    <!--Glyphs ItemsControl-->
    <ItemsControl x:Name="GlyphsItemsControl" .../>
</Grid> 

Here is the full code for Drag Cue popup:

XML
<Popup x:Name="TheDragCue"
       AllowsTransparency="True"
       Placement="RelativePoint"
       PlacementTarget="{Binding ElementName=TopLevelPanel}"
       IsOpen="{Binding Path=(visuals:DragBehavior.IsDragOn), Mode=OneWay}"
       HorizontalOffset="{Binding Path=(visuals:DragBehavior.TotalShiftWithRespectToContainer).X}"
       VerticalOffset="{Binding Path=(visuals:DragBehavior.TotalShiftWithRespectToContainer).Y}"
       DataContext="{Binding Path=(visuals:DragBehavior.CurrentlyDraggedElement),
                             ElementName=TopLevelPanel}">
    <Popup.Resources>
        <visuals:BinaryToDoubleConverter x:Key="DragCueOpacityConverter"
                                         TrueValue="1"
                                         FalseValue="0.4" />
    </Popup.Resources>
    <visuals:LabelContainer x:Name="TheCueLabelContainer"
                            Background="LightGreen"
                            TheLabel="{Binding Path=DataContext.Number}"
                            Width="{Binding Path=ActualWidth}"
                            Height="{Binding Path=ActualHeight}"
                            Opacity="{Binding Path=(visuals:DropBehavior.IsDragAbove), 
                                              Converter={StaticResource DragCueOpacityConverter},
                                              ElementName=GlyphsItemsControl}" />
</Popup>

Note, that the popup's DataContext is set to the DragBehavior.CurrentlyDraggedElement of the drag container panel:

C#
DataContext="{Binding Path=(visuals:DragBehavior.CurrentlyDraggedElement),
                      ElementName=TopLevelPanel}"  

Because of that, we can bind the IsOpen, HorizontalOffset and VerticalOffset properties to the corresponding DragBehavior properties on the currently dragged object (the DataContext of the popup:

C#
IsOpen="{Binding Path=(visuals:DragBehavior.IsDragOn), Mode=OneWay}"
HorizontalOffset="{Binding Path=(visuals:DragBehavior.TotalShiftWithRespectToContainer).X}"
VerticalOffset="{Binding Path=(visuals:DragBehavior.TotalShiftWithRespectToContainer).Y}"  

The LabelContainer is just a TextBlock within a Border object serving to display the Drag Cue. Note that its Width and Height are set to be the same as the dragged object ActualWidth and ActualHeight:

C#
Width="{Binding Path=ActualWidth}"
Height="{Binding Path=ActualHeight}"  

Also note the binding of its Opacity property:

C#
Opacity="{Binding Path=(visuals:DropBehavior.IsDragAbove), 
                  Converter={StaticResource DragCueOpacityConverter},
                  ElementName=GlyphsItemsControl}"  

It is bound to DropBehavior.IsDragAbove attached property of the GlyphsItemsControl element, which specifies if the drop into the glyphs container is possible.

Here is the code for the number items List (the source of the drag/drop operation):

XML
<!--Number Items (Drag Source)-->
<ItemsControl x:Name="DragSourceList"
              ItemsSource="{Binding Path=ListItems}"
              Width="100"
              ItemTemplate="{StaticResource NumberItemDataTemplate}">
    <ItemsControl.ItemContainerStyle>
        <Style TargetType="FrameworkElement">
            <Setter Property="visuals:DragBehavior.DragContainerElement"
                    Value="{Binding ElementName=TopLevelPanel}" />
            <Setter Property="visuals:DragBehavior.DraggedElement"
                    Value="{Binding RelativeSource={RelativeSource Mode=Self}}" />
            <Setter Property="visuals:DragBehavior.BounceBackAtDragEnd"
                    Value="True" />
            <Setter Property="Margin"
                    Value="10,2" />
        </Style>
    </ItemsControl.ItemContainerStyle>
</ItemsControl> 

We use the ItemContainerStyle to set the DragBehavior properties for every item within the list. DragContainerElement and DraggedElement properties should be familiar to you from the previous sections. BoundBackAtDragEnd property set to true will restore the DragBehavior.Shift property to the original value before the drag operation occurred.

Finally, here is the code for the GlyphsItemsControl:

XML
<!--Glyphs ItemsControl-->
<ItemsControl x:Name="GlyphsItemsControl"
              Grid.Column="2"
              Background="Pink"
              visuals:ActualSizeBehavior.IsSet="True"
              HorizontalAlignment="Stretch"
              VerticalAlignment="Stretch"
              ItemsSource="{Binding Path=Glyphs}"
              visuals:DropBehavior.ContainerElement=
              "{Binding RelativeSource={RelativeSource Mode=Self}}"
              visuals:DropBehavior.DraggedElement=
              "{Binding Path=(visuals:DragBehavior.CurrentlyDraggedElement), 
                                         ElementName=TopLevelPanel}"
              visuals:DropBehavior.TheDropOperation=
                     "{x:Static local:DropIntoGlyphPanelOperation.Instance}"
              ItemTemplate="{StaticResource NumberNameItemDataTemplate}">
    <ItemsControl.ItemContainerStyle>
        <Style TargetType="FrameworkElement">
            <Setter Property="HorizontalAlignment"
                    Value="Left" />
            <Setter Property="VerticalAlignment"
                    Value="Top" />
            <Setter Property="Width"
                    Value="50" />
            <Setter Property="Height"
                    Value="30" />
            <Setter Property="Margin"
                    Value="-25,-15,0,0" />
            <Setter Property="visuals:ShiftBehavior.Position"
                    Value="{Binding Path=Position, 
                                    Converter={x:Static visuals:ToVisualPointConverter.TheInstance}, 
                                    Mode=OneWay}" />
        </Style>
    </ItemsControl.ItemContainerStyle>
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <!-- Set item panel to grid (we want a container that takes the whole space 
                 of the items control-->
            <Grid />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>  

Note the DropBehavior's attached properties:

C#
<!--The drop container is set to the whole GlyphsItemsControl.-->
visuals:DropBehavior.ContainerElement="{Binding RelativeSource={RelativeSource Mode=Self}}"
C#
<!--The <code>DraggedElement</code> is obtained from the drag container.-->
visuals:DropBehavior.DraggedElement="{Binding Path=(visuals:DragBehavior.CurrentlyDraggedElement), 
                                              ElementName=TopLevelPanel}"  
C#
<!--The <code>DropBehavior.TheDropOperation</code> 
is attached to the drop container to be invoked when
a dragged item is released into the drop container.-->
visuals:DropBehavior.TheDropOperation="{x:Static local:DropIntoGlyphPanelOperation.Instance}"

Now, take a look at ItemContainerStyle - the style for each glyph item. The interesting line here is:

C#
<Setter Property="visuals:ShiftBehavior.Position"
        Value="{Binding Path=Position, 
                        Converter={x:Static visuals:ToVisualPointConverter.TheInstance}, 
                        Mode=OneWay}" />

This line sets the position of the glyph to the Position property of the NumberVmWithPosition View Model object via ShiftBehavior described above.

List to List Drag and Drop Sample

Our last sample demonstrates how to drag an item from source list to target list. You can choose between which two items of the target list the dropped item will be added. There is a Drop Cue to indicate where the dragged item is added if released at the current mouse position.

This sample is located under NP.Tests.DragDropFromListToList solution.

If you run it, you'll see the list of numbers on the left (same as in the previous sample) and empty space on the right:

Image 5

You can drag and drop items from the left panel to the right panel:

Image 6

When you drag an item and your mouse is above the target list, the red line between the target items will indicate where the dropped item is placed if released at this mouse position (as shown in the image above). This red line represents the Drop Cue.

The code for the sample is very similar to that of the previous sample. NumberVm is exactly as above. There is no need for NumberVmWithPosition - the position of an item within the target list is determined by its order. Here is how TestVm looks:

C#
public class TestVm 
{
    public ObservableCollection<numbervm> SourceList { get; }
    public ObservableCollection<numbervm> TargetList { get; }

    public TestVm()
    {
        SourceList = new ObservableCollection<numbervm>();

        SourceList.Add(new NumberVm(1, "One"));
        SourceList.Add(new NumberVm(2, "Two"));
        SourceList.Add(new NumberVm(3, "Three"));
        SourceList.Add(new NumberVm(4, "Four"));
        SourceList.Add(new NumberVm(5, "Five"));

        TargetList = new ObservableCollection<numbervm>();
    }
} 

Now, let us take a look at the XAML code in MainWindow.xaml file. The Drag Cue and the drag source list are exactly the same as in the previous sample. Here is the code for the target area (including the target list):

XML
<!--Drop Target-->
<Grid Grid.Column="2">
    <Rectangle x:Name="DropCue"
               HorizontalAlignment="Stretch"
               VerticalAlignment="Top"
               Height="2"
               Margin="0,-1"
               Fill="Red"
               Visibility="{Binding Path=(visuals:DropBehavior.CanDrop), 
                                    Converter={x:Static visuals:BoolToVisConverter.TheInstance},
                                    ElementName=DropTargetList}">
        <Rectangle.RenderTransform>
            <TranslateTransform Y="{Binding Path=(visuals:DropBehavior.DropPosition).Y, 
                                               ElementName=DropTargetList}" />
        </Rectangle.RenderTransform>
    </Rectangle>
    <ItemsControl x:Name="DropTargetList"
                  HorizontalAlignment="Stretch"
                  ItemsSource="{Binding Path=TargetList}"
                  visuals:DropBehavior.ContainerElement=
                  "{Binding RelativeSource={RelativeSource Mode=Self}}"
                  visuals:DropBehavior.DraggedElement=
                  "{Binding Path=(visuals:DragBehavior.CurrentlyDraggedElement), 
                                                                ElementName=TopLevelPanel}"
                  visuals:DropBehavior.TheDropOperation=
                  "{x:Static local:DropIntoItemsPanelOperation.Instance}"
                  visuals:DropBehavior.TheDropPositionChooser=
                  "{x:Static local:VerticalItemsPanelPositionChooser.Instance}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel Margin="0,2" />
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <visuals:LabelContainer Background="LightGreen"
                                        TheLabel="{Binding NumStr}"
                                        Margin="0,2" />
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Grid>  

Note, that the target area is a Grid that contains the DropCue rectangle and DropTargetList ItemsControl. The visibility of the DropCue is controlled by the following line:

C#
Visibility="{Binding Path=(visuals:DropBehavior.CanDrop), 
                    Converter={x:Static visuals:BoolToVisConverter.TheInstance},
                    ElementName=DropTargetList}"    

It becomes visible when DropBehavior.CanDrop attached property is true on the drop target.

The vertical location of the Drop Cue is controlled by TranslateTransform:

C#
<Rectangle.RenderTransform>
    <TranslateTransform Y="{Binding Path=(visuals:DropBehavior.DropPosition).Y, 
                                       ElementName=DropTargetList}" />
</Rectangle.RenderTransform>  

The important lines from the DropTargetList items control are:

C#
visuals:DropBehavior.TheDropOperation="{x:Static local:DropIntoItemsPanelOperation.Instance}"
visuals:DropBehavior.TheDropPositionChooser=
"{x:Static local:VerticalItemsPanelPositionChooser.Instance}"

The first of them binds DropBehavior.TheDropOperation attached property to an object of type DropIntoItemsPanelOperation that chooses what to do when drop happens. The second line binds DropBehavior.TheDropPositionChooser to the object that chooses the position of the drop cue.

To figure out the vertical position and index of the possible insertion during the drop, I employ:

C#
public static class VerticalPositionHelper
{
    public static (double, int) GetVerticalOffsetAndInsertIdx
                  (this FrameworkElement dropContainer, double mouseVerticalOffset)
   {
      ...
   }
}

method. The method is a bit convoluted and does not have direct relationship to what we discuss here, so I leave it for the readers to figure out its implementation (if they really want it). This method accepts the drop container and the current mouse vertical offset within the container and it returns the vertical position for the Drop Cue and the insertion index for the Dropped item in case the drop occurs at this point.

There are two classes mentioned above that use this method: VerticalItemsPanelPositionChooser (used for figuring out the position of the Drop Cue) and DropIntoItemsPanelOperation that actually performs the drop, when the dragged item is released into the target. Here is the documented code for both classes:

C#
public class VerticalItemsPanelPositionChooser : IDropPositionChooser
{
    // used for easy XAML reference
    public static VerticalItemsPanelPositionChooser Instance { get; } = 
        new VerticalItemsPanelPositionChooser();

    public Point GetPositionWithinDropDontainer
    (
        FrameworkElement droppedElement, 
        FrameworkElement dropContainer, 
        Point mousePositionWithRespectToContainer)
    {
        // get the vertical offset
        (double verticalOffset, _) = dropContainer.GetVerticalOffsetAndInsertIdx(mousePositionWithRespectToContainer.Y);

        // return the position corresponding to the vertical offset.
        return new Point(0, verticalOffset);
    }
}    

and:

C#
public class DropIntoItemsPanelOperation : IDropOperation
{
    // used for easy XAML reference
    public static DropIntoItemsPanelOperation Instance { get; } = 
        new DropIntoItemsPanelOperation();

    public void Drop(FrameworkElement droppedElement, 
    FrameworkElement dropContainer, Point mousePositionWithRespectToContainer)
    {
        // get the view model for the items that's being dropped. 
        NumberVm droppedVm = droppedElement.DataContext as NumberVm;

        // get the view model into which we insert the dropped item.
        TestVm testVm = dropContainer.DataContext as TestVm;

        // get the insertion index
        (_, int insertIdx) = 
        dropContainer.GetVerticalOffsetAndInsertIdx(mousePositionWithRespectToContainer.Y);

        // insert the item into the target view model. 
        testVm.TargetList.Insert(insertIdx, droppedVm);
    }
}  

Conclusion

In this article, I described parts of the functionality of NP.Visuals package related to Drag and Drop. As you can see, one can do virtually any drag and drop using this functionality.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Nick Polyak
Architect AWebPros
United States United States
I am a software architect and a developer with great passion for new engineering solutions and finding and applying design patterns.

I am passionate about learning new ways of building software and sharing my knowledge with others.

I worked with many various languages including C#, Java and C++.

I have my Ph.D. from RPI.

here is my linkedin profile - I'll be happy to connect!

Comments and Discussions

 
QuestionCan't find source for NP.Visuals Pin
netizenk10-Sep-18 7:36
professionalnetizenk10-Sep-18 7:36 
AnswerRe: Can't find source for NP.Visuals Pin
Nick Polyak11-Sep-18 15:23
professionalNick Polyak11-Sep-18 15:23 
GeneralRe: Can't find source for NP.Visuals Pin
netizenk12-Sep-18 5:19
professionalnetizenk12-Sep-18 5:19 
GeneralRe: Can't find source for NP.Visuals Pin
Nick Polyak12-Sep-18 8:24
professionalNick Polyak12-Sep-18 8:24 
GeneralRe: Can't find source for NP.Visuals Pin
netizenk13-Sep-18 5:12
professionalnetizenk13-Sep-18 5:12 
GeneralRe: Can't find source for NP.Visuals Pin
Nick Polyak13-Sep-18 7:45
professionalNick Polyak13-Sep-18 7:45 
GeneralRe: Can't find source for NP.Visuals Pin
Nick Polyak12-Sep-18 11:17
professionalNick Polyak12-Sep-18 11:17 
AnswerRe: Can't find source for NP.Visuals Pin
Nick Polyak11-Sep-18 15:25
professionalNick Polyak11-Sep-18 15:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.