ReordereableListBox

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);

}

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: