ReordereableListBox

UPDATE : I’ve rewritten the sample using the RTM of WPF it’s available at http://www.quarrie.net/WPFSamples/02ReorderableListBox.zip

This sample shows how to build a custom ListBox control where you can reorder the contents by dragging the items around.  I have based this on Marcelo’s drag/drop adorner but don’t actually make use of the DragDrop class.

First off we create the custom control class and add the needed event handlers

public class ReorderableListBox : ListBox
{
protected override void OnInitialized(EventArgs e)
   {
base.OnInitialized(e);
this.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(OnPreviewMouseDown);

this.PreviewMouseLeftButtonUp += new MouseButtonEventHandler(OnPreviewMouseUp);

this.SelectionChanged += new SelectionChangedEventHandler(OnSelectionChanged);

this.PreviewMouseMove += newMouseEventHandler(OnPreviewMouseMove);

   }
}

The idea here is that we use the PreviewMouseLeftButtonDown and PreviewMouseLeftButtonUp events to decide when we are dragging and when we have dropped, the PreviewMouseMove event to move the adorner layer, and the SelectionChanged event to track the item that’s moving and the position we are going to move it to.

To manage the different states we need to declare the following

// The point at which to start drawing the adorner layer

Point dragStartPoint;
// Are we dragging?

bool dragging;
// Have we selected the item we want to move?

bool dragItemSelected;
// The AdornerLayer to draw the item we are dragging

AdornerLayer adornerLayer;
// The Marcelo’s DropPreviewAdorner

DropPreviewAdorner overlayElement;

And the following public property and backing variable so we can access the original item from where where we are declared

private int originalItemIndex;
public int OriginalItemIndex
{
   get { return originalItemIndex; }
set { originalItemIndex = value; }
}

This is all fairly simple logic but for completeness, when the PreviewMouseLeftButtonDown event is fired we set dragStartPoint and dragging.

void OnPreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
   dragStartPoint = e.GetPosition(this); 
dragging = true;
}

This will be followed by the  SelectionChanged event being fired where we check for both dragging and !dragItemSelected.  If this is the case then we set dragItemSelected to true, and originalItemIndex to SelectedIndex.  Now that we know what item we want to move, we can set up the AdornerLayer.   Marcelo’s DragDropAdornerLayer takes the adornedElement and the adorningElement.  In this case the listbox itself and the item we want to move.  We create a new ContentPresenter and set the Content to be the SelectedItem and the ContentTemplate to be the ItemTemplate of the listbox, assigning it to overlayElement.  Finally we add the new DropPreviewAdorner to the AdornerLayer.

void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
   if (dragging && !dragItemSelected)
   {
this.dragItemSelected = true;
this.originalItemIndex = this.SelectedIndex;
ContentPresenter presenter = new ContentPresenter();
      presenter.Content = this.SelectedItem;
      presenter.ContentTemplate = this.ItemTemplate;
this.overlayElement = new DropPreviewAdorner((UIElement)this, presenter);
this.AdornerLayer.Add(overlayElement);
  }
}

The OnPreviewMouseMove event is quite simple too, we just set the LeftOffset and TopOffset for overlayElement based on the new mouseposition.

void ReorderableListBox_PreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
   if (e.LeftButton == MouseButtonState.Pressed && dragging)
   {
if (overlayElement != null)
      {
Point currentPosition = (Point)e.GetPosition((IInputElement)this);
         overlayElement.LeftOffset = currentPosition.X;
         overlayElement.TopOffset = currentPosition.Y;
      }
   }       
}   

Finally we need to handle the reordering, since this will be dependent on if we are databound or not, we can defined a routedEvent called ItemsReorderedEvent thus leaving the reordering logic up to the controls host.

public static readonly RoutedEvent ItemsReorderedEvent;
public event RoutedEventHandler ItemsReordered
{
   add { base.AddHandler(ItemsReorderedEvent, value); }
   remove { base.RemoveHandler(ItemsReorderedEvent, value); }
}

static ReorderableListBox()
{
    ItemsReorderedEvent = EventManager.RegisterRoutedEvent(“ItemsReordered”,
    RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ReorderableListBox));
}

We raise this event in the OnPreviewMouseLeftButtonUp event handler, after resetting dragging and dragItemSelected back to their original values, and removingthe adornerLayer.

void OnPreviewMouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
   dragging = false;
   dragItemSelected = false;
   adornerLayer.Remove(overlayElement);
RoutedEventArgs routedEventArgs;
   routedEventArgs = new RoutedEventArgs(ItemsReorderedEvent, this);
base.RaiseEvent(routedEventArgs);
}

Since my listbox is databound, When we consume the ItemsReordered event, can cast the ItemsSource to an ObservableCollection of our bound type and use Move to do the reorder.

private void OnItemsReordered(object sernder, RoutedEventArgs e)
{
   ((ObservableCollection<CourseMark>)reorderableListBox.ItemsSource).Move(reorderableListBox.OriginalItemIndex, reorderableListBox.SelectedIndex);

}

Technorati tags: , , , , ,

Posted in WPF. 21 Comments »

21 Responses to “ReordereableListBox”

  1. Jim Says:

    Do you have the complete project downloadable somewhere??

  2. Simon Middlemiss Says:

    Not for RTM of WPF. I’ll try and recreate it for you, watch this space.

  3. Boris Says:

    Hi Simon,
    i luckyly found your nice listbox sample. Thanks for sharing that. One thing though, i am not able to solve, is that right now the “OnItemsReordered” event gets fired every time i just selects an item with the mouse. How can i distinguish between dragging operation by the user and simple selection?

    regards Boris

  4. Wade Says:

    Hi Simon,

    I am confused with the following command

    ((ObservableCollection)reorderableListBox.ItemsSource).Move(reorderableListBox.OriginalItemIndex, reorderableListBox.SelectedIndex);

    Could you tell me how to and where to define . I did not find from the demo coding.

    Thanks

  5. Wade Says:

    Could you tell me how to and where to define CourseMark ?

  6. furryfren Says:

    Hi Simon,

    Thanks for this sample. I’m able to drag items around on the playbox but when dropping items, nothing happens. Any ideas?

  7. furryfren Says:

    Hi Simon,

    Thanks for this sample. I’m able to drag items around on the listbox* but when dropping items, nothing happens. Any ideas?

  8. Re-order listBox in C# with drag and drop - Global Point Forum Says:

    […] row reordering. Free source code and programming help Even for WPF you can take a look at this: ReordereableListBox PsiSpace by Simon Middlemiss I think there’s quite enought resources to get you back on track. […]

  9. Qcontinuum Says:

    Your sample runs after removing the file path to SampleSourceData.xml. I was expecting to be able to drop the item anywhere in the list and have it moved to that location. Could you please give us a sample that goes that far? Thanks in any case for the current sample.

  10. Itay Says:

    found an easier approach to do the same.
    I have a Window that is bounded to a collection of keywords (i.e this.DataContext = myCollection;)

    The window show the keyword using a System.Windows.Controls.ListBox
    where every item in the ListBox is a TextBox.

    And i wanted to allow reordering of those TextBox’s

    I did the following:

    // gets a Point and returns the relevant TextBlock
    // the InputHitTest is the important functino in this part.
    // it somehow knows to give me the TextBlock i put my //mouse on.
    private static TextBlock GetObjectDataFromPoint(ListBox source, Point point)
    {
    TextBlock element = (source.InputHitTest(point) as TextBlock);
    return element;

    }

    // on mouse down. we need to findout which TextBox we //selected using the previous function, and than
    // start the DragDrop Operation using this TextBox
    // (object data is the TextBox).

    private void ListBox_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
    {
    ListBox n = sender as ListBox;
    object data = (object)GetObjectDataFromPoint(n, e.GetPosition(n));
    if (data != null)
    {
    DragDrop.DoDragDrop((DependencyObject)data, data, DragDropEffects.Move);
    }
    }

    // The last function is the most important and the most
    // complicated .
    // The e.Data stores the original Object that
    // we started dragging in order to get the actual TextBlock
    // we call e.Data.GetData(typeof(TextBlock));
    // and once we got the TextBlock its easy to
    // get the Bounded element which is in my case the //KeywordTag (which is in the DataContext Property of the // TextBlock)
    // than we need(this was hard to get to…) to aquire the
    // new index in which we want to put our dragged item in to
    // we do this using the e.Position (which is the position of //the mouse when we release it..( i.e where we want to //drop the item.)
    // we allready know how to get from the position to the
    // TextBlock and from the TextBlock to the KeywordTag
    // and now the final step is to replace the indices of the
    // original TextBlock with the “new” text Block Position.
    // and that we can’t do on the ListBox itself..because it is //bounded and we cant change it.
    // we must make the change on the bounded data itself.
    // i.e the original Bounded Collection. in this case (tagList)
    // which is a collection of KeywordTag
    // and finally we can call Move on this collection to finish the
    // job.

    private void ListBox_Drop(object sender, DragEventArgs e)
    {
    ListBox n = sender as ListBox;

    DataObject dataObject = e.Data as DataObject;
    TextBlock o = dataObject.GetData(typeof(TextBlock)) as TextBlock;
    KeywordTag sourceTag = o.DataContext as KeywordTag;

    KeywordTag targetTag = ExtractTargetKeywordTag(e, n);
    if (sourceTag != null && targetTag != null)
    {
    tagList.Move(tagList.IndexOf(sourceTag), tagList.IndexOf(targetTag));
    }
    }

    // Enjoy!

  11. Cederash Says:

    Зачет, сенкс автору

  12. Ferinannnd Says:

    Захватывающе. Зачет! и ниипет!

  13. Avertedd Says:

    Так зачитался, что пропустил любимую передачу)

  14. holodila5302 Says:

    , …
    http://odnoklassnikiseks.pochta.ru
    [u]http://odnoklassnikiseks.pochta.ru[/u]

  15. Carlo Says:

    This can’t co-exist with D&D from THIS listbox to another control right?

  16. ToniM Says:

    Hi Simon,

    I found an error in the behavior:
    Select an item just by clicking and then try to move it, it wont work. The next hovered item will be moved, instead of the selected one, because the SelectionChange just fire, if the index is different.
    Solution:
    private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
    if (!this.dragging)
    {
    // ToniM: moved up here and just do it, when not dragging
    this.originalItemIndex = this.SelectedIndex;
    }
    else if (this.dragging && !this.dragItemSelected)
    {
    this.dragItemSelected = true;

    // ToniM: Changed selected index to original index.
    ListBoxItem listBoxItem = (ListBoxItem)ItemContainerGenerator.ContainerFromIndex(originalItemIndex);

    this.overlayElement = new DropPreviewAdorner((UIElement)this, listBoxItem);

    this.AdornerLayer.Add(this.overlayElement);
    }
    }

    One other good thing (from my point of view) is to use the system parameter for detecting the drag, because otherwise everytime click will be handled as moving.
    Solution:
    private void OnPreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
    {
    if (e.LeftButton == MouseButtonState.Pressed)
    {
    // ToniM: Use system parameters to detect a ‘real’ move
    Vector diff = dragStartPoint – e.GetPosition(this);
    if (Math.Abs(diff.X) > SystemParameters.MinimumHorizontalDragDistance ||
    Math.Abs(diff.Y) > SystemParameters.MinimumVerticalDragDistance)
    {
    // ToniM: set here that we’re in dragging mode (not in SelectionChanged)
    this.dragging = true;
    if (this.overlayElement != null)
    {
    Point currentPosition = (Point)e.GetPosition((IInputElement)this);
    this.overlayElement.LeftOffset = currentPosition.X;
    this.overlayElement.TopOffset = currentPosition.Y;
    }
    }
    }
    }

    BR ToniM

  17. y2trooper Says:

    A very helpful post. I’ve successfully managed to do this via touch screen for my personal app. Thanks.

  18. Preston Says:

    I got this website from my pal who informed me concerning this web site and
    now this time I am browsing this website and reading very informative articles at this place.

  19. buy fifa 17 coins Says:

    You’ve gotten the most effective webpages

  20. clash of clans Says:

    I like assembling useful information , this post has got me even more info! .


Leave a reply to Wade Cancel reply