Lately I've been investigating a few performance related issues found around big virtualized lists and trees.
Setup was always quite similar and innocent:
SelectedItems to an ObservableCollection<T> (or similar)CollectionChanged event on said collection and then do some operation on the whole selection (let's say we calculate a sum of selected numbers)When recreating such setup in ControlCatalog (with a list of 100k integers and CollectionChanged handler summing SelectedItems) we are getting around 40 seconds freeze due to 100k discrete SelectionChanged events being raised.
We can't even batch such changes since we don't know which SelectionChanged event is last. And Avalonia cannot use AddRange method since there is no standard one available (https://github.com/dotnet/corefx/issues/10752, which actually got merged for a moment but was promptly reverted since it broke WPF).
This makes one wonder about usability of SelectedItems when dealing with bigger data and/or caring about performance in general (reducing UI stutters, etc.). Since even having a small collection and expensive calculation can freeze the whole application.
One can use Rx.Net buffering or throttling capabilities but this again increases complexity of the solution and makes debugging harder. And requires user to actually use reactive extensions and such basic concept as selection should work fine out of box in my opinion.
One is actually forced to subscribe to SelectionChanged event in this case (which is also broken, but that one is described in https://github.com/AvaloniaUI/Avalonia/pull/3435 and can be fixed).
I wonder about applicability in MVVM scenarios where developer may choose SelectedItems during development not realizing its limitations and then hitting a wall when bigger data set is used by the user, with no means of resolving this problem apart from a rewrite using SelectionChanged event.
From my understanding WPF can't deal with multiple added / removed items in one event but that isn't limiting us from supporting this.
Note that AvaloniaList does have AddRange available. So we could attempt to cast the collection to IAvaloniaList and do a batched change.
Yeah, this is definitely a problem, trying to cast to IAvaloniaList<object> isn't ideal either because IAvaloniaList isn't covariant.
There are actually a few problems here:
AddRange on IList or similar interfaces meaning that we can't batch selectionsSelectedItems - this could be a huge number of itemsI think the solution (which I'm currently working on) is to port WinUI's SelectionModel to Avalonia. SelectionModel has the following properties:
ListBox and TreeView have separate selection implementations currently)0-99999My current thinking is that there would be a bindable direct property on e.g. SelectingItemsControl:
public ISelectionModel Selection { get; set; }
If unbound, the control would create its own SelectionModel or the user could bind it to a SelectionModel in their view model.
Ported SelectionModel but hit a few roadblocks:
SelectionModel.SelectionChanged doesn't actually tell you what changed (https://github.com/microsoft/microsoft-ui-xaml/issues/1645). This is a showstopper.SelectionModel doesn't handle Reset events: it _can't_ because it only stores the selected indexes and has no idea how these indexes apply to the data after a reset. Not sure what to do about this.AlwaysSelected flag adding (this one's easy)
Most helpful comment
Yeah, this is definitely a problem, trying to cast to
IAvaloniaList<object>isn't ideal either becauseIAvaloniaListisn't covariant.There are actually a few problems here:
AddRangeonIListor similar interfaces meaning that we can't batch selectionsSelectedItems- this could be a huge number of itemsI think the solution (which I'm currently working on) is to port WinUI's
SelectionModelto Avalonia.SelectionModelhas the following properties:ListBoxandTreeViewhave separate selection implementations currently)0-99999My current thinking is that there would be a bindable direct property on e.g.
SelectingItemsControl:If unbound, the control would create its own
SelectionModelor the user could bind it to aSelectionModelin their view model.