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:
NP.Utilities
- a library containing some basic non visual utilities and extension classes 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 art1
and art2
, 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:
NP.Visuals
NP.Concepts
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:
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:
<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.
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.
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.
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:
<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:
visuals:DragBehavior.StartBoundaryPoint="0, 0"
so that the circle code now looks like:
<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:
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:
<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:
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...
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:
<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:
Here is the code for the sample:
<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
:
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
:
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:
- 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.
- 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:
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:
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;
}
}
NumberVm
- a view model for an item in the list consists of an integer number and the string
representing that number as a word:
public class NumberVm
{
public int Number { get; }
public string NumStr { get; }
public NumberVm(int n, string str)
{
Number = n;
NumStr = str;
}
}
NumberVmWithPosition
- a view model for each item dropped into the glyph panel. It subclasses NumberVm
adding position to it: 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:
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:
public MainWindow()
{
InitializeComponent();
DataContext = new TestVm();
}
There is one more C# class DropIntoGlypPanelOperation
. This file defines the operation that happens during the drop:
public class DropIntoGlyphPanelOperation : IDropOperation
{
public void Drop
(
FrameworkElement draggedAndDroppedElement,
FrameworkElement dropContainer,
Point mousePositionWithRespectToContainer)
{
NumberVm droppedVm =
draggedAndDroppedElement.DataContext as NumberVm;
TestVm testVm =
dropContainer.DataContext as TestVm;
NumberVmWithPostion newGlyphVm = new NumberVmWithPostion
(
droppedVm.Number,
droppedVm.NumStr,
mousePositionWithRespectToContainer.ToPoint2D());
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:
<Grid x:Name="TopLevelPanel">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Popup x:Name="TheDragCue" .../>
<ItemsControl x:Name="DragSourceList" .../>
<Grid x:Name="Separator" .../>
<ItemsControl x:Name="GlyphsItemsControl" .../>
</Grid>
Here is the full code for Drag Cue popup:
<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:
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:
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
:
Width="{Binding Path=ActualWidth}"
Height="{Binding Path=ActualHeight}"
Also note the binding of its Opacity
property:
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):
<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
:
<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>
<Grid />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Note the DropBehavior
's attached properties:
<!--The drop container is set to the whole GlyphsItemsControl.-->
visuals:DropBehavior.ContainerElement="{Binding RelativeSource={RelativeSource Mode=Self}}"
<!--The <code>DraggedElement</code> is obtained from the drag container.-->
visuals:DropBehavior.DraggedElement="{Binding Path=(visuals:DragBehavior.CurrentlyDraggedElement),
ElementName=TopLevelPanel}"
<!--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:
<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:
You can drag and drop items from the left panel to the right panel:
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:
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):
<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:
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
:
<Rectangle.RenderTransform>
<TranslateTransform Y="{Binding Path=(visuals:DropBehavior.DropPosition).Y,
ElementName=DropTargetList}" />
</Rectangle.RenderTransform>
The important lines from the DropTargetList
items control are:
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:
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:
public class VerticalItemsPanelPositionChooser : IDropPositionChooser
{
public static VerticalItemsPanelPositionChooser Instance { get; } =
new VerticalItemsPanelPositionChooser();
public Point GetPositionWithinDropDontainer
(
FrameworkElement droppedElement,
FrameworkElement dropContainer,
Point mousePositionWithRespectToContainer)
{
(double verticalOffset, _) = dropContainer.GetVerticalOffsetAndInsertIdx(mousePositionWithRespectToContainer.Y);
return new Point(0, verticalOffset);
}
}
and:
public class DropIntoItemsPanelOperation : IDropOperation
{
public static DropIntoItemsPanelOperation Instance { get; } =
new DropIntoItemsPanelOperation();
public void Drop(FrameworkElement droppedElement,
FrameworkElement dropContainer, Point mousePositionWithRespectToContainer)
{
NumberVm droppedVm = droppedElement.DataContext as NumberVm;
TestVm testVm = dropContainer.DataContext as TestVm;
(_, int insertIdx) =
dropContainer.GetVerticalOffsetAndInsertIdx(mousePositionWithRespectToContainer.Y);
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.