diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index 7ce2624d02..2556bd4c4c 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -436,6 +436,29 @@ namespace Avalonia.Controls.Selection } } + private protected override bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e) + { + if (!base.IsValidCollectionChange(e)) + { + return false; + } + + if (ItemsView is object && e.Action == NotifyCollectionChangedAction.Add) + { + if (e.NewStartingIndex <= _selectedIndex) + { + return _selectedIndex + e.NewItems.Count < ItemsView.Count; + } + + if (e.NewStartingIndex <= _anchorIndex) + { + return _anchorIndex + e.NewItems.Count < ItemsView.Count; + } + } + + return true; + } + protected override void OnSourceCollectionChangeFinished() { if (_operation is object) diff --git a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs index ff3b8f43a8..230575074a 100644 --- a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs +++ b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using Avalonia.Controls.Utils; #nullable enable @@ -234,6 +235,11 @@ namespace Avalonia.Controls.Selection var shiftIndex = -1; List? removed = null; + if (!IsValidCollectionChange(e)) + { + return; + } + switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -276,6 +282,34 @@ namespace Avalonia.Controls.Selection } } + private protected virtual bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e) + { + // If the selection is modified in a CollectionChanged handler before the selection + // model's CollectionChanged handler has had chance to run then we can end up with + // a selected index that refers to the *new* state of the Source intermixed with + // indexes that reference an old state of the source. + // + // There's not much we can do in this situation, so detect whether shifting the + // current selected indexes would result in an invalid index in the source, and if + // so bail. + // + // See unit test Handles_Selection_Made_In_CollectionChanged for more details. + if (ItemsView is object && + RangesEnabled && + Ranges.Count > 0 && + e.Action == NotifyCollectionChangedAction.Add) + { + var lastIndex = Ranges.Last().End; + + if (e.NewStartingIndex <= lastIndex) + { + return lastIndex + e.NewItems.Count < ItemsView.Count; + } + } + + return true; + } + private protected struct CollectionChangeState { public int ShiftIndex;