For my upcoming NetDrives tool (will be released shortly) I wanted to enable the user to reorder managed network shares using drag and drop using a preview of the dragged row:
As it turned out, it’s not too hard to implement, but it took my a while to find all pieces of the puzzle, so I compiled a short sample. You can find the sample link at the end of the article.
Drag Indicator Popup
I used a popup as an drag indicator, which I bound to the item that was currently dragged (DraggedItem dependency property):
<!-- the popup that is displayed if user moves rows -->
<Popup
x:Name="popup1"
IsHitTestVisible="False"
Placement="RelativePoint"
PlacementTarget="{Binding ElementName=me}"
AllowsTransparency="True">
<Border
BorderBrush="{DynamicResource CellBorderBrush}"
BorderThickness="2"
Background="White"
Opacity="0.75">
<StackPanel
Orientation="Horizontal"
Margin="4,3,8,3">
<Image
Source="/Shared/Images/DragInsert.png"
Width="16"
Height="16" />
<TextBlock
Style="{DynamicResource DefaultLabel}"
FontWeight="Bold"
VerticalAlignment="Center"
Text="{Binding ElementName=me, Path=DraggedItem.Name}"
Margin="8,0,0,0" />
</StackPanel>
</Border>
</Popup>
Disabling Drag and Drop in Edit Mode
I didn’t want to enable drag and drop if the grid was in edit mode. Accordingly, I registered two event listeners on the grid:
<dg:DataGrid
BeginningEdit="OnBeginEdit"
CellEditEnding="OnEndEdit"
.. />
The corresponding event listeners just set the IsEditing flag, which is evaluated when handling mouse events:
/// <summary>
/// State flag which indicates whether the grid is in edit
/// mode or not.
/// </summary>
public bool IsEditing { get; set; }
private void OnBeginEdit(object sender, DataGridBeginningEditEventArgs e)
{
IsEditing = true;
//in case we are in the middle of a drag/drop operation, cancel it...
if (IsDragging) ResetDragDrop();
}
private void OnEndEdit(object sender, DataGridCellEditEndingEventArgs e)
{
IsEditing = false;
}
Listening to Mouse Events
In order to display and move the popup with the mouse, I registered listeners for the following three mouse events:
- PreviewMouseLeftButtonDown (on the datagrid)
- MouseLeftButtonUp (directly on the layout root)
- MouseMove (directly on the layout root)
Note: I started with listeners on the grid only, which caused some side effects. Apparently, the datagrid (current March release) not always fires the mouse events properly. This caused choppy animations when hovering over certain cells. Fortunately, this is not an issue with the MouseMove event of the layout root.
Starting Drag and Drop
DnD is started as soon as the user presses the left mouse button on the datagrid. I had to use the PreviewLeftMouseButton event in order to get the notification, and I needed to determine the clicked row based on the mouse position. I blogged about finding an element under the mouse a while ago here, but the UIHelpers class is part of the sample project here.
My mouse button event listener basically does the following:
- Check if the mouse is being placed over a grid row.
- Set the IsDragging flag to true.
- Store the dragged item in the DraggedItem dependency property (used by the popup to display the name).
private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
//exit if in edit mode
if (IsEditing) return;
//find the clicked row
var row = UIHelpers.TryFindFromPoint<DataGridRow>((UIElement) sender,
e.GetPosition(shareGrid));
if (row == null) return;
//set flag that indicates we're capturing mouse movements
IsDragging = true;
DraggedItem = (IShareConfiguration) row.Item;
}
Moving the Popup
I registered a listener for the MouseMove event directly on the layout root (not on the datagrid). Basically, the event listener just moves the popup to the current mouse location along with a few minor tasks:
- If the popup has not been opened yet, display it.
- Set the grid to read-only.
- Reposition the popup by setting the PlacementRectangle property.
- Make sure the grid row under the mouse is being selected. Once again, this didn’t work reliably if I relied on the datagrid to do it by itself.
/// <summary>
/// Completes a drag/drop operation.
/// </summary>
private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (!IsDragging || IsEditing)
{
return;
}
//get the target item
ShareConfiguration targetItem = (ShareConfiguration) shareGrid.SelectedItem;
if (targetItem == null || !ReferenceEquals(DraggedItem, targetItem))
{
//remove the source from the list
ShareList.Remove(DraggedItem);
//get target index
var targetIndex = ShareList.IndexOf(targetItem);
//move source at the target's location
ShareList.Insert(targetIndex, DraggedItem);
//select the dropped item
shareGrid.SelectedItem = DraggedItem;
}
//reset
ResetDragDrop();
}
Finishing Drag and Drop
Once the user releases the mouse button, I need to perform the actual drop operation. I already had the dragged item (DraggedItem property, was set when the operation started) so all I needed was the drop target. My target is the currently selected row.
/// <summary>
/// Completes a drag/drop operation.
/// </summary>
private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (!IsDragging || IsEditing || shareGrid.SelectedItem == null)
{
return;
}
//get the target item
IShareConfiguration targetItem = (IShareConfiguration)shareGrid.SelectedItem;
if (!ReferenceEquals(DraggedItem, targetItem))
{
//the actual business logic that works on source and target
}
//reset
ResetDragDrop();
}
You might also want to check whether the mouse is currently over the grid (or a grid row) or not – in that case just use the TryFindFromPoint method from the UIHelpers class.
Cleaning Up
The ResetDragDrop method just performs a cleanup of the code by closing the popup and adjusting a few properties:
/// <summary>
/// Closes the popup and resets the
/// grid to read-enabled mode.
/// </summary>
private void ResetDragDrop()
{
IsDragging = false;
popup1.IsOpen = false;
shareGrid.IsReadOnly = false;
}
Download Sample: http://blog.hardcodet.net/wp-content/uploads/2009/03/datagrid_dragdrop.zip