@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel ;
using System.Linq ;
using Avalonia.Collections ;
using Avalonia.Controls.Mixins ;
using Avalonia.Controls.Presenters ;
using Avalonia.Controls.Primitives ;
using Avalonia.Controls.Selection ;
@ -134,7 +135,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
target . SelectedItems . Add ( "foo" ) ;
bool raised = false ;
target . PropertyChanged + = ( s , e ) = >
target . PropertyChanged + = ( s , e ) = >
raised | = e . Property . Name = = "SelectedIndex" | |
e . Property . Name = = "SelectedItem" ;
@ -152,9 +153,9 @@ namespace Avalonia.Controls.UnitTests.Primitives
target . SelectedItems . Add ( "foo" ) ;
bool raised = false ;
target . PropertyChanged + = ( s , e ) = >
raised | = e . Property . Name = = "SelectedIndex" & &
( int ) e . OldValue ! = = 0 & &
target . PropertyChanged + = ( s , e ) = >
raised | = e . Property . Name = = "SelectedIndex" & &
( int ) e . OldValue ! = = 0 & &
( int ) e . NewValue ! = = - 1 ;
target . SelectedItems . RemoveAt ( 0 ) ;
@ -708,14 +709,14 @@ namespace Avalonia.Controls.UnitTests.Primitives
using var app = Start ( ) ;
var items = new [ ]
{
new Item Container( ) ,
new Item Container( ) ,
new Test Container( ) ,
new Test Container( ) ,
} ;
var target = CreateTarget ( items : items ) ;
target . Items . Add ( new Item Container { IsSelected = true } ) ;
target . Items . Add ( new Item Container { IsSelected = true } ) ;
target . Items . Add ( new Test Container { IsSelected = true } ) ;
target . Items . Add ( new Test Container { IsSelected = true } ) ;
Assert . Equal ( 2 , target . SelectedIndex ) ;
Assert . Equal ( target . Items [ 2 ] , target . SelectedItem ) ;
@ -867,12 +868,169 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert . Equal ( 2 , raised ) ;
}
[Fact]
public void Can_Bind_Initial_Selected_State_Via_ItemContainerTheme ( )
{
using var app = Start ( ) ;
var items = new ItemViewModel [ ] { new ( "Item 0" , true ) , new ( "Item 1" , false ) , new ( "Item 2" , true ) } ;
var itemTheme = new ControlTheme ( typeof ( ContentPresenter ) )
{
Setters =
{
new Setter ( SelectingItemsControl . IsSelectedProperty , new Binding ( "IsSelected" ) ) ,
}
} ;
var target = CreateTarget ( itemsSource : items , itemContainerTheme : itemTheme ) ;
Assert . Equal ( new [ ] { 0 , 2 } , SelectedContainers ( target ) ) ;
Assert . Equal ( 0 , target . SelectedIndex ) ;
Assert . Equal ( items [ 0 ] , target . SelectedItem ) ;
Assert . Equal ( new [ ] { 0 , 2 } , target . Selection . SelectedIndexes ) ;
Assert . Equal ( new [ ] { items [ 0 ] , items [ 2 ] } , target . Selection . SelectedItems ) ;
}
[Fact]
public void Can_Bind_Initial_Selected_State_Via_Style ( )
{
using var app = Start ( ) ;
var items = new ItemViewModel [ ] { new ( "Item 0" , true ) , new ( "Item 1" , false ) , new ( "Item 2" , true ) } ;
var style = new Style ( x = > x . OfType < ContentPresenter > ( ) )
{
Setters =
{
new Setter ( SelectingItemsControl . IsSelectedProperty , new Binding ( "IsSelected" ) ) ,
}
} ;
var target = CreateTarget ( itemsSource : items , styles : new [ ] { style } ) ;
Assert . Equal ( new [ ] { 0 , 2 } , SelectedContainers ( target ) ) ;
Assert . Equal ( 0 , target . SelectedIndex ) ;
Assert . Equal ( items [ 0 ] , target . SelectedItem ) ;
Assert . Equal ( new [ ] { 0 , 2 } , target . Selection . SelectedIndexes ) ;
Assert . Equal ( new [ ] { items [ 0 ] , items [ 2 ] } , target . Selection . SelectedItems ) ;
}
[Fact]
public void Selection_State_Is_Updated_Via_IsSelected_Binding ( )
{
using var app = Start ( ) ;
var items = new ItemViewModel [ ] { new ( "Item 0" , true ) , new ( "Item 1" , false ) , new ( "Item 2" , true ) } ;
var itemTheme = new ControlTheme ( typeof ( TestContainer ) )
{
BasedOn = CreateTestContainerTheme ( ) ,
Setters =
{
new Setter ( SelectingItemsControl . IsSelectedProperty , new Binding ( "IsSelected" ) ) ,
}
} ;
// For the container selection state to be communicated back to the SelectingItemsControl
// we need a container which raises the SelectingItemsControl.IsSelectedChangedEvent when
// the IsSelected property changes.
var target = CreateTarget < TestSelectorWithContainers > (
itemsSource : items ,
itemContainerTheme : itemTheme ) ;
items [ 1 ] . IsSelected = true ;
Assert . Equal ( new [ ] { 0 , 1 , 2 } , SelectedContainers ( target ) ) ;
Assert . Equal ( 0 , target . SelectedIndex ) ;
Assert . Equal ( items [ 0 ] , target . SelectedItem ) ;
Assert . Equal ( new [ ] { 0 , 1 , 2 } , target . Selection . SelectedIndexes ) ;
Assert . Equal ( new [ ] { items [ 0 ] , items [ 1 ] , items [ 2 ] } , target . Selection . SelectedItems ) ;
items [ 0 ] . IsSelected = false ;
Assert . Equal ( new [ ] { 1 , 2 } , SelectedContainers ( target ) ) ;
Assert . Equal ( 1 , target . SelectedIndex ) ;
Assert . Equal ( items [ 1 ] , target . SelectedItem ) ;
Assert . Equal ( new [ ] { 1 , 2 } , target . Selection . SelectedIndexes ) ;
Assert . Equal ( new [ ] { items [ 1 ] , items [ 2 ] } , target . Selection . SelectedItems ) ;
}
[Fact]
public void Selection_State_Is_Written_Back_To_Item_Via_IsSelected_Binding ( )
{
using var app = Start ( ) ;
var items = new ItemViewModel [ ] { new ( "Item 0" , true ) , new ( "Item 1" , false ) , new ( "Item 2" , true ) } ;
var itemTheme = new ControlTheme ( typeof ( ContentPresenter ) )
{
Setters =
{
new Setter ( SelectingItemsControl . IsSelectedProperty , new Binding ( "IsSelected" ) ) ,
}
} ;
var target = CreateTarget ( itemsSource : items , itemContainerTheme : itemTheme ) ;
var container0 = Assert . IsAssignableFrom < Control > ( target . ContainerFromIndex ( 0 ) ) ;
var container1 = Assert . IsAssignableFrom < Control > ( target . ContainerFromIndex ( 1 ) ) ;
SelectingItemsControl . SetIsSelected ( container1 , true ) ;
Assert . True ( items [ 1 ] . IsSelected ) ;
SelectingItemsControl . SetIsSelected ( container0 , false ) ;
Assert . False ( items [ 0 ] . IsSelected ) ;
}
[Fact]
public void Selection_State_Change_On_Unrealized_Item_Is_Respected_With_IsSelected_Binding ( )
{
using var app = Start ( ) ;
var items = Enumerable . Range ( 0 , 1 0 0 ) . Select ( x = > new ItemViewModel ( $"Item {x}" , false ) ) . ToList ( ) ;
var itemTheme = new ControlTheme ( typeof ( ContentPresenter ) )
{
Setters =
{
new Setter ( SelectingItemsControl . IsSelectedProperty , new Binding ( "IsSelected" ) ) ,
new Setter ( Control . HeightProperty , 1 0 0.0 ) ,
}
} ;
// Create a SelectingItemsControl with a virtualizing stack panel.
var target = CreateTarget ( itemsSource : items , itemContainerTheme : itemTheme , virtualizing : true ) ;
var panel = Assert . IsType < VirtualizingStackPanel > ( target . ItemsPanelRoot ) ;
var scroll = panel . FindAncestorOfType < ScrollViewer > ( ) ! ;
// Scroll item 1 out of view.
scroll . Offset = new ( 0 , 1 0 0 0 ) ;
Layout ( target ) ;
Assert . Equal ( 1 0 , panel . FirstRealizedIndex ) ;
Assert . Equal ( 1 9 , panel . LastRealizedIndex ) ;
// Select item 1 now it's unrealized.
items [ 1 ] . IsSelected = true ;
// The SelectingItemsControl does not yet know anything about the selection change.
Assert . Empty ( SelectedContainers ( target ) ) ;
Assert . Equal ( - 1 , target . SelectedIndex ) ;
Assert . Null ( target . SelectedItem ) ;
Assert . Empty ( target . Selection . SelectedIndexes ) ;
Assert . Empty ( target . Selection . SelectedItems ) ;
// Scroll item 1 back into view.
scroll . Offset = new ( 0 , 0 ) ;
Layout ( target ) ;
// The item and container should be marked as selected.
Assert . True ( items [ 1 ] . IsSelected ) ;
Assert . Equal ( new [ ] { 1 } , SelectedContainers ( target ) ) ;
Assert . Equal ( 1 , target . SelectedIndex ) ;
Assert . Equal ( items [ 1 ] , target . SelectedItem ) ;
Assert . Equal ( new [ ] { 1 } , target . Selection . SelectedIndexes ) ;
Assert . Equal ( new [ ] { items [ 1 ] } , target . Selection . SelectedItems ) ;
}
private static IEnumerable < int > SelectedContainers ( SelectingItemsControl target )
{
Assert . NotNull ( target . ItemsPanel ) ;
return target . ItemsPanelRoot ! . Children
. Select ( x = > x . Classes . Contains ( ":selected" ) ? target . IndexFromContainer ( x ) : - 1 )
. Select ( x = > SelectingItemsControl . GetIsSelected ( x ) ? target . IndexFromContainer ( x ) : - 1 )
. Where ( x = > x ! = - 1 ) ;
}
@ -882,9 +1040,33 @@ namespace Avalonia.Controls.UnitTests.Primitives
IList ? itemsSource = null ,
ControlTheme ? itemContainerTheme = null ,
IDataTemplate ? itemTemplate = null ,
bool performLayout = true )
IEnumerable < Style > ? styles = null ,
bool performLayout = true ,
bool virtualizing = false )
{
var target = new TestSelector
return CreateTarget < TestSelector > (
dataContext : dataContext ,
items : items ,
itemsSource : itemsSource ,
itemContainerTheme : itemContainerTheme ,
itemTemplate : itemTemplate ,
styles : styles ,
performLayout : performLayout ,
virtualizing : virtualizing ) ;
}
private static T CreateTarget < T > (
object? dataContext = null ,
IList ? items = null ,
IList ? itemsSource = null ,
ControlTheme ? itemContainerTheme = null ,
IDataTemplate ? itemTemplate = null ,
IEnumerable < Style > ? styles = null ,
bool performLayout = true ,
bool virtualizing = false )
where T : TestSelector , new ( )
{
var target = new T
{
DataContext = dataContext ,
ItemContainerTheme = itemContainerTheme ,
@ -899,8 +1081,17 @@ namespace Avalonia.Controls.UnitTests.Primitives
target . Items . Add ( item ) ;
}
if ( virtualizing )
target . ItemsPanel = new FuncTemplate < Panel ? > ( ( ) = > new VirtualizingStackPanel ( ) ) ;
var root = CreateRoot ( target ) ;
if ( styles is not null )
{
foreach ( var style in styles )
root . Styles . Add ( style ) ;
}
if ( performLayout )
root . LayoutManager . ExecuteInitialLayoutPass ( ) ;
@ -914,6 +1105,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Resources =
{
{ typeof ( TestSelector ) , CreateTestSelectorControlTheme ( ) } ,
{ typeof ( TestContainer ) , CreateTestContainerTheme ( ) } ,
{ typeof ( ScrollViewer ) , CreateScrollViewerTheme ( ) } ,
} ,
Child = child ,
@ -951,6 +1143,28 @@ namespace Avalonia.Controls.UnitTests.Primitives
} ) ;
}
private static ControlTheme CreateTestContainerTheme ( )
{
return new ControlTheme ( typeof ( TestContainer ) )
{
Setters =
{
new Setter ( TreeView . TemplateProperty , CreateTestContainerTemplate ( ) ) ,
} ,
} ;
}
private static FuncControlTemplate CreateTestContainerTemplate ( )
{
return new FuncControlTemplate < TestContainer > ( ( parent , scope ) = >
new ContentPresenter
{
Name = "PART_ContentPresenter" ,
[!ContentPresenter.ContentProperty] = parent [ ! TestContainer . ContentProperty ] ,
[!ContentPresenter.ContentTemplateProperty] = parent [ ! TestContainer . ContentTemplateProperty ] ,
} . RegisterInNameScope ( scope ) ) ;
}
private static ControlTheme CreateScrollViewerTheme ( )
{
return new ControlTheme ( typeof ( ScrollViewer ) )
@ -1009,7 +1223,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
private class TestSelector : SelectingItemsControl
{
public static readonly new AvaloniaProperty < IList ? > SelectedItemsProperty =
public static readonly new AvaloniaProperty < IList ? > SelectedItemsProperty =
SelectingItemsControl . SelectedItemsProperty ;
public static readonly new DirectProperty < SelectingItemsControl , ISelectionModel > SelectionProperty =
SelectingItemsControl . SelectionProperty ;
@ -1043,6 +1257,66 @@ namespace Avalonia.Controls.UnitTests.Primitives
public void Toggle ( int index ) = > UpdateSelection ( index , true , false , true ) ;
}
private class TestSelectorWithContainers : TestSelector , IStyleable
{
Type IStyleable . StyleKey = > typeof ( TestSelector ) ;
protected internal override bool IsItemItsOwnContainerOverride ( Control item )
{
return item is TestContainer ;
}
protected internal override Control CreateContainerForItemOverride ( )
{
return new TestContainer ( ) ;
}
}
private class TestContainer : ContentControl , ISelectable
{
public static readonly StyledProperty < bool > IsSelectedProperty =
SelectingItemsControl . IsSelectedProperty . AddOwner < TestContainer > ( ) ;
static TestContainer ( )
{
SelectableMixin . Attach < TestContainer > ( SelectingItemsControl . IsSelectedProperty ) ;
}
public bool IsSelected
{
get = > GetValue ( IsSelectedProperty ) ;
set = > SetValue ( IsSelectedProperty , value ) ;
}
}
private class ItemViewModel : NotifyingBase
{
private bool _ isSelected ;
public ItemViewModel ( string value , bool isSelected = false )
{
Value = value ;
_ isSelected = isSelected ;
}
public string Value { get ; set ; }
public bool IsSelected
{
get = > _ isSelected ;
set
{
if ( _ isSelected ! = value )
{
_ isSelected = value ;
RaisePropertyChanged ( ) ;
}
}
}
public override string ToString ( ) = > Value ;
}
private class OldDataContextViewModel
{
public OldDataContextViewModel ( )
@ -1052,15 +1326,9 @@ namespace Avalonia.Controls.UnitTests.Primitives
Selection = new SelectionModel < string > ( ) ;
}
public List < string > Items { get ; }
public List < string > Items { get ; }
public List < string > SelectedItems { get ; }
public SelectionModel < string > Selection { get ; }
}
private class ItemContainer : Control , ISelectable
{
public string? Value { get ; set ; }
public bool IsSelected { get ; set ; }
}
}
}