csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
523 lines
19 KiB
523 lines
19 KiB
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Linq;
|
|
using Avalonia.Collections;
|
|
using Avalonia.Controls.Presenters;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Controls.Templates;
|
|
using Avalonia.Threading;
|
|
using Avalonia.UnitTests;
|
|
using Xunit;
|
|
|
|
namespace Avalonia.Controls.UnitTests.Primitives
|
|
{
|
|
public class SelectingItemsControlTests_AutoSelect : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public void First_Item_Should_Be_Selected()
|
|
{
|
|
var target = new TestSelector
|
|
{
|
|
ItemsSource = new[] { "foo", "bar" },
|
|
Template = Template(),
|
|
};
|
|
|
|
target.ApplyTemplate();
|
|
|
|
Assert.Equal(0, target.SelectedIndex);
|
|
Assert.Equal("foo", target.SelectedItem);
|
|
}
|
|
|
|
[Fact]
|
|
public void First_Item_Should_Be_Selected_When_Added()
|
|
{
|
|
var items = new AvaloniaList<string>();
|
|
var target = new TestSelector
|
|
{
|
|
ItemsSource = items,
|
|
Template = Template(),
|
|
};
|
|
|
|
target.ApplyTemplate();
|
|
items.Add("foo");
|
|
|
|
Assert.Equal(0, target.SelectedIndex);
|
|
Assert.Equal("foo", target.SelectedItem);
|
|
}
|
|
|
|
|
|
[Fact]
|
|
public void First_Item_Should_Be_Selected_When_Reset()
|
|
{
|
|
var items = new ResetOnAdd();
|
|
var target = new TestSelector
|
|
{
|
|
ItemsSource = items,
|
|
Template = Template(),
|
|
};
|
|
|
|
target.ApplyTemplate();
|
|
items.Add("foo");
|
|
|
|
Assert.Equal(0, target.SelectedIndex);
|
|
Assert.Equal("foo", target.SelectedItem);
|
|
}
|
|
|
|
[Fact]
|
|
public void Item_Should_Be_Selected_When_Selection_Removed()
|
|
{
|
|
var items = new AvaloniaList<string>(new[] { "foo", "bar", "baz", "qux" });
|
|
|
|
var target = new TestSelector
|
|
{
|
|
ItemsSource = items,
|
|
Template = Template(),
|
|
};
|
|
|
|
target.ApplyTemplate();
|
|
target.SelectedIndex = 2;
|
|
items.RemoveAt(2);
|
|
|
|
Assert.Equal(0, target.SelectedIndex);
|
|
Assert.Equal("foo", target.SelectedItem);
|
|
}
|
|
|
|
[Fact]
|
|
public void Can_Change_Selection_From_SelectionChanged_When_AlwaysSelected_Reselects()
|
|
{
|
|
// Regression test for the #7536 fix: clearing the selection makes AlwaysSelected
|
|
// reselect the first item (via LostSelection), which raises SelectionChanged. A
|
|
// handler that changes the selection from there must be honoured rather than
|
|
// swallowed by the batch update that wraps the LostSelection handler.
|
|
var target = new TestSelector
|
|
{
|
|
ItemsSource = new[] { "foo", "bar", "baz" },
|
|
Template = Template(),
|
|
};
|
|
|
|
target.ApplyTemplate();
|
|
target.SelectedIndex = 1;
|
|
|
|
var raised = 0;
|
|
target.SelectionChanged += (s, e) =>
|
|
{
|
|
if (++raised == 1)
|
|
target.SelectedIndex = 2;
|
|
};
|
|
|
|
target.SelectedIndex = -1;
|
|
|
|
Assert.Equal(2, target.SelectedIndex);
|
|
Assert.Equal("baz", target.SelectedItem);
|
|
}
|
|
|
|
[Fact]
|
|
public void Selection_Should_Be_Cleared_When_No_Items_Left()
|
|
{
|
|
var items = new AvaloniaList<string>(new[] { "foo", "bar" });
|
|
|
|
var target = new TestSelector
|
|
{
|
|
ItemsSource = items,
|
|
Template = Template(),
|
|
};
|
|
|
|
target.ApplyTemplate();
|
|
target.SelectedIndex = 1;
|
|
items.RemoveAt(1);
|
|
items.RemoveAt(0);
|
|
|
|
Assert.Equal(-1, target.SelectedIndex);
|
|
Assert.Null(target.SelectedItem);
|
|
}
|
|
|
|
[Fact]
|
|
public void Removing_Selected_First_Item_Should_Select_Next_Item()
|
|
{
|
|
var items = new AvaloniaList<string>(new[] { "foo", "bar" });
|
|
var target = new TestSelector
|
|
{
|
|
ItemsSource = items,
|
|
Template = Template(),
|
|
};
|
|
|
|
target.ApplyTemplate();
|
|
target.Presenter!.ApplyTemplate();
|
|
items.RemoveAt(0);
|
|
|
|
Assert.Equal(0, target.SelectedIndex);
|
|
Assert.Equal("bar", target.SelectedItem);
|
|
}
|
|
|
|
[Fact]
|
|
public void First_Visible_Item_Should_Be_Selected_When_First_Container_Is_Hidden()
|
|
{
|
|
// Uses own-container items (Controls) so that IsVisible can be set before preparation.
|
|
var target = new TestSelector
|
|
{
|
|
ItemsSource = new object[]
|
|
{
|
|
new ListBoxItem { Content = "hidden", IsVisible = false },
|
|
new ListBoxItem { Content = "visible" },
|
|
},
|
|
Template = Template(),
|
|
};
|
|
|
|
target.ApplyTemplate();
|
|
target.Presenter!.ApplyTemplate();
|
|
|
|
Assert.Equal(1, target.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void First_Enabled_Item_Should_Be_Selected_When_First_Container_Is_Disabled()
|
|
{
|
|
var target = new TestSelector
|
|
{
|
|
ItemsSource = new object[]
|
|
{
|
|
new ListBoxItem { Content = "disabled", IsEnabled = false },
|
|
new ListBoxItem { Content = "enabled" },
|
|
},
|
|
Template = Template(),
|
|
};
|
|
|
|
target.ApplyTemplate();
|
|
target.Presenter!.ApplyTemplate();
|
|
|
|
Assert.Equal(1, target.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void First_Visible_Item_Should_Be_Selected_When_Container_Becomes_Hidden_During_Preparation()
|
|
{
|
|
// Regression test for https://github.com/AvaloniaUI/Avalonia/pull/20798
|
|
// Simulates a MVVM scenario where container visibility is set by a binding applied
|
|
// during PrepareContainerForItemOverride (e.g. an ItemContainerTheme). Verifies that
|
|
// selection lands on the first truly visible container rather than an unrealized item.
|
|
var target = new AlwaysSelectedTestSelectorHidingFirstContainers(hiddenCount: 2)
|
|
{
|
|
ItemsSource = new[] { "item-0", "item-1", "item-2" },
|
|
Template = Template(),
|
|
};
|
|
|
|
target.ApplyTemplate();
|
|
target.Presenter!.ApplyTemplate();
|
|
|
|
Assert.Equal(2, target.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void Selection_Should_Be_Cleared_When_All_Containers_Are_Hidden_During_Preparation()
|
|
{
|
|
// When all containers are made invisible during preparation (MVVM binding scenario),
|
|
// SelectedIndex must be -1 rather than the last container's index. Previously the
|
|
// selection would cascade to the last unrealized item and land on an invisible one.
|
|
var target = new AlwaysSelectedTestSelectorHidingFirstContainers(hiddenCount: 3)
|
|
{
|
|
ItemsSource = new[] { "item-0", "item-1", "item-2" },
|
|
Template = Template(),
|
|
};
|
|
|
|
target.ApplyTemplate();
|
|
target.Presenter!.ApplyTemplate();
|
|
|
|
Assert.Equal(-1, target.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void First_Visible_Item_Should_Be_Selected_When_Items_Are_Added_To_A_Laid_Out_Control()
|
|
{
|
|
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
|
|
|
|
var items = new AvaloniaList<string>();
|
|
var target = new AlwaysSelectedTestSelectorHidingFirstContainers(hiddenCount: 1);
|
|
InitWithBeginEndInit(target, items);
|
|
|
|
items.Add("item-0");
|
|
items.Add("item-1");
|
|
|
|
Assert.Equal(1, target.SelectedIndex);
|
|
Assert.Equal("item-1", target.SelectedItem);
|
|
}
|
|
|
|
[Fact]
|
|
public void Selection_Should_Be_Cleared_When_A_Hidden_Item_Is_Added_To_A_Laid_Out_Control()
|
|
{
|
|
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
|
|
|
|
var items = new AvaloniaList<string>();
|
|
var target = new AlwaysSelectedTestSelectorHidingFirstContainers(hiddenCount: 1);
|
|
InitWithBeginEndInit(target, items);
|
|
|
|
items.Add("item-0");
|
|
|
|
Assert.Equal(-1, target.SelectedIndex);
|
|
Assert.Null(target.SelectedItem);
|
|
}
|
|
|
|
[Fact]
|
|
public void First_Enabled_Item_Should_Be_Selected_When_Items_Are_Added_To_A_Laid_Out_Control()
|
|
{
|
|
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
|
|
|
|
var items = new AvaloniaList<string>();
|
|
var target = new AlwaysSelectedTestSelectorDisablingFirstContainers(disabledCount: 1);
|
|
InitWithBeginEndInit(target, items);
|
|
|
|
items.Add("item-0");
|
|
items.Add("item-1");
|
|
|
|
Assert.Equal(1, target.SelectedIndex);
|
|
Assert.Equal("item-1", target.SelectedItem);
|
|
}
|
|
|
|
[Fact]
|
|
public void Selection_Should_Settle_On_The_Visible_Item_When_Items_Are_Added_To_A_Laid_Out_Control()
|
|
{
|
|
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
|
|
|
|
var items = new AvaloniaList<string>();
|
|
var target = new AlwaysSelectedTestSelectorHidingFirstContainers(hiddenCount: 1);
|
|
InitWithBeginEndInit(target, items);
|
|
|
|
var selectionChanges = new List<object?>();
|
|
target.SelectionChanged += (_, _) => selectionChanges.Add(target.SelectedItem);
|
|
|
|
items.Add("item-0");
|
|
items.Add("item-1");
|
|
|
|
Assert.Equal("item-1", selectionChanges.Last());
|
|
Assert.Equal("item-1", target.SelectedItem);
|
|
}
|
|
|
|
[Fact]
|
|
public void AutoScrollToSelectedItem_Should_Work_When_Becoming_Visible()
|
|
{
|
|
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
|
|
{
|
|
var items = Enumerable.Range(0, 100).Select(i => $"Item {i}").ToList();
|
|
|
|
var target = new ListBox
|
|
{
|
|
Template = new FuncControlTemplate(CreateListBoxTemplate),
|
|
ItemsSource = items,
|
|
ItemTemplate = new FuncDataTemplate<string>((_, _) => new TextBlock { Height = 50 }),
|
|
Height = 100,
|
|
ItemsPanel = new FuncTemplate<Panel?>(() => new VirtualizingStackPanel { CacheLength = 0 }),
|
|
AutoScrollToSelectedItem = true,
|
|
IsVisible = false
|
|
};
|
|
|
|
target.Width = target.Height = 100;
|
|
var root = new TestRoot(target);
|
|
root.LayoutManager.ExecuteInitialLayoutPass();
|
|
|
|
// Select item 50
|
|
target.SelectedIndex = 50;
|
|
|
|
// Make visible
|
|
target.IsVisible = true;
|
|
target.UpdateLayout();
|
|
|
|
// Wait for dispatcher
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
target.UpdateLayout();
|
|
|
|
var scrollViewer = (ScrollViewer)target.VisualChildren[0];
|
|
var offset = scrollViewer.Offset.Y;
|
|
|
|
// Item 50 is at 50 * 50 = 2500.
|
|
// ListBox height is 100, so it should be visible if offset is between 2400 and 2500.
|
|
Assert.InRange(offset, 2400, 2500);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AutoScrollToSelectedItem_Should_Work_When_Ancestor_Becomes_Visible()
|
|
{
|
|
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
|
|
{
|
|
var items = Enumerable.Range(0, 100).Select(i => $"Item {i}").ToList();
|
|
|
|
var target = new ListBox
|
|
{
|
|
Template = new FuncControlTemplate(CreateListBoxTemplate),
|
|
ItemsSource = items,
|
|
ItemTemplate = new FuncDataTemplate<string>((_, _) => new TextBlock { Height = 50 }),
|
|
Height = 100,
|
|
ItemsPanel = new FuncTemplate<Panel?>(() => new VirtualizingStackPanel { CacheLength = 0 }),
|
|
AutoScrollToSelectedItem = true,
|
|
};
|
|
|
|
target.Width = target.Height = 100;
|
|
|
|
var host = new StackPanel
|
|
{
|
|
IsVisible = false,
|
|
Children =
|
|
{
|
|
target,
|
|
},
|
|
};
|
|
|
|
var root = new TestRoot(host);
|
|
root.LayoutManager.ExecuteInitialLayoutPass();
|
|
|
|
target.SelectedIndex = 50;
|
|
Assert.False(target.IsEffectivelyVisible);
|
|
|
|
host.IsVisible = true;
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
|
|
var scrollViewer = (ScrollViewer)target.VisualChildren[0];
|
|
var offset = scrollViewer.Offset.Y;
|
|
Assert.InRange(offset, 2400, 2500);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AutoScrollToSelectedItem_Executes_Within_Layout_Pass_When_Becoming_Visible()
|
|
{
|
|
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
|
|
|
|
var items = Enumerable.Range(0, 100).Select(i => $"Item {i}").ToList();
|
|
|
|
var target = new ListBox
|
|
{
|
|
Template = new FuncControlTemplate(CreateListBoxTemplate),
|
|
ItemsSource = items,
|
|
ItemTemplate = new FuncDataTemplate<string>((_, _) => new TextBlock { Height = 50 }),
|
|
Height = 100,
|
|
ItemsPanel = new FuncTemplate<Panel?>(() => new VirtualizingStackPanel { CacheLength = 0 }),
|
|
AutoScrollToSelectedItem = true,
|
|
IsVisible = false
|
|
};
|
|
|
|
target.Width = target.Height = 100;
|
|
var root = new TestRoot(target);
|
|
root.LayoutManager.ExecuteInitialLayoutPass();
|
|
|
|
target.SelectedIndex = 50;
|
|
target.IsVisible = true;
|
|
|
|
target.UpdateLayout();
|
|
|
|
var scrollViewer = (ScrollViewer)target.VisualChildren[0];
|
|
Assert.InRange(scrollViewer.Offset.Y, 2400, 2500);
|
|
}
|
|
|
|
private static FuncControlTemplate Template()
|
|
{
|
|
return new FuncControlTemplate<SelectingItemsControl>((control, scope) =>
|
|
new ItemsPresenter
|
|
{
|
|
Name = "itemsPresenter",
|
|
[~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty],
|
|
}.RegisterInNameScope(scope));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes and lays out <paramref name="target"/> the way it's done in XAML.
|
|
/// This is important for some tests, as the selection model isn't committed yet while doing so.
|
|
/// </summary>
|
|
private static void InitWithBeginEndInit(SelectingItemsControl target, IEnumerable items)
|
|
{
|
|
target.BeginInit();
|
|
target.Template = Template();
|
|
target.ItemsSource = items;
|
|
target.EndInit();
|
|
|
|
var root = new TestRoot(target);
|
|
root.LayoutManager.ExecuteInitialLayoutPass();
|
|
}
|
|
|
|
private class TestSelector : SelectingItemsControl
|
|
{
|
|
static TestSelector()
|
|
{
|
|
SelectionModeProperty.OverrideDefaultValue<TestSelector>(SelectionMode.AlwaysSelected);
|
|
}
|
|
}
|
|
|
|
private class ResetOnAdd : List<string>, INotifyCollectionChanged
|
|
{
|
|
public event NotifyCollectionChangedEventHandler? CollectionChanged;
|
|
|
|
public new void Add(string item)
|
|
{
|
|
base.Add(item);
|
|
CollectionChanged?.Invoke(
|
|
this,
|
|
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A selector with AlwaysSelected that hides the first N containers during preparation.
|
|
/// This simulates a MVVM scenario where an ItemContainerTheme binding sets IsVisible=false on some containers.
|
|
/// </summary>
|
|
private class AlwaysSelectedTestSelectorHidingFirstContainers(int hiddenCount) : SelectingItemsControl
|
|
{
|
|
static AlwaysSelectedTestSelectorHidingFirstContainers()
|
|
{
|
|
SelectionModeProperty.OverrideDefaultValue<AlwaysSelectedTestSelectorHidingFirstContainers>(SelectionMode.AlwaysSelected);
|
|
}
|
|
|
|
protected internal override void PrepareContainerForItemOverride(Control container, object? item, int index)
|
|
{
|
|
base.PrepareContainerForItemOverride(container, item, index);
|
|
if (index < hiddenCount)
|
|
container.IsVisible = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A selector with AlwaysSelected that disables the first N containers during preparation.
|
|
/// This simulates a MVVM scenario where an ItemContainerTheme binding sets IsEnabled=false on some containers.
|
|
/// </summary>
|
|
private class AlwaysSelectedTestSelectorDisablingFirstContainers(int disabledCount) : SelectingItemsControl
|
|
{
|
|
static AlwaysSelectedTestSelectorDisablingFirstContainers()
|
|
{
|
|
SelectionModeProperty.OverrideDefaultValue<AlwaysSelectedTestSelectorDisablingFirstContainers>(SelectionMode.AlwaysSelected);
|
|
}
|
|
|
|
protected internal override void PrepareContainerForItemOverride(Control container, object? item, int index)
|
|
{
|
|
base.PrepareContainerForItemOverride(container, item, index);
|
|
if (index < disabledCount)
|
|
container.IsEnabled = false;
|
|
}
|
|
}
|
|
|
|
private Control CreateListBoxTemplate(TemplatedControl parent, INameScope scope)
|
|
{
|
|
return new ScrollViewer
|
|
{
|
|
Name = "PART_ScrollViewer",
|
|
Template = new FuncControlTemplate(CreateScrollViewerTemplate),
|
|
Content = new ItemsPresenter
|
|
{
|
|
Name = "PART_ItemsPresenter",
|
|
[~ItemsPresenter.ItemsPanelProperty] =
|
|
((ListBox)parent).GetObservable(ItemsControl.ItemsPanelProperty).ToBinding(),
|
|
}.RegisterInNameScope(scope)
|
|
}.RegisterInNameScope(scope);
|
|
}
|
|
private Control CreateScrollViewerTemplate(TemplatedControl parent, INameScope scope)
|
|
{
|
|
return new ScrollContentPresenter
|
|
{
|
|
Name = "PART_ContentPresenter",
|
|
[~ContentPresenter.ContentProperty] =
|
|
parent.GetObservable(ContentControl.ContentProperty).ToBinding(),
|
|
}.RegisterInNameScope(scope);
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
|