Movable rows in WPF DataGrid
By Mirek on (tags: Attached property, DataGrid, WPF, categories: code)DataGrid control in WPF is a rich table structured control, which is perfect for displaying any kind of data in table like format. The one think about DataGrid it always lack, is a possibility to easily change the order of rows. It is not even about drag and drop of rows and items from and into the DataGrid , but simply rearrange existing rows in DataGrid control. It this post I will present you my solution for this problem, which additionally sticks to MvvM pattern perfectly.
There are many solutions for this problem in the internet., for instance here, here or here, but those all always involve a code behind implementation of this feature. There are two things about that that I couldn’t accept. One is that this is not in accordance with MvvM pattern which assumes no code behind. Ok, one could say this is strictly a “view” related code, but still, I don’t like it. The second thing, and the reason I write this post for, is fact that having this code in code behind files does not stick to DRY principle. We would need to copy it for every window or control where we want to have this feature implemented.
Saying that, I have decided to implement a movable rows in DataGrid as a attached dependency property, so I am able to attach it to every DataGrid control I want with just a one line of XAML.
Let’s start by creating a helper class VisualHelper and defining a attached dependency porperty in in, which will enable the MOvable rows on target DataGrid
public class VisualHelper
{
//EnableRowsMoveProperty is used to enable rows moving by mouse drag and move in data grid
//the only requirement is to ItemsSource collection of datagrid be a ObservableCollection or at least IList collection
public static readonly DependencyProperty EnableRowsMoveProperty =
DependencyProperty.RegisterAttached("EnableRowsMove", typeof(bool), typeof(VisualHelper), new PropertyMetadata(false, EnableRowsMoveChanged));
public static bool GetEnableRowsMove(DataGrid obj)
{
return (bool)obj.GetValue(EnableRowsMoveProperty);
}
public static void SetEnableRowsMove(DataGrid obj, bool value)
{
obj.SetValue(EnableRowsMoveProperty, value);
}
}
Next thing is to implement the EnableRowsMoveChange methos which will be fired every time the attached property EnableRowsMove is changed. Inside this method we need to register three mouse events on our DataGrid control.
private static void EnableRowsMoveChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var grid = (d as DataGrid);
if (grid == null) return;
if ((bool)e.NewValue)
{
grid.PreviewMouseLeftButtonDown += OnMouseLeftButtonDown;
grid.PreviewMouseLeftButtonUp += OnMouseLeftButtonUp;
grid.PreviewMouseMove += OnMouseMove;
}
else
{
grid.PreviewMouseLeftButtonDown -= OnMouseLeftButtonDown;
grid.PreviewMouseLeftButtonUp -= OnMouseLeftButtonUp;
grid.PreviewMouseMove -= OnMouseMove;
}
}
I think, it is quite trivial, we only assign those events to controls which are a DataGrid. Now there is one tricky thing in my implementation. When we move row with mouse move, we need to somehow remember the source item, we started dragging, so at the end, when we release mouse button, we know which item to replace with final row. We cannot use a static property or field on VisualHelper class, because it could cause interference if we have more than one DataGrid with movablr rows. So we can do it with attached property as well, but we will mark them as private, so it can be used only inside a VisualHelper class.
//Private DraggedItemProperty attached property used only for EnableRowsMoveProperty
private static readonly DependencyProperty DraggedItemProperty =
DependencyProperty.RegisterAttached("DraggedItem", typeof(object), typeof(VisualHelper), new PropertyMetadata(null));
private static object GetDraggedItem(DependencyObject obj)
{
return (object)obj.GetValue(DraggedItemProperty);
}
private static void SetDraggedItem(DependencyObject obj, object value)
{
obj.SetValue(DraggedItemProperty, value);
}
As you can see we marked the attached property as well as property getter and setter as private, so there is no access to this property from XAML.
Now we can implement the rest of mouse events.
private static void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
//find datagrid row by mouse point position
var row = TryFindFromPoint<DataGridRow>((UIElement)sender, e.GetPosition((sender as DataGrid)));
if (row == null || row.IsEditing) return;
VisualHelper.SetDraggedItem(sender as DataGrid, row.Item);
}
private static void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
var draggeditem = VisualHelper.GetDraggedItem(sender as DependencyObject);
if (draggeditem == null) return;
ExchangeItems(sender, (sender as DataGrid).SelectedItem);
//select the dropped item
(sender as DataGrid).SelectedItem = draggeditem;
//reset
VisualHelper.SetDraggedItem(sender as DataGrid, null);
}
private static void OnMouseMove(object sender, MouseEventArgs e)
{
var draggeditem = VisualHelper.GetDraggedItem(sender as DependencyObject);
if (draggeditem == null) return;
var row = TryFindFromPoint<DataGridRow>((UIElement)sender, e.GetPosition((sender as DataGrid)));
if (row == null || row.IsEditing) return;
ExchangeItems(sender, row.Item);
}
private static void ExchangeItems(object sender, object targetItem)
{
var draggeditem = VisualHelper.GetDraggedItem(sender as DependencyObject);
if (draggeditem == null) return;
if (targetItem != null && !ReferenceEquals(draggeditem, targetItem))
{
var list = (sender as DataGrid).ItemsSource as IList;
if (list == null)
throw new ApplicationException("EnableRowsMoveProperty requires the ItemsSource property of DataGrid to be at least IList inherited collection. Use ObservableCollection to have movements reflected in UI.");
//get target index
var targetIndex = list.IndexOf(targetItem);
//remove the source from the list
list.Remove(draggeditem);
//move source at the target's location
list.Insert(targetIndex, draggeditem);
}
}
As shown above we also exchange source and target item on mouse move over the DataGrid. This gives a pretty effect of moving rows up and down while we are moving mouse cursor. One important thing here is the requirement we set on the DataGrid to be able to service the movable rows. This is the ItemsSource property to be set to at least IList implemented collection or even better to an ObservableCollection. This is because we not actually moving the DataGrid rows but we are moving items on underlying collection, and to have it possible we need it to implement at least IList interface. But to have changes in rows also visible on the grid we need to bind it to a ObservableCollection or other collection which implement INotifyCollectionChanged basically.
Having that in place, all we need is to decorate our DataGrid control in xaml
<DataGrid ItemsSource="{Binding People}"
SelectionMode="Single"
local:VisualHelper.EnableRowsMove="True" />
Full solution for this post is available in attached file.
John Herman
11/1/2016 5:20 PM
Thanks for the example. It is a good demonstration, however the movable row functionality stops working after selection of a column heading.John Herman
11/1/2016 5:39 PM
Appears to be an overlap of the OnMouseMove and the OnMouseLeftButtonUP methods.bennett
10/20/2017 6:47 PM
thank you for the example and thorough explanation writeup! very helpful! as this post is a couple years old, do you have any new thoughts?Ahmed Dawod
2/14/2018 3:01 PM
Thanks man that was very helpfulStarwer
10/3/2018 11:36 AM
Wonderful tutorial on how to implement a proper attached property on a really useful use-case. Thanks a lot for this helpful post !Carmen Mate
2/5/2019 8:26 PM
Thank you so much. It was really helpful.skidaddytn
8/19/2020 12:01 AM
Thanks so much for sharing this! This thing is pretty much plug n play... You just saved me a ton of time.Oliver
7/7/2022 10:19 AM
Nice approach, but only a bug. If the user sorts a column in the presented example, the solution doesn’t work anymore.