diff --git a/samples/TreeDataGridDemo/App.axaml.cs b/samples/TreeDataGridDemo/App.axaml.cs index 9edeec1e..cc3d1f15 100644 --- a/samples/TreeDataGridDemo/App.axaml.cs +++ b/samples/TreeDataGridDemo/App.axaml.cs @@ -9,6 +9,7 @@ public class App : Application public override void Initialize() { AvaloniaXamlLoader.Load(this); + Bogus.Randomizer.Seed = new System.Random(0); } public override void OnFrameworkInitializationCompleted() diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs index 5f4e4850..483acddb 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs @@ -663,6 +663,13 @@ private void RecycleElement(Control element, int index) private void RecycleElementOnItemRemoved(Control element) { + if (element == _focusedElement) + { + _focusedElement.LostFocus -= OnUnrealizedFocusedElementLostFocus; + _focusedElement = null; + _focusedIndex = -1; + } + UnrealizeElementOnItemRemoved(element); element.IsVisible = false; ElementFactory!.RecycleElement(element); @@ -691,6 +698,12 @@ private void TrimUnrealizedChildren() private void OnItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { + void ClearFocusedElement(int index, int count) + { + if (_focusedElement is not null && _focusedIndex >= index && _focusedIndex < index + count) + RecycleElementOnItemRemoved(_focusedElement); + } + InvalidateMeasure(); if (_realizedElements is null) @@ -703,16 +716,21 @@ private void OnItemsCollectionChanged(object? sender, NotifyCollectionChangedEve break; case NotifyCollectionChangedAction.Remove: _realizedElements.ItemsRemoved(e.OldStartingIndex, e.OldItems!.Count, _updateElementIndex, _recycleElementOnItemRemoved); + ClearFocusedElement(e.OldStartingIndex, e.OldItems!.Count); break; case NotifyCollectionChangedAction.Replace: _realizedElements.ItemsReplaced(e.OldStartingIndex, e.OldItems!.Count, _recycleElementOnItemRemoved); + ClearFocusedElement(e.OldStartingIndex, e.OldItems!.Count); break; case NotifyCollectionChangedAction.Move: _realizedElements.ItemsRemoved(e.OldStartingIndex, e.OldItems!.Count, _updateElementIndex, _recycleElementOnItemRemoved); _realizedElements.ItemsInserted(e.NewStartingIndex, e.NewItems!.Count, _updateElementIndex); + ClearFocusedElement(e.OldStartingIndex, e.OldItems!.Count); break; case NotifyCollectionChangedAction.Reset: _realizedElements.ItemsReset(_recycleElementOnItemRemoved); + if (_focusedElement is not null ) + RecycleElementOnItemRemoved(_focusedElement); break; } } diff --git a/tests/Avalonia.Controls.TreeDataGrid.Tests/Primitives/TreeDataGridRowsPresenterTests.cs b/tests/Avalonia.Controls.TreeDataGrid.Tests/Primitives/TreeDataGridRowsPresenterTests.cs index b762dd26..d3822bd7 100644 --- a/tests/Avalonia.Controls.TreeDataGrid.Tests/Primitives/TreeDataGridRowsPresenterTests.cs +++ b/tests/Avalonia.Controls.TreeDataGrid.Tests/Primitives/TreeDataGridRowsPresenterTests.cs @@ -352,6 +352,78 @@ public void Handles_Removing_Row_Range_That_Invalidates_Current_Viewport() Assert.Equal(new Size(100, 100), target.Bounds.Size); } + [AvaloniaFact(Timeout = 10000)] + public void Handles_Removing_Focused_Row_While_Outside_Viewport() + { + var (target, scroll, items) = CreateTarget(); + var element = target.RealizedElements.ElementAt(0)!; + + element.Focusable = true; + element.Focus(); + + // Scroll down one item. + scroll.Offset = new Vector(0, 10); + Layout(target); + + // Remove the focused element. + items.RemoveAt(0); + + // Scroll back to the beginning. + scroll.Offset = new Vector(0, 0); + Layout(target); + + // The correct element should be shown. + Assert.Same(items[0], target.RealizedElements.ElementAt(0)!.DataContext); + } + + [AvaloniaFact(Timeout = 10000)] + public void Handles_Replacing_Focused_Row_While_Outside_Viewport() + { + var (target, scroll, items) = CreateTarget(); + var element = target.RealizedElements.ElementAt(0)!; + + element.Focusable = true; + element.Focus(); + + // Scroll down one item. + scroll.Offset = new Vector(0, 10); + Layout(target); + + // Replace the focused element. + items[0] = new Model { Id = 100, Title = "New Item" }; + + // Scroll back to the beginning. + scroll.Offset = new Vector(0, 0); + Layout(target); + + // The correct element should be shown. + Assert.Same(items[0], target.RealizedElements.ElementAt(0)!.DataContext); + } + + [AvaloniaFact(Timeout = 10000)] + public void Handles_Moving_Focused_Row_While_Outside_Viewport() + { + var (target, scroll, items) = CreateTarget(); + var element = target.RealizedElements.ElementAt(0)!; + + element.Focusable = true; + element.Focus(); + + // Scroll down one item. + scroll.Offset = new Vector(0, 10); + Layout(target); + + // Move the focused element. + items.Move(0, items.Count - 1); + + // Scroll back to the beginning. + scroll.Offset = new Vector(0, 0); + Layout(target); + + // The correct element should be shown. + Assert.Same(items[0], target.RealizedElements.ElementAt(0)!.DataContext); + } + [AvaloniaFact(Timeout = 10000)] public void Updates_Star_Column_ActualWidth() {