@ -1,9 +1,12 @@
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 ;
@ -118,6 +121,235 @@ namespace Avalonia.Controls.UnitTests.Primitives
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 , 1 0 0 ) . Select ( i = > $"Item {i}" ) . ToList ( ) ;
var target = new ListBox
{
Template = new FuncControlTemplate ( CreateListBoxTemplate ) ,
ItemsSource = items ,
ItemTemplate = new FuncDataTemplate < string > ( ( _ , _ ) = > new TextBlock { Height = 5 0 } ) ,
Height = 1 0 0 ,
ItemsPanel = new FuncTemplate < Panel ? > ( ( ) = > new VirtualizingStackPanel { CacheLength = 0 } ) ,
AutoScrollToSelectedItem = true ,
IsVisible = false
} ;
target . Width = target . Height = 1 0 0 ;
var root = new TestRoot ( target ) ;
root . LayoutManager . ExecuteInitialLayoutPass ( ) ;
// Select item 50
target . SelectedIndex = 5 0 ;
// 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 , 2 4 0 0 , 2 5 0 0 ) ;
}
}
[Fact]
public void AutoScrollToSelectedItem_Should_Work_When_Ancestor_Becomes_Visible ( )
{
using ( UnitTestApplication . Start ( TestServices . MockPlatformRenderInterface ) )
{
var items = Enumerable . Range ( 0 , 1 0 0 ) . Select ( i = > $"Item {i}" ) . ToList ( ) ;
var target = new ListBox
{
Template = new FuncControlTemplate ( CreateListBoxTemplate ) ,
ItemsSource = items ,
ItemTemplate = new FuncDataTemplate < string > ( ( _ , _ ) = > new TextBlock { Height = 5 0 } ) ,
Height = 1 0 0 ,
ItemsPanel = new FuncTemplate < Panel ? > ( ( ) = > new VirtualizingStackPanel { CacheLength = 0 } ) ,
AutoScrollToSelectedItem = true ,
} ;
target . Width = target . Height = 1 0 0 ;
var host = new StackPanel
{
IsVisible = false ,
Children =
{
target ,
} ,
} ;
var root = new TestRoot ( host ) ;
root . LayoutManager . ExecuteInitialLayoutPass ( ) ;
target . SelectedIndex = 5 0 ;
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 , 2 4 0 0 , 2 5 0 0 ) ;
}
}
private static FuncControlTemplate Template ( )
{
return new FuncControlTemplate < SelectingItemsControl > ( ( control , scope ) = >
@ -128,6 +360,21 @@ namespace Avalonia.Controls.UnitTests.Primitives
} . 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 ( )
@ -148,5 +395,69 @@ namespace Avalonia.Controls.UnitTests.Primitives
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 ) ;
}
}
}