Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Reorderable ListView

0.00/5 (No votes)
14 Dec 2009 2  
Inherited ListView which enables user to order multiple items by mouse, and automatically scrolls if necessary
Demo screenshot

Introduction

This work just extends another work I found here at CodeProject, and is inspired mainly with the article "Manual reordering of items inside a ListView". What is new in this project is that it is compact (everything needed is part of the inherited control), and that it supports draging multiple items and automatic scrolling, which I shaped by playlist of Foobar 2000 multimedia player. In this article, I will not describe everything in detail, on the other side I will describe all extended behavior, which I intended to implement.

Drag-and-drop Construction

As you probably found already, some coders, including MSDN writers, prefer to use built in Drag and Drop methods of ListView to implement Reorder by Mouse function. I found this quite limiting, and bringing no advantages at all. It seems also that we can ask, what does reordering have to do with clipboard. I guess the answer is pretty clear, until you need external data to enter ListView. So I used a method quite similar to that in the referred article. The only thing I changed is that the MouseDown event is not used to initiate Draging - both: initialization and progress are done in MouseMove. This is also the place where all values which are derived from mouse position are computed. Override WndProc is used to draw insertion line, when Invalidate is called from MouseMove. Finally this event handler is calling autoscroll initializer, which we are always draging. Of course MouseUp event is used to finalize reordering or eventually to cancel it.

Scrolling Concept

Three images on scrolling principle

Scrolling starts when IsDraging is set to True, and MouseMove occurs in a specified rectangle on top or at the bottom of ListView (Image 1). I defined this area in units of ItemCount, and speed of scrolling is computed from the relative position of mouse to these item sequences. This means that you can set property MaxScrollAreaSize to some count of items, and while your list is high enough to contain this number twice (otherwise Image 2), you can be sure that the control starts to scroll when you place your draging cursor above this number of items on top or at the bottom of the visible area. Scrolling itself is managed by one routine which computes its parameters from the mouse location, and one timer. 

Private Sub StartScroll()
  Dim hp# = Me.GetUserHurryPercentage
  Me.ScrollPerTick = cspt.Invoke(hp, Me.GetMaxItemVisibleCount)
  If Me.ScrollPerTick <> 0 Then
    Me.IsScrolling = True
    Me.ScrollTimer.Interval = csti.Invoke(hp)
    Me.ScrollTimer.Start()
  Else
    Me.ScrollTimer.Stop()
    Me.IsScrolling = False
  End If
End Sub

Note that scrolling parameters are two: count of items per scroll, and scroll interval. This is very important because if we had one of these options static, scrolling would not be very friendly (and fit for reordering process). Both these parameters are computed from HurryPercentage, what is just a position of the cursor above number of top or bottom items, in percent (Image 3). For example if we had this scrollarea to be three items at each side, and we put mouse to the first of them, HurryPercentage should be around 33 percents (= 0.33--). GetHurryPercentage function returns zero, if scrolling should stay turned off (when we are at the start or end already, or when we actually aren't in scroll area), <-1,0) for scrolling up, and (0,1> for scrolling down. Also note that I used delegate functions to convert this value to ScrollTimer interval and to ScrollPerTick<number of items> value. There is no other reason than to give control's user an opportunity to customize scrolling behavior if needed.

Public Delegate Function ScrollPerTickCalculator%_
	(ByVal UserHurryPercentage#, ByVal ItemsPerPage%)
Public Delegate Function ScrollTimerIntervalCalculator%(ByVal UserHurryPercentage#)
Private cspt As ScrollPerTickCalculator = _
	Function(a#, b%) Comparer(Of Double).Default.Compare(a, 0)
Private csti As ScrollTimerIntervalCalculator = _
	Function(a#) CInt(220 - 200 * Math.Abs(a))

These are default functions which in my opinion produce scrolling very similar to the one in Foobar. As you dig into lambdas above, you see that the function computing number of items per tick returns just {-1, 0, 1}, which is only job of every comparer. This means that visual component of scrolling is being smooth always, in default setting here. So only the second component can regulate and regulates scroll speed - you see, ScrollTimerInterval is always somewhere between 220 and 20 milliseconds. This means that above 500 items it gets a little bit boring to scroll from start to end. But still, it is not a very significant delay. If you need more, your functions can speed down TimerInterval for last 30% of hurry, and return ItemsPerPage or its derivate for ScrollPerTick... which will result in option of smooth scrolling and also fast scrolling.

External Drag-and-drop Support

Second version already implements this option. Such implementation brings some new elements to behavior, and of course requires few more properties. You can research, that on DragEnter, EventHandlers managing scrolling and InsertionLine drawing react no more. It is pretty easy to make it up. I simply added new handlers for events like DragOver or DragLeave, and these handlers generally do nothing more than calling MouseMove, or MouseLeave. One remarkable thing is, that when you need to Drag some items from this control, you call DoDragDrop from your MouseLeave when specified mouse button is pressed, so that even if you return with these data back to control, you can't switch somehow from Drag-and-drop mode to MouseMove handled reordering routine. This is solved by data origin recognition in internal DragDrop event handler. BTW, it is possible to provide all functionality based on standard Mouse events with Drag events, with some improvisation (you are not as well informed with DragEventArgs as with MouseEventArgs).

Notice, that every external drop must be confirmed in DragEnter event, by setting Effect to value different from None. All external Drops must come in array of ListViewItem or in Collection to be managed automatically. If this is not possible, CantConvertDrop event is raised, and user can convert them for control, or let event unhandled to cancel Drop operation. DropCancelled event is raised if data couldn't be converted, or if data collection was empty. Notice IsDragingInside and IsDragingOutside properties, they can help you managing DragEnter or MouseLeave events.

Behavior Extensions for other ListView `View` Modes

Second version modifies concept of internal event handling. If you follow naming convence for event handlers (when extending base class) which is described in top-level XML remark, you can just add new behavior to Enum BehaviorExtensionType, and all work with switching eventhandlers for another extension is fully automated for you (see code region Internal handler managing). If you want to extend control by inheritation, you must suffice with protected property AllowedBehaviorExtension by which you can switch between your bases behavior extensions (or disable them).

The reason for pretty though Reflection coding of events is efficiency, but also easy extensibility. With two shadows events already it seems pretty difficult to make some another extension, without some built in helper eventhandler management. You could simply erase all this section and preserve that property. If you mind hardcoding bunch of AddHandler and RemoveHandler statements within Select Case BehaviorExtension. For me it seems much easier to move on with just remembering straight ahead naming convence.

Important Note

  • Remember to set AllowDrop if you need to transfer items between controls.
  • Notice shadows events DragEnter and DragDrop. If you handle DragEnter, you can prevent control from any reaction on crossing Drag-and-drop data by setting Effect to None. And if you do this in DragDrop you can cancel all consequences of Drop.
  • DragDrop accepts only array of ListViewItem or Collection, so ensure, that you allow in and out only this type of data, or that you handle conversion event CantConvertDrop.
  • Control behavior is extended only when its View property is set to Detail.
  • BottomItem & LastVisibleItem property in another View mode may return invalid results (as standard TopItem will).

Public Properties of Control

  • AllowReorder - determines whether reordering and all subsequent behavior is enabled
  • AutoDropFocus - determines whether control focuses itself on successful Drop
  • AutoDropSelect - determines whether control selects Droped data
  • DragMouseButton - gets or sets mouse button used to intitate reorder or drag
  • MaxScrollAreaSize - gets or sets count of items at both ends of list, which maintains scrolling, this number is used when count of visible items >= this number. If there are not enough items visible, a smaller number is used. Basically demanding two items high list to provide reordering and automatic scrolling is nonsense. All routines in this class check if the result number is higher than zero, else do nothing. Result number is computed on every SizeChanged and FontChanged. This property is Designer and DesignerSerialize friendly.
  • CalcScrollPerTick - gets or sets function which computes smoothness of scrolling from user requested speed percentage, you can also use its second parameter, which tells you how long the current page of listview is. This property is not PropertyEditor friendly, nor does it save to designer code.
  • CalcScrollTimerInterval - gets or sets function which computes speed for ScrollTimer. This property is not PropertyEditor friendly, nor does it save to designer code.
  • IsDragingInside - determines whether control is currently in reordering or draging-in state
  • IsDragingOutside - determines whether control was in reordering or draging-in state, but user left its area with stuff. Note, that this property does not reflect origin of current dragdrop data.
  • LastVisibleItem - gets last visible item of ListViewRO, this property is extension of BottomItem function
  • SetReorderAutoScrollParameters - This is just a method which provides setting of all scrolling parameters at once.
  • BottomItem - gets item which is at bottom of DisplayRectangle
  • InsertionLineWidth - gets or sets thickness of insertionline. This property is Designer friendly and is stored in designer code.
  • InsertionLineColor - gets or sets color of insertionline. This property is Designer friendly and is stored in designer code.
  • InsertionLinePen - gets or sets two previous properties at all. This property is not Designer friendly nor it is saved in designer code.

Other Important Exposed Stuff

  • ItemsReordered - public event, bringing few important informations about current reorder or dragdrop operation. On external Drop this event occures before Focus (when AutoDropFocus is on)
  • CantConvertDrop - is raised when Data allowed to come in are Droped in, but are not in recogized format (see important note section).
  • CancelDrop - is raised when Data could not be converted or were empty.
  • Reorder - This routine moves selected items to specified index and raises ItemsReordered event, if necessary.

Limitations

This control in the current state doesn't support external Drop. I can't imagine what is the best logic for extremely subsized ListView, so by default it just loses its Reorder & Autoscroll functionality. Probably you can solve it by setting Drag-and-drop Effect to Scroll+Move, any ideas welcomed.

Discussion

I will be glad if you find some different lambdas for computing scroll parameters. I can then update this code with some Enum, which will switch between them. I am also curious if someone has some idea about moving multiple items when there are spaces in selection - as you may have researched, my code joins all selected items to a single sequence when moved to newIndex.

History

  • 30th August, 2009: Initial post
  • 14th December, 2009: General updates, mostly based on deep bug report and feature request by KPEBEDKO.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here