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
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.