I’m working with a listbox and an accompanying data model, which must be kept in sync with the content of the listbox.
This works great, and I can even update my data model when dragging and dropping rows in my listbox to reorder them.
The only issue is sorting by clicking the column headers. I can get a notification that the columns are about to be sorted (see events HeaderPressed and SortColumn), but these fire before the sorting has taken place.
To update my model I would need to be notified after the listbox’s sort has completed, so I don’t have out-of-order data.
Is there such an event? How do you react to a listbox being sorted?
I guess you want to be certain the sort is over. What about :
Function isSorted(LB as listbox, Col as integer) As Boolean
Dim Ascending as Boolean
if LB.ListCount >=1 then
Ascending = LB.Cell(0,Col) < LB.Cell(1,Col)
if Ascending then
For i as integer = 0 to LB.ListCount-2
if LB.Cell(i,Col) > LB.Cell(i+1,Col) then return false
next
else // Descending
For i as integer = LB.ListCount-1 to 1 step -1
if LB.Cell(i,Col) > LB.Cell(i-1,Col) then return false
next
end if
return true
end if
Return false
End Function
Add a 100 ms off timer to the page, with :
Sub Action()
if isSorted(ListBox1, 0) then
me.mode = Timer.ModeOff
msgbox "Sorted"
end if
End Sub
In the ListBox :
Function SortColumn(column As Integer) As Boolean
Timer1.Mode = Timer.ModeMultiple
End Function
IsSorted assumes the default string sort is used. Other kinds of sort such as numbers or booleans will need a modified method.
Hmm, that’s pretty smart, but it ignores a possible custom sort in the comparerows event. I wonder if it is possible to use the comparerows event to determine if the rows being compared are the last ones so that we can assume the sort will be finished after that. If this is the case it should also be a lot faster.
Event HeaderPressed(column as Integer) As Boolean
IsSorting = True
End
Event CellTextPaint(g As Graphics, row As Integer, column As Integer, x as Integer, y as Integer) As Boolean
If isSorting Then
isSorting = False
// …
End
End[/code]
I did mention that the method had to be modified for special sorts. But it may not be necessary since one can assume the special sort will be carried out that a method that can trigger an action when it returns.
Originally, I just tested row 0 and 1, then last and before last. But I would not trust fate that will once in a while produce just that kind of situation at random. Then I thought about testing just the end or the beginning depending on the order, but having no idea what kind of sorting method is employed by Xojo, I decided to take no chance. One cannot assume the sort will be nicely done from beginning to end.
Thanks for all the replies! I wanted to avoid using a timer since that seemed hack-ish. I also wanted to avoid depending on implementation details (such as how Xojo calls the CompareRows event handler).
I’m definitely open to subclassing.
Looks like I hadn’t thought of doing the sort directly in the SortColumn event (and returning True to avoid being sorted again), which ensures that the sort is complete before I call a custom “FinishedSorting” event.
While Marco’s idea is excellent (kudos), timers should not be dismissed as “hackish”. In an event-oriented RAD, they are a very useful and perfectly fine object, when they are used right. Just like threads. They are also the only valid way to obtain delays between operations, unlike tight loops and other horror code trying to pause execution in typical procedural style.
Timers have their use, for sure, but using a timer to work around a problem of notification seems like a hack, because there will be a period of time, while the main loop is running, that my model will not match what’s displayed in the listbox. And relying on an implementation detail (how long does it take to sort a listbox? On what hardware?) is fraught with peril. I have considered it, but am looking for a better alternative. I’ll implement and report back here.
In the release branch of our software (the one that’s about to go out the door), I implemented Marco’s method and, once Me.Sort() was called I re-arranged the model immediately to match and returned True, so that no other calls to Sort would be made.
In the master branch (under active development), I did not implement Marco’s method. I added a flag to the model to mark it as invalid, and set that flag in HeaderPressed (which then returns False, because it does nothing). I then changed all direct access to my model to an accessor method that checked the flag and, if it was set, rearranged the model. So whenever you accessed the model, it was always up-to-date and this decoupled the UI changes (listbox) from the model changes (model).
Why the two approaches? The second one is cleaner, for one simple reason: drag-and-drop. I can re-use the same flag in the DragReorderRows() event and the code will “just work”: one code path, very clean. But I had to change my model to add this flag, which means that the scope was much higher (basically, the whole window). Which is fine in master branch, but less than ideal in the Release branch.
in the Release branch I wrote a special method (with unit tests) to re-arrange the model when DragReorderRows() is called. It’s a bit more complex (two methods to rearrange the model) but limits the scope quite nicely, which is the desired effect here.
Just FYI, there is no peril at all. You are guaranteed that a very short period timer will not fire until the sort is complete. So far from being “hackish”, the timer guarantees completion with minimum delay. I’m not saying it’s the best solution for this problem, but it shouldn’t be brushed of as an invalid solution.