It took me some time to find a solution to this problem, but here is a working solution.
I wanted to be able to drag 'n drop WPF DataGrid rows in MVVM, and although this solution is not strickly MVVM, I still think that it fits nicely in that design patten. All GUI it handled within the view, and all logic in the ViewModel.
I found a good starting point for this solution somewhere, but again unfortunately I have forgotten where. I think it was on codeproject. My sincere apologies to the author.
The follwing goes into the UserControl code behind:
using Baltz.ViewModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Baltz.View
{
/// <summary>
/// Interaction logic for DataGridDraggableRowView.xaml
/// </summary>
public partial class DataGridDraggableRowView : UserControl
{
public DataGridDraggableRowView()
{
InitializeComponent();
}
#region Static Methods
private static T FindVisualParent<T>(UIElement element) where T : UIElement
{
var parent = element;
while (parent != null)
{
var correctlyTyped = parent as T;
if (correctlyTyped != null)
{
return correctlyTyped;
}
parent = VisualTreeHelper.GetParent(parent) as UIElement;
}
return null;
}
#endregion
#region Fields
private int _originalIndex;
private DataGridRow _oldRow;
private DataGridDraggableRowItem _targetItem;
private DataGridDraggableRowViewModel _viewModel;
#endregion Fields
#region Event Handlers
/// <summary>
/// Updates the grid as a drag progresses
/// </summary>
private void OnMainGridCheckDropTarget(object sender, DragEventArgs e)
{
var row = FindVisualParent<DataGridRow>(e.OriginalSource as UIElement);
// Set the DragDropEffects
if ((row == null) || !(row.Item is DataGridDraggableRowItem))
{
e.Effects = DragDropEffects.None;
}
else
{
var currentIndex = row.GetIndex();
// Erase old drop-line
if (_oldRow != null)
_oldRow.BorderThickness = new Thickness(0);
// Draw new drop-line
var direction = (currentIndex - _originalIndex);
if (direction < 0)
row.BorderThickness = new Thickness(0, 2, 0, 0);
else if (direction > 0)
row.BorderThickness = new Thickness(0, 0, 0, 2);
row.BorderBrush = new SolidColorBrush(Color.FromArgb(0xFF, 0x00, 0x9C, 0xDD));
// Reset old row
_oldRow = row;
}
}
/// <summary>
/// Gets the view model from the data Context and assigns it to a member variable.
/// </summary>
private void OnMainGridDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
_viewModel = (DataGridDraggableRowViewModel)this.DataContext;
}
/// <summary>
/// Process a row drop on the DataGrid.
/// </summary>
private void OnMainGridDrop(object sender, DragEventArgs e)
{
e.Effects = DragDropEffects.None;
e.Handled = true;
// Verify that this is a valid drop and then store the drop target
var row = FindVisualParent<DataGridRow>(e.OriginalSource as UIElement);
if (row != null)
{
_targetItem = row.Item as DataGridDraggableRowItem;
if (_targetItem != null)
{
e.Effects = DragDropEffects.Move;
}
}
// Erase last drop-line
if (_oldRow != null)
_oldRow.BorderThickness = new Thickness(0, 0, 0, 0);
}
/// <summary>
/// Processes a drag in the main grid.
/// </summary>
private void OnMainGridMouseMove(object sender, MouseEventArgs e)
{
// Exit if left mouse button aren't pressed
if (e.LeftButton != MouseButtonState.Pressed)
return;
// Find the row the mouse button was pressed on
var row = FindVisualParent<DataGridRow>(e.OriginalSource as FrameworkElement);
_originalIndex = row.GetIndex();
// If the row was already selected, begin drag
if ((row != null) && row.IsSelected)
{
// Get the grocery item represented by the selected row
var selectedItem = (DataGridDraggableRowItem)row.Item;
var finalDropEffect = DragDrop.DoDragDrop(row, selectedItem, DragDropEffects.Move);
if ((finalDropEffect == DragDropEffects.Move) && (_targetItem != null))
{
/* A drop was accepted. Determine the index of the item being
* dragged and the drop location. If they are different, then
* move the selectedItem to the new location. */
// Move the dragged item to its drop position
var oldIndex = _viewModel.Rows.IndexOf(selectedItem);
var newIndex = _viewModel.Rows.IndexOf(_targetItem);
if (oldIndex != newIndex)
_viewModel.Rows.Move(oldIndex, newIndex);
_targetItem = null;
}
}
}
#endregion
}
}
A side note: This style is based on a style posted in another post
<ResourceDictionary x:Class="Ellab.Main.Settings.SettingsWindow.View.Styles.DraggableDataGridStyle"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="DataGridStyle.xaml"/>
</ResourceDictionary.MergedDictionaries>
<Style x:Key="DefaultDraggableDataGrid" BasedOn="{StaticResource DefaultSettingsDataGrid}" TargetType="{x:Type DataGrid}">
<Setter Property="HeadersVisibility" Value="Column"/>
<Setter Property="AllowDrop" Value="True"/>
</Style>
<Style TargetType="{x:Type DataGridCell}">
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="#FF009CDD" />
<SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="#FF009CDD" />
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Black" />
<SolidColorBrush x:Key="{x:Static SystemColors.ControlTextBrushKey}" Color="Black" />
</Style.Resources>
</Style>
</ResourceDictionary>
And this is the complete View - most of it isn't relevant for the dragable rows.
<UserControl x:Class="foo.View.barDataGridDraggableRowView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Styles/DraggableDataGridStyle.xaml"/>
<ResourceDictionary Source="Styles/ContentPresenterStyle.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel Orientation="Vertical" Background="Transparent">
<ContentPresenter Content="{Binding Path=HeaderButton}" Style="{StaticResource DefaultContentPresenter}"/>
<StackPanel Orientation="Vertical" Background="Transparent" Visibility="{Binding Path=IsExpanded}">
<DataGrid ItemsSource="{Binding Path=Rows}"
MouseMove="OnMainGridMouseMove"
DragEnter="OnMainGridCheckDropTarget"
DragLeave="OnMainGridCheckDropTarget"
DragOver="OnMainGridCheckDropTarget"
Drop="OnMainGridDrop"
DataContextChanged="OnMainGridDataContextChanged"
Style="{StaticResource DefaultDraggableDataGrid}">
<DataGrid.Columns>
<DataGridTemplateColumn IsReadOnly="True" Width="20">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Image Source="../../Resources/DragDrop.png" Stretch="Fill" Opacity="{Binding Opacity}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn IsReadOnly="True">
<DataGridTemplateColumn.Header>
<TextBlock Text="{Binding DataContext.EnabledColumnHeader, RelativeSource={RelativeSource AncestorType=DataGrid}}" MaxWidth="65" TextWrapping="WrapWithOverflow" TextTrimming="CharacterEllipsis">
<TextBlock.LayoutTransform>
<TransformGroup>
<RotateTransform Angle="90" />
<ScaleTransform ScaleX="-1" ScaleY="-1"/>
</TransformGroup>
</TextBlock.LayoutTransform>
</TextBlock>
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox HorizontalAlignment="Center" VerticalAlignment="Center" IsChecked="{Binding EnabledChecked, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Opacity="{Binding Opacity}" IsEnabled="{Binding Enable}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn IsReadOnly="True">
<DataGridTemplateColumn.Header>
<TextBlock Text="{Binding DataContext.WizardColumnHeader, RelativeSource={RelativeSource AncestorType=DataGrid}}" MaxWidth="65" TextWrapping="WrapWithOverflow" TextTrimming="CharacterEllipsis">
<TextBlock.LayoutTransform>
<TransformGroup>
<RotateTransform Angle="90" />
<ScaleTransform ScaleX="-1" ScaleY="-1"/>
</TransformGroup>
</TextBlock.LayoutTransform>
</TextBlock>
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox HorizontalAlignment="Center" VerticalAlignment="Center" IsChecked="{Binding WizardChecked, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Opacity="{Binding Opacity}" IsEnabled="{Binding Enable}" Visibility="{Binding WizardVisible}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn IsReadOnly="True" Width="*">
<DataGridTemplateColumn.Header>
<TextBlock Text="{Binding DataContext.TextColumnHeader, RelativeSource={RelativeSource AncestorType=DataGrid}}" TextWrapping="WrapWithOverflow" TextTrimming="CharacterEllipsis" VerticalAlignment="Bottom"/>
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" HorizontalAlignment="Left" VerticalAlignment="Center" Opacity="{Binding Opacity}" IsEnabled="{Binding Enable}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn IsReadOnly="True" Width="40">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Image Source="{Binding TileImage}" Opacity="{Binding Opacity}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<ContentPresenter Content="{Binding Path=TilesRemainingText}"/>
</StackPanel>
</StackPanel>
</UserControl>
As always, feel free to comment, or ask.