Introduction
In this tip, I would like to share my experience on creating ArrangePanel
, that allows users to reorder items by dragging them around, supports different layout strategies, fully animated and supports MVVM.
Background
When I started with implementation of reordering functionality, I faced the issue with separation of view and view model. The matter is that View doesn't know anything about View Model and at the same time View Model doesn't know anything about View. Under such conditions, it is hard to notify View Model about changes in items order and not to expose one side to another. And the fact that I expect this panel to be used as layout control in items controls makes reordering synchronization an even harder task.
After thinking a little, I've come up with a small trick: instead of changing children order in child collection, I've introduced an attached property called Order
that represents item position in the list. So when user grabs an element and moves it, ArrangePanel
will only change Order
property of elements that changed their positions.
Another trick is DesiredPosition
and Position
attached properties. These properties define where panel's child elements will be placed. Both properties are of Rect
type, and both define where particular child property should be displayed. Position
property controls actual child position inside the panel, while DesiredPosition
reflects position that child is going to take. Each time when DesiredPosition
changed - it creates a RectAnimation
for Position
property that will animate child movement from its current position to DesiredPosition
. Animation duration depends on distance between these properties.
Using the Code
ArrangePanel
can be used just like plain StackPanel
or WrapPanel
. It can be used as ItemsPanelTemplate
in items controls and as stand alone layout panel. Using it allows items reordering by using drag and drop with full animation.
MainWindow.xaml file of demo project contains Grid
with three columns. Left and central columns demonstrate ArrangePanel
usage as ItemsPanelTemplate
and right panel contains it with predefined set of child elements.
Left and central lists are bound to the same data source so that when items from one list are reordered, it is immediately reflected in the other list.
To achieve this functionality, Lancer1WPF:ArrangePanel.Order
property is attached to ListBoxItem
is bound to the Order
property of DataItem
.
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Lancer1WPF:ArrangePanel.Order" Value="{Binding Path=Order, Mode=TwoWay}" />
</Style>
</ListBox.ItemContainerStyle>
Right now, only TableLayoutStrategy
is implemented as is working as default layout for the panel, but it is fairly easy to add new layout strategies.
ArrangePanel
ArrangePanel
does not contain any layout logic in it, instead it is delegated to layout strategy. Layout strategy interface is defined as:
public interface ILayoutStrategy
{
Size ResultSize { get; }
void Calculate(Size availableSize, Size[] sizes);
Rect GetPosition(int index);
int GetIndex(Point position);
}
Layout has three responsibilities:
- To determine position of the element. To accomplish this,
Calculate
and GetPosition
methods are used. Calculate
method receives the array of children sizes, and available size of the panel where all elements should be placed. While GetPosition
returns position of each element. - To determine child index by given position.
GetIndex
is used in reordering functionality to determine the place where to put dragged element. - To determine resulting panel size.
ArrangePanel
itself could be divided into two main parts: reordering functionality and arrangement functionality.
StartReordering
, DoReordering
and StopReordering
methods perform all the job. StartReordering
brings dragging element to front by setting ZIndex
property. This value will be cleared in StopReordering
method. DoReordering
is called from mouse move handler and its main responsibility to determine new order index of dragged element.
As ArrangePanel
uses attached property Order
to store child's index order, it will always look after Order
's values and it will assign sequential numbers for each child starting from 0
value for first child. It is done in InitializeEmptyOrder
and ReorderOthers
methods.