Browse Source

Fix scrolling to selection.

A few `AutoScrollToSelectedItem` improvements:

- Scroll to current selected item when it's set to true
- Scroll to current selected item when list first displayed
- Scroll to current selected item when attached to visual tree if the selection was changed while it wasn't attached

Fixes #4100
pull/4659/head
Steven Kirk 5 years ago
parent
commit
14f314341e
  1. 1
      src/Avalonia.Controls/ListBox.cs
  2. 48
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  3. 119
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

1
src/Avalonia.Controls/ListBox.cs

@ -163,6 +163,7 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
Scroll = e.NameScope.Find<IScrollable>("PART_ScrollViewer");
}
}

48
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -116,6 +116,7 @@ namespace Avalonia.Controls.Primitives
private IList? _oldSelectedItems;
private bool _ignoreContainerSelectionChanged;
private UpdateState? _updateState;
private bool _hasScrolledToSelectedItem;
/// <summary>
/// Initializes static members of the <see cref="SelectingItemsControl"/> class.
@ -381,6 +382,28 @@ namespace Avalonia.Controls.Primitives
}
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
AutoScrollToSelectedItemIfNecessary();
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
void ExecuteScrollWhenLayoutUpdated(object sender, EventArgs e)
{
LayoutUpdated -= ExecuteScrollWhenLayoutUpdated;
AutoScrollToSelectedItemIfNecessary();
}
if (AutoScrollToSelectedItem)
{
LayoutUpdated += ExecuteScrollWhenLayoutUpdated;
}
}
/// <inheritdoc/>
protected override void OnContainersMaterialized(ItemContainerEventArgs e)
{
@ -481,6 +504,10 @@ namespace Avalonia.Controls.Primitives
{
base.OnPropertyChanged(change);
if (change.Property == AutoScrollToSelectedItemProperty)
{
AutoScrollToSelectedItemIfNecessary();
}
if (change.Property == ItemsProperty && _updateState is null && _selection is object)
{
var newValue = change.NewValue.GetValueOrDefault<IEnumerable>();
@ -669,12 +696,10 @@ namespace Avalonia.Controls.Primitives
/// <param name="e">The event args.</param>
private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ISelectionModel.AnchorIndex) && AutoScrollToSelectedItem)
if (e.PropertyName == nameof(ISelectionModel.AnchorIndex))
{
if (Selection.AnchorIndex > 0)
{
ScrollIntoView(Selection.AnchorIndex);
}
_hasScrolledToSelectedItem = false;
AutoScrollToSelectedItemIfNecessary();
}
else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex) && _oldSelectedIndex != SelectedIndex)
{
@ -751,6 +776,19 @@ namespace Avalonia.Controls.Primitives
}
}
private void AutoScrollToSelectedItemIfNecessary()
{
if (AutoScrollToSelectedItem &&
!_hasScrolledToSelectedItem &&
Presenter is object &&
Selection.AnchorIndex > 0 &&
((IVisual)this).IsAttachedToVisualTree)
{
ScrollIntoView(Selection.AnchorIndex);
_hasScrolledToSelectedItem = true;
}
}
/// <summary>
/// Called when a container raises the <see cref="IsSelectedChangedEvent"/>.
/// </summary>

119
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -1402,12 +1402,36 @@ namespace Avalonia.Controls.UnitTests.Primitives
Items = items,
};
var raised = false;
Prepare(target);
target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true);
target.SelectedIndex = 2;
Assert.True(raised);
}
[Fact]
public void AutoScrollToSelectedItem_Causes_Scroll_To_Initial_SelectedItem()
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"Baz"
};
var target = new ListBox
{
Template = Template(),
Items = items,
};
var raised = false;
target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true);
target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true);
target.SelectedIndex = 2;
Prepare(target);
Assert.True(raised);
}
@ -1451,6 +1475,99 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
}
[Fact]
public void AutoScrollToSelectedItem_Scrolls_When_Reattached_To_Visual_Tree_If_Selection_Changed_While_Detached_From_Visual_Tree()
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"Baz"
};
var target = new ListBox
{
Template = Template(),
Items = items,
SelectedIndex = 2,
};
var raised = false;
Prepare(target);
var root = (TestRoot)target.Parent;
target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true);
root.Child = null;
target.SelectedIndex = 1;
root.Child = target;
Assert.True(raised);
}
[Fact]
public void AutoScrollToSelectedItem_Doesnt_Scroll_If_Reattached_To_Visual_Tree_With_No_Selection_Change()
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"Baz"
};
var target = new ListBox
{
Template = Template(),
Items = items,
SelectedIndex = 2,
};
var raised = false;
Prepare(target);
var root = (TestRoot)target.Parent;
target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true);
root.Child = null;
root.Child = target;
Assert.False(raised);
}
[Fact]
public void AutoScrollToSelectedItem_Causes_Scroll_When_Turned_On()
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"Baz"
};
var target = new ListBox
{
Template = Template(),
Items = items,
AutoScrollToSelectedItem = false,
};
Prepare(target);
var raised = false;
target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true);
target.SelectedIndex = 2;
Assert.False(raised);
target.AutoScrollToSelectedItem = true;
Assert.True(raised);
}
[Fact]
public void Can_Set_Both_SelectedItem_And_SelectedItems_During_Initialization()
{

Loading…
Cancel
Save