From fbc0eacdbfb09f6e83fdbeaf1ca4c11fdd7a9dd7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 Jun 2020 17:50:53 +0200 Subject: [PATCH 01/39] Renamed some `ItemsRepeater` events to be general use. They will be used/exposed on `IItemsPresenter` so will no longer be necessarily specific to `ItemsRepeater`. --- ...ventArgs.cs => ElementClearingEventArgs.cs} | 6 +++--- ...Args.cs => ElementIndexChangedEventArgs.cs} | 7 ++++--- ...ventArgs.cs => ElementPreparedEventArgs.cs} | 6 +++--- .../Repeater/ItemsRepeater.cs | 18 +++++++++--------- 4 files changed, 19 insertions(+), 18 deletions(-) rename src/Avalonia.Controls/{Repeater/ItemsRepeaterElementClearingEventArgs.cs => ElementClearingEventArgs.cs} (68%) rename src/Avalonia.Controls/{Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs => ElementIndexChangedEventArgs.cs} (80%) rename src/Avalonia.Controls/{Repeater/ItemsRepeaterElementPreparedEventArgs.cs => ElementPreparedEventArgs.cs} (78%) diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs b/src/Avalonia.Controls/ElementClearingEventArgs.cs similarity index 68% rename from src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs rename to src/Avalonia.Controls/ElementClearingEventArgs.cs index 75d50e52a61..03ac301753c 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs +++ b/src/Avalonia.Controls/ElementClearingEventArgs.cs @@ -8,11 +8,11 @@ namespace Avalonia.Controls { /// - /// Provides data for the event. + /// Provides notification that a recyclable element has been cleared to a recycling pool. /// - public class ItemsRepeaterElementClearingEventArgs : EventArgs + public class ElementClearingEventArgs : EventArgs { - internal ItemsRepeaterElementClearingEventArgs(IControl element) => Element = element; + public ElementClearingEventArgs(IControl element) => Element = element; /// /// Gets the element that is being cleared for re-use. diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs b/src/Avalonia.Controls/ElementIndexChangedEventArgs.cs similarity index 80% rename from src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs rename to src/Avalonia.Controls/ElementIndexChangedEventArgs.cs index 9f1c32bf64b..846582a6d73 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs +++ b/src/Avalonia.Controls/ElementIndexChangedEventArgs.cs @@ -8,11 +8,12 @@ namespace Avalonia.Controls { /// - /// Provides data for the event. + /// Provides notification that a recyclable element has been reused to represent a different + /// item index. /// - public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs + public class ElementIndexChangedEventArgs : EventArgs { - internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int oldIndex, int newIndex) + public ElementIndexChangedEventArgs(IControl element, int oldIndex, int newIndex) { Element = element; OldIndex = oldIndex; diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs b/src/Avalonia.Controls/ElementPreparedEventArgs.cs similarity index 78% rename from src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs rename to src/Avalonia.Controls/ElementPreparedEventArgs.cs index 5a30dbcf2ad..a8ba6b509ed 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs +++ b/src/Avalonia.Controls/ElementPreparedEventArgs.cs @@ -6,11 +6,11 @@ namespace Avalonia.Controls { /// - /// Provides data for the event. + /// Provides notification that a recyclable element has been prepared for use. /// - public class ItemsRepeaterElementPreparedEventArgs + public class ElementPreparedEventArgs { - internal ItemsRepeaterElementPreparedEventArgs(IControl element, int index) + public ElementPreparedEventArgs(IControl element, int index) { Element = element; Index = index; diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 8bc356bdec7..c1fee8c836d 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -63,9 +63,9 @@ public class ItemsRepeater : Panel private VirtualizingLayoutContext _layoutContext; private NotifyCollectionChangedEventArgs _processingItemsSourceChange; private bool _isLayoutInProgress; - private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs; - private ItemsRepeaterElementClearingEventArgs _elementClearingArgs; - private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs; + private ElementPreparedEventArgs _elementPreparedArgs; + private ElementClearingEventArgs _elementClearingArgs; + private ElementIndexChangedEventArgs _elementIndexChangedArgs; /// /// Initializes a new instance of the class. @@ -171,7 +171,7 @@ private LayoutContext LayoutContext /// outside the range of realized items. Elements are cleared when they become available /// for re-use. /// - public event EventHandler ElementClearing; + public event EventHandler ElementClearing; /// /// Occurs for each realized when the index for the item it @@ -186,7 +186,7 @@ private LayoutContext LayoutContext /// represents has changed. For example, when another item is added or removed in the data /// source, the index for items that come after in the ordering will be impacted. /// - public event EventHandler ElementIndexChanged; + public event EventHandler ElementIndexChanged; /// /// Occurs each time an element is prepared for use. @@ -195,7 +195,7 @@ private LayoutContext LayoutContext /// The prepared element might be newly created or an existing element that is being re- /// used. /// - public event EventHandler ElementPrepared; + public event EventHandler ElementPrepared; /// /// Retrieves the index of the item from the data source that corresponds to the specified @@ -522,7 +522,7 @@ internal void OnElementPrepared(IControl element, int index) { if (_elementPreparedArgs == null) { - _elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index); + _elementPreparedArgs = new ElementPreparedEventArgs(element, index); } else { @@ -539,7 +539,7 @@ internal void OnElementClearing(IControl element) { if (_elementClearingArgs == null) { - _elementClearingArgs = new ItemsRepeaterElementClearingEventArgs(element); + _elementClearingArgs = new ElementClearingEventArgs(element); } else { @@ -556,7 +556,7 @@ internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex { if (_elementIndexChangedArgs == null) { - _elementIndexChangedArgs = new ItemsRepeaterElementIndexChangedEventArgs(element, oldIndex, newIndex); + _elementIndexChangedArgs = new ElementIndexChangedEventArgs(element, oldIndex, newIndex); } else { From e5babc9cf9278c42f6ea03b55832cf7c57129a62 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 11 Aug 2020 17:43:24 +0200 Subject: [PATCH 02/39] Use ItemsRepeater as base for ItemsPresenter. Mostly working, although `Carousel` is currently non-functional. --- ...ctiveUI.Events.UnitTests.v3.ncrunchproject | 5 + Avalonia.sln | 26 +- Directory.Build.props | 1 + samples/ControlCatalog/SideBar.xaml | 6 +- samples/RenderDemo/SideBar.xaml | 6 +- samples/VirtualizationDemo/MainWindow.xaml | 6 - .../ViewModels/MainWindowViewModel.cs | 10 - src/Avalonia.Controls/Carousel.cs | 9 +- src/Avalonia.Controls/ComboBox.cs | 40 +- src/Avalonia.Controls/ContextMenu.cs | 1 - .../ElementIndexChangedEventArgs.cs | 3 +- .../Generators/IItemContainerGenerator.cs | 98 +- .../Generators/ItemContainerGenerator.cs | 226 +-- .../Generators/ItemContainerGenerator`1.cs | 84 +- .../Generators/MenuItemContainerGenerator.cs | 10 +- .../Generators/TabItemContainerGenerator.cs | 6 +- .../Generators/TreeItemContainerGenerator.cs | 165 +- .../ItemVirtualizationMode.cs | 18 - src/Avalonia.Controls/ItemsControl.cs | 528 +++--- src/Avalonia.Controls/ItemsSourceView.cs | 17 +- src/Avalonia.Controls/ListBox.cs | 42 +- src/Avalonia.Controls/Menu.cs | 8 +- src/Avalonia.Controls/MenuBase.cs | 20 +- src/Avalonia.Controls/MenuItem.cs | 26 +- .../Presenters/CarouselPresenter.cs | 276 ---- .../Presenters/IItemsPresenter.cs | 77 +- .../Presenters/IItemsPresenterHost.cs | 10 +- .../Presenters/ItemContainerSync.cs | 125 -- .../Presenters/ItemVirtualizer.cs | 303 ---- .../Presenters/ItemVirtualizerNone.cs | 119 -- .../Presenters/ItemVirtualizerSimple.cs | 582 ------- .../Presenters/ItemsPresenter.cs | 191 +-- .../Presenters/ItemsPresenterBase.cs | 252 --- .../Primitives/HeaderedItemsControl.cs | 16 + .../HeaderedSelectingItemsControl.cs | 25 +- .../Primitives/SelectingItemsControl.cs | 145 +- src/Avalonia.Controls/Primitives/TabStrip.cs | 8 +- src/Avalonia.Controls/Repeater/ViewManager.cs | 2 +- .../Repeater/ViewportManager.cs | 2 +- src/Avalonia.Controls/Selection/IndexPath.cs | 249 +++ src/Avalonia.Controls/StackPanel.cs | 4 - src/Avalonia.Controls/TabControl.cs | 91 +- .../TreeElementIndexChangedEventArgs.cs | 38 + .../TreeElementPreparedEventArgs.cs | 31 + src/Avalonia.Controls/TreeView.cs | 221 ++- src/Avalonia.Controls/TreeViewItem.cs | 99 +- .../Diagnostics/Views/ConsoleView.xaml | 3 +- .../Diagnostics/Views/TreePageView.xaml.cs | 9 +- src/Avalonia.Dialogs/ManagedFileChooser.xaml | 1 - src/Avalonia.Input/FocusManager.cs | 99 ++ src/Avalonia.Input/IFocusManager.cs | 7 + src/Avalonia.Input/NavigationDirection.cs | 16 +- src/Avalonia.Layout/LayoutManager.cs | 21 +- src/Avalonia.Themes.Default/Carousel.xaml | 6 +- src/Avalonia.Themes.Default/ComboBox.xaml | 5 +- src/Avalonia.Themes.Default/ContextMenu.xaml | 4 +- src/Avalonia.Themes.Default/ItemsControl.xaml | 4 +- src/Avalonia.Themes.Default/ListBox.xaml | 7 +- src/Avalonia.Themes.Default/Menu.xaml | 6 +- src/Avalonia.Themes.Default/MenuItem.xaml | 8 +- src/Avalonia.Themes.Default/TabControl.xaml | 16 +- src/Avalonia.Themes.Default/TabStrip.xaml | 11 +- src/Avalonia.Themes.Default/TreeView.xaml | 4 +- src/Avalonia.Themes.Default/TreeViewItem.xaml | 4 +- src/Avalonia.Themes.Fluent/Carousel.xaml | 6 +- src/Avalonia.Themes.Fluent/ComboBox.xaml | 7 +- src/Avalonia.Themes.Fluent/ContextMenu.xaml | 4 +- src/Avalonia.Themes.Fluent/ItemsControl.xaml | 6 +- src/Avalonia.Themes.Fluent/ListBox.xaml | 7 +- src/Avalonia.Themes.Fluent/Menu.xaml | 4 +- src/Avalonia.Themes.Fluent/MenuItem.xaml | 8 +- src/Avalonia.Themes.Fluent/TabControl.xaml | 4 +- src/Avalonia.Themes.Fluent/TabStrip.xaml | 11 +- src/Avalonia.Themes.Fluent/TreeView.xaml | 6 +- src/Avalonia.Themes.Fluent/TreeViewItem.xaml | 4 +- .../CarouselTests.cs | 582 +++---- .../Generators/ItemContainerGeneratorTests.cs | 163 -- .../ItemContainerGeneratorTypedTests.cs | 42 - .../ItemsControlTests.cs | 188 +-- .../ListBoxTests.cs | 384 ++--- .../ListBoxTests_Multiple.cs | 67 +- ...ts_Single.cs => ListBoxTests_Selection.cs} | 112 +- .../Presenters/CarouselPresenterTests.cs | 1436 ++++++++--------- .../Presenters/ItemsPresenterTests.cs | 351 ---- .../ItemsPresenterTests_Virtualization.cs | 374 ----- ...emsPresenterTests_Virtualization_Simple.cs | 1110 ------------- .../Primitives/SelectingItemsControlTests.cs | 255 ++- .../SelectingItemsControlTests_AutoSelect.cs | 59 +- .../SelectingItemsControlTests_Multiple.cs | 491 +++--- .../TabControlTests.cs | 44 +- .../TreeViewTests.cs | 919 +++++------ tests/Avalonia.LeakTests/ControlTests.cs | 2 +- .../CompiledBindingExtensionTests.cs | 4 +- .../Xaml/DataTemplateTests.cs | 3 +- .../AutoDataTemplateBindingHookTest.cs | 16 +- 95 files changed, 3670 insertions(+), 7457 deletions(-) create mode 100644 .ncrunch/Avalonia.ReactiveUI.Events.UnitTests.v3.ncrunchproject delete mode 100644 src/Avalonia.Controls/ItemVirtualizationMode.cs delete mode 100644 src/Avalonia.Controls/Presenters/CarouselPresenter.cs delete mode 100644 src/Avalonia.Controls/Presenters/ItemContainerSync.cs delete mode 100644 src/Avalonia.Controls/Presenters/ItemVirtualizer.cs delete mode 100644 src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs delete mode 100644 src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs delete mode 100644 src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs create mode 100644 src/Avalonia.Controls/Selection/IndexPath.cs create mode 100644 src/Avalonia.Controls/TreeElementIndexChangedEventArgs.cs create mode 100644 src/Avalonia.Controls/TreeElementPreparedEventArgs.cs delete mode 100644 tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs delete mode 100644 tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs rename tests/Avalonia.Controls.UnitTests/{ListBoxTests_Single.cs => ListBoxTests_Selection.cs} (62%) delete mode 100644 tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs delete mode 100644 tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs delete mode 100644 tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs diff --git a/.ncrunch/Avalonia.ReactiveUI.Events.UnitTests.v3.ncrunchproject b/.ncrunch/Avalonia.ReactiveUI.Events.UnitTests.v3.ncrunchproject new file mode 100644 index 00000000000..319cd523cec --- /dev/null +++ b/.ncrunch/Avalonia.ReactiveUI.Events.UnitTests.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/Avalonia.sln b/Avalonia.sln index 66777f33eb9..5d117d453be 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -222,7 +222,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "sr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.ReactiveUI.Events.UnitTests", "tests\Avalonia.ReactiveUI.Events.UnitTests\Avalonia.ReactiveUI.Events.UnitTests.csproj", "{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}" EndProject @@ -2040,6 +2040,30 @@ Global {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.Build.0 = Release|Any CPU {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhone.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhone.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|Any CPU.Build.0 = Release|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhone.ActiveCfg = Release|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhone.Build.0 = Release|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Directory.Build.props b/Directory.Build.props index b41f8c488e3..023213a47fb 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,5 +2,6 @@ $(MSBuildThisFileDirectory)build-intermediate/nuget $(MSBuildThisFileDirectory)\src\tools\Avalonia.Designer.HostApp\bin\$(Configuration)\netcoreapp2.0\Avalonia.Designer.HostApp.dll + false diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 7c911e91e9e..b8d5d8623cc 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -13,6 +13,9 @@ + + + diff --git a/samples/RenderDemo/SideBar.xaml b/samples/RenderDemo/SideBar.xaml index fd23067f611..f48db372699 100644 --- a/samples/RenderDemo/SideBar.xaml +++ b/samples/RenderDemo/SideBar.xaml @@ -18,9 +18,9 @@ Background="{TemplateBinding Background}"> + Items="{TemplateBinding ItemsView}" + ItemTemplate="{TemplateBinding ItemTemplate}" + Layout="{TemplateBinding Layout}"> - - - - - diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs index 852c01399fc..d589a61c08b 100644 --- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs @@ -21,7 +21,6 @@ internal class MainWindowViewModel : ReactiveObject private ScrollBarVisibility _horizontalScrollBarVisibility = ScrollBarVisibility.Auto; private ScrollBarVisibility _verticalScrollBarVisibility = ScrollBarVisibility.Auto; private Orientation _orientation = Orientation.Vertical; - private ItemVirtualizationMode _virtualizationMode = ItemVirtualizationMode.Simple; public MainWindowViewModel() { @@ -81,15 +80,6 @@ public ScrollBarVisibility VerticalScrollBarVisibility public IEnumerable ScrollBarVisibilities => Enum.GetValues(typeof(ScrollBarVisibility)).Cast(); - public ItemVirtualizationMode VirtualizationMode - { - get { return _virtualizationMode; } - set { this.RaiseAndSetIfChanged(ref _virtualizationMode, value); } - } - - public IEnumerable VirtualizationModes => - Enum.GetValues(typeof(ItemVirtualizationMode)).Cast(); - public ReactiveCommand AddItemCommand { get; private set; } public ReactiveCommand RecreateCommand { get; private set; } public ReactiveCommand RemoveItemCommand { get; private set; } diff --git a/src/Avalonia.Controls/Carousel.cs b/src/Avalonia.Controls/Carousel.cs index 4cacf51fa1c..d376bd8179f 100644 --- a/src/Avalonia.Controls/Carousel.cs +++ b/src/Avalonia.Controls/Carousel.cs @@ -23,20 +23,13 @@ public class Carousel : SelectingItemsControl public static readonly StyledProperty PageTransitionProperty = AvaloniaProperty.Register(nameof(PageTransition)); - /// - /// The default value of for - /// . - /// - private static readonly ITemplate PanelTemplate = - new FuncTemplate(() => new Panel()); - /// /// Initializes static members of the class. /// static Carousel() { SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); - ItemsPanelProperty.OverrideDefaultValue(PanelTemplate); + ////ItemsPanelProperty.OverrideDefaultValue(PanelTemplate); } /// diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 27313b0b4c2..1cbe101981c 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -19,12 +19,6 @@ namespace Avalonia.Controls /// public class ComboBox : SelectingItemsControl { - /// - /// The default value for the property. - /// - private static readonly FuncTemplate DefaultPanel = - new FuncTemplate(() => new VirtualizingStackPanel()); - /// /// Defines the property. /// @@ -46,12 +40,6 @@ public class ComboBox : SelectingItemsControl public static readonly DirectProperty SelectionBoxItemProperty = AvaloniaProperty.RegisterDirect(nameof(SelectionBoxItem), o => o.SelectionBoxItem); - /// - /// Defines the property. - /// - public static readonly StyledProperty VirtualizationModeProperty = - ItemsPresenter.VirtualizationModeProperty.AddOwner(); - /// /// Defines the property. /// @@ -86,7 +74,6 @@ public class ComboBox : SelectingItemsControl /// static ComboBox() { - ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); FocusableProperty.OverrideDefaultValue(true); SelectedItemProperty.Changed.AddClassHandler((x,e) => x.SelectedItemChanged(e)); KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e), Interactivity.RoutingStrategies.Tunnel); @@ -137,15 +124,6 @@ public IBrush PlaceholderForeground set { SetValue(PlaceholderForegroundProperty, value); } } - /// - /// Gets or sets the virtualization mode for the items. - /// - public ItemVirtualizationMode VirtualizationMode - { - get { return GetValue(VirtualizationModeProperty); } - set { SetValue(VirtualizationModeProperty, value); } - } - /// /// Gets or sets the horizontal alignment of the content within the control. /// @@ -221,7 +199,8 @@ protected override void OnKeyDown(KeyEventArgs e) else if (IsDropDownOpen && SelectedIndex < 0 && ItemCount > 0 && (e.Key == Key.Up || e.Key == Key.Down)) { - var firstChild = Presenter?.Panel?.Children.FirstOrDefault(c => CanFocus(c)); + var panel = Presenter as IPanel; + var firstChild = panel?.Children.FirstOrDefault(c => CanFocus(c)); if (firstChild != null) { FocusManager.Instance?.Focus(firstChild, NavigationMethod.Directional); @@ -343,12 +322,12 @@ private void TryFocusSelectedItem() var selectedIndex = SelectedIndex; if (IsDropDownOpen && selectedIndex != -1) { - var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); + var container = TryGetContainer(selectedIndex); if (container == null && SelectedIndex != -1) { ScrollIntoView(Selection.SelectedIndex); - container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); + container = TryGetContainer(selectedIndex); } if (container != null && CanFocus(container)) @@ -395,11 +374,16 @@ private void UpdateSelectionBoxItem(object item) private void SelectFocusedItem() { - foreach (ItemContainerInfo dropdownItem in ItemContainerGenerator.Containers) + if (Presenter is null) + { + return; + } + + foreach (var element in Presenter.RealizedElements) { - if (dropdownItem.ContainerControl.IsFocused) + if (element.IsFocused) { - SelectedIndex = dropdownItem.Index; + SelectedIndex = GetContainerIndex(element); break; } } diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index c4df5c1815a..4001870fc27 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -96,7 +96,6 @@ public ContextMenu(IMenuInteractionHandler interactionHandler) /// static ContextMenu() { - ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); PlacementModeProperty.OverrideDefaultValue(PlacementMode.Pointer); ContextMenuProperty.Changed.Subscribe(ContextMenuChanged); } diff --git a/src/Avalonia.Controls/ElementIndexChangedEventArgs.cs b/src/Avalonia.Controls/ElementIndexChangedEventArgs.cs index 846582a6d73..1d72c667e7b 100644 --- a/src/Avalonia.Controls/ElementIndexChangedEventArgs.cs +++ b/src/Avalonia.Controls/ElementIndexChangedEventArgs.cs @@ -8,8 +8,7 @@ namespace Avalonia.Controls { /// - /// Provides notification that a recyclable element has been reused to represent a different - /// item index. + /// Provides notification that the index for a recyclable element has changed. /// public class ElementIndexChangedEventArgs : EventArgs { diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs index 6220a20a822..6a544a32567 100644 --- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs @@ -1,102 +1,44 @@ using System; -using System.Collections.Generic; -using Avalonia.Controls.Templates; + +#nullable enable namespace Avalonia.Controls.Generators { /// /// Creates containers for items and maintains a list of created containers. /// - public interface IItemContainerGenerator + public interface IItemContainerGenerator : IElementFactory { /// - /// Gets the currently realized containers. - /// - IEnumerable Containers { get; } - - /// - /// Gets or sets the data template used to display the items in the control. - /// - IDataTemplate ItemTemplate { get; set; } - - /// - /// Gets the ContainerType, or null if its an untyped ContainerGenerator. - /// - Type ContainerType { get; } - - /// - /// Signaled whenever new containers are materialized. - /// - event EventHandler Materialized; - - /// - /// Event raised whenever containers are dematerialized. - /// - event EventHandler Dematerialized; - - /// - /// Event raised whenever containers are recycled. - /// - event EventHandler Recycled; - - /// - /// Creates a container control for an item. - /// - /// - /// The index of the item of data in the control's items. - /// - /// The item. - /// The created controls. - ItemContainerInfo Materialize(int index, object item); - - /// - /// Removes a set of created containers. + /// Gets the that the generator belongs to. /// - /// - /// The index of the first item in the control's items. - /// - /// The the number of items to remove. - /// The removed containers. - IEnumerable Dematerialize(int startingIndex, int count); - - /// - /// Inserts space for newly inserted containers in the index. - /// - /// The index at which space should be inserted. - /// The number of blank spaces to create. - void InsertSpace(int index, int count); - - /// - /// Removes a set of created containers and updates the index of later containers to fill - /// the gap. - /// - /// - /// The index of the first item in the control's items. - /// - /// The the number of items to remove. - /// The removed containers. - IEnumerable RemoveRange(int startingIndex, int count); - - bool TryRecycle(int oldIndex, int newIndex, object item); - - /// - /// Clears all created containers and returns the removed controls. - /// - /// The removed controls. - IEnumerable Clear(); + ItemsControl Owner { get; } + } + public static class ItemContainerGeneratorExtensions + { /// /// Gets the container control representing the item with the specified index. /// + /// The generator. /// The index. /// The container, or null if no container created. - IControl ContainerFromIndex(int index); + [Obsolete("Use ItemsControl.TryGetContainer")] + public static IControl? ContainerFromIndex(this IItemContainerGenerator generator, int index) + { + return generator.Owner.TryGetContainer(index); + } /// /// Gets the index of the specified container control. /// + /// The generator. /// The container. /// The index of the container, or -1 if not found. - int IndexFromContainer(IControl container); + [Obsolete("Use ItemsControl.GetContainerIndex")] + public static int IndexFromContainer(this IItemContainerGenerator generator, IControl container) + { + return generator.Owner.GetContainerIndex(container); + } } } diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index f16834f6e16..b6d9fa691d2 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -1,10 +1,9 @@ using System; -using System.Collections.Generic; -using System.Linq; using Avalonia.Controls.Presenters; -using Avalonia.Controls.Templates; using Avalonia.Data; +#nullable enable + namespace Avalonia.Controls.Generators { /// @@ -12,230 +11,67 @@ namespace Avalonia.Controls.Generators /// public class ItemContainerGenerator : IItemContainerGenerator { - private SortedDictionary _containers = new SortedDictionary(); + private RecyclePool _recyclePool = new RecyclePool(); - /// - /// Initializes a new instance of the class. - /// - /// The owner control. - public ItemContainerGenerator(IControl owner) + public ItemContainerGenerator(ItemsControl owner) { - Contract.Requires(owner != null); - - Owner = owner; + Owner = owner ?? throw new ArgumentNullException(nameof(owner)); } - /// - public IEnumerable Containers => _containers.Values; - - /// - public event EventHandler Materialized; - - /// - public event EventHandler Dematerialized; + public ItemsControl Owner { get; } + public bool SupportsRecycling => false; - /// - public event EventHandler Recycled; + public bool Match(object data) => true; - /// - /// Gets or sets the data template used to display the items in the control. - /// - public IDataTemplate ItemTemplate { get; set; } - - /// - /// Gets the owner control. - /// - public IControl Owner { get; } - - /// - public virtual Type ContainerType => null; - - /// - public ItemContainerInfo Materialize(int index, object item) + public IControl Build(object param) { - var container = new ItemContainerInfo(CreateContainer(item), item, index); - - _containers.Add(container.Index, container); - Materialized?.Invoke(this, new ItemContainerEventArgs(container)); - - return container; + return GetElement(new ElementFactoryGetArgs { Data = param }); } - /// - public virtual IEnumerable Dematerialize(int startingIndex, int count) + public IControl GetElement(ElementFactoryGetArgs args) { - var result = new List(); + var result = _recyclePool.TryGetElement(string.Empty, args.Parent); - for (int i = startingIndex; i < startingIndex + count; ++i) + if (result is null) { - result.Add(_containers[i]); - _containers.Remove(i); - } - - Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result)); - - return result; - } + result = CreateContainer(args); - /// - public virtual void InsertSpace(int index, int count) - { - if (count > 0) - { - var toMove = _containers.Where(x => x.Key >= index) - .OrderByDescending(x => x.Key) - .ToList(); - - foreach (var i in toMove) + if (result.Parent == null) { - _containers.Remove(i.Key); - i.Value.Index += count; - _containers.Add(i.Value.Index, i.Value); - } - } - } - - /// - public virtual IEnumerable RemoveRange(int startingIndex, int count) - { - var result = new List(); - - if (count > 0) - { - for (var i = startingIndex; i < startingIndex + count; ++i) - { - ItemContainerInfo found; - - if (_containers.TryGetValue(i, out found)) - { - result.Add(found); - } - - _containers.Remove(i); - } - - var toMove = _containers.Where(x => x.Key >= startingIndex) - .OrderBy(x => x.Key).ToList(); - - foreach (var i in toMove) - { - _containers.Remove(i.Key); - i.Value.Index -= count; - _containers.Add(i.Value.Index, i.Value); - } - - Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result)); - - if (toMove.Count > 0) - { - var containers = toMove.Select(x => x.Value).ToList(); - Recycled?.Invoke(this, new ItemContainerEventArgs(containers[0].Index, containers)); + ((ISetLogicalParent)result).SetParent(Owner); } } return result; } - /// - public virtual bool TryRecycle(int oldIndex, int newIndex, object item) => false; - - /// - public virtual IEnumerable Clear() + public void RecycleElement(ElementFactoryRecycleArgs args) { - var result = Containers.ToList(); - _containers.Clear(); - - if (result.Count > 0) - { - Dematerialized?.Invoke(this, new ItemContainerEventArgs(0, result)); - } - - return result; + _recyclePool.PutElement(args.Element, string.Empty, args.Parent); } - /// - public IControl ContainerFromIndex(int index) + protected virtual IControl CreateContainer(ElementFactoryGetArgs args) { - ItemContainerInfo result; - _containers.TryGetValue(index, out result); - return result?.ContainerControl; - } - - /// - public int IndexFromContainer(IControl container) - { - foreach (var i in _containers) + if (args.Data is IControl c) { - if (i.Value.ContainerControl == container) - { - return i.Key; - } + return c; } - return -1; - } - - /// - /// Creates the container for an item. - /// - /// The item. - /// The created container control. - protected virtual IControl CreateContainer(object item) - { - var result = item as IControl; + var result = new ContentPresenter(); + result.Bind( + ContentPresenter.ContentProperty, + result.GetBindingObservable(Control.DataContextProperty), + BindingPriority.Style); - if (result == null) + if (Owner.ItemTemplate is object) { - result = new ContentPresenter(); - result.SetValue(ContentPresenter.ContentProperty, item, BindingPriority.Style); - - if (ItemTemplate != null) - { - result.SetValue( - ContentPresenter.ContentTemplateProperty, - ItemTemplate, - BindingPriority.TemplatedParent); - } + result.SetValue( + ContentPresenter.ContentTemplateProperty, + Owner.ItemTemplate, + BindingPriority.TemplatedParent); } return result; } - - /// - /// Moves a container. - /// - /// The old index. - /// The new index. - /// The new item. - /// The container info. - protected ItemContainerInfo MoveContainer(int oldIndex, int newIndex, object item) - { - var container = _containers[oldIndex]; - container.Index = newIndex; - container.Item = item; - _containers.Remove(oldIndex); - _containers.Add(newIndex, container); - return container; - } - - /// - /// Gets all containers with an index that fall within a range. - /// - /// The first index. - /// The number of elements in the range. - /// The containers. - protected IEnumerable GetContainerRange(int index, int count) - { - return _containers.Where(x => x.Key >= index && x.Key < index + count).Select(x => x.Value); - } - - /// - /// Raises the event. - /// - /// The event args. - protected void RaiseRecycled(ItemContainerEventArgs e) - { - Recycled?.Invoke(this, e); - } } } diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs index 3a098593dc1..5fea8c4c295 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs @@ -2,6 +2,8 @@ using Avalonia.Controls.Templates; using Avalonia.Data; +#nullable enable + namespace Avalonia.Controls.Generators { /// @@ -17,85 +19,45 @@ namespace Avalonia.Controls.Generators /// The container's Content property. /// The container's ContentTemplate property. public ItemContainerGenerator( - IControl owner, - AvaloniaProperty contentProperty, - AvaloniaProperty contentTemplateProperty) + ItemsControl owner, + AvaloniaProperty contentProperty, + AvaloniaProperty contentTemplateProperty) : base(owner) { - Contract.Requires(owner != null); - Contract.Requires(contentProperty != null); - - ContentProperty = contentProperty; - ContentTemplateProperty = contentTemplateProperty; + ContentProperty = contentProperty ?? throw new ArgumentNullException(nameof(contentProperty)); + ContentTemplateProperty = contentTemplateProperty ?? + throw new ArgumentNullException(nameof(contentTemplateProperty)); } - /// - public override Type ContainerType => typeof(T); - /// /// Gets the container's Content property. /// - protected AvaloniaProperty ContentProperty { get; } + protected AvaloniaProperty ContentProperty { get; } /// /// Gets the container's ContentTemplate property. /// - protected AvaloniaProperty ContentTemplateProperty { get; } + protected AvaloniaProperty ContentTemplateProperty { get; } - /// - protected override IControl CreateContainer(object item) + protected override IControl CreateContainer(ElementFactoryGetArgs args) { - var container = item as T; - - if (item == null) - { - return null; - } - else if (container != null) + if (args.Data is T t) { - return container; + return t; } - else - { - var result = new T(); - - if (ContentTemplateProperty != null) - { - result.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style); - } - - result.SetValue(ContentProperty, item, BindingPriority.Style); - if (!(item is IControl)) - { - result.DataContext = item; - } - - return result; - } - } - - /// - public override bool TryRecycle(int oldIndex, int newIndex, object item) - { - var container = ContainerFromIndex(oldIndex); - - if (container == null) - { - throw new IndexOutOfRangeException("Could not recycle container: not materialized."); - } - - container.SetValue(ContentProperty, item); - - if (!(item is IControl)) - { - container.DataContext = item; - } + var result = new T(); - var info = MoveContainer(oldIndex, newIndex, item); - RaiseRecycled(new ItemContainerEventArgs(info)); + result.Bind( + ContentProperty, + result.GetBindingObservable(Control.DataContextProperty), + BindingPriority.Style); + result.Bind( + ContentTemplateProperty, + Owner.GetBindingObservable(ItemsControl.ItemTemplateProperty), + BindingPriority.Style); - return true; + return result; } } } diff --git a/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs index d3cf70e0f80..5f832437381 100644 --- a/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs @@ -6,16 +6,14 @@ public class MenuItemContainerGenerator : ItemContainerGenerator /// Initializes a new instance of the class. /// /// The owner control. - public MenuItemContainerGenerator(IControl owner) - : base(owner, MenuItem.HeaderProperty, null) + public MenuItemContainerGenerator(ItemsControl owner) + : base(owner, MenuItem.HeaderProperty, MenuItem.HeaderTemplateProperty) { } - /// - protected override IControl CreateContainer(object item) + protected override IControl CreateContainer(ElementFactoryGetArgs args) { - var separator = item as Separator; - return separator != null ? separator : base.CreateContainer(item); + return args.Data is Separator s ? s : base.CreateContainer(args); } } } diff --git a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs index d2e05ee1363..a8089a22b52 100644 --- a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs @@ -12,9 +12,9 @@ public TabItemContainerGenerator(TabControl owner) public new TabControl Owner { get; } - protected override IControl CreateContainer(object item) + protected override IControl CreateContainer(ElementFactoryGetArgs args) { - var tabItem = (TabItem)base.CreateContainer(item); + var tabItem = (TabItem)base.CreateContainer(args); tabItem[~TabControl.TabStripPlacementProperty] = Owner[~TabControl.TabStripPlacementProperty]; @@ -25,7 +25,7 @@ protected override IControl CreateContainer(object item) if (tabItem.Header == null) { - if (item is IHeadered headered) + if (args.Data is IHeadered headered) { tabItem.Header = headered.Header; } diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index 9e65ef5f810..3b8deae9f7d 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -1,43 +1,30 @@ -using System; -using System.Collections.Generic; +using System; +using System.Collections; using System.Linq; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.LogicalTree; +#nullable enable + namespace Avalonia.Controls.Generators { - /// - /// Creates containers for tree items and maintains a list of created containers. - /// - /// The type of the container. - public class TreeItemContainerGenerator : ItemContainerGenerator, ITreeItemContainerGenerator + public class TreeItemContainerGenerator : ItemContainerGenerator, ITreeItemContainerGenerator where T : class, IControl, new() { - private TreeView _treeView; + private TreeView? _treeView; - /// - /// Initializes a new instance of the class. - /// - /// The owner control. - /// The container's Content property. - /// The container's ContentTemplate property. - /// The container's Items property. - /// The container's IsExpanded property. public TreeItemContainerGenerator( - IControl owner, - AvaloniaProperty contentProperty, - AvaloniaProperty contentTemplateProperty, - AvaloniaProperty itemsProperty, - AvaloniaProperty isExpandedProperty) - : base(owner, contentProperty, contentTemplateProperty) + ItemsControl owner, + AvaloniaProperty headerProperty, + AvaloniaProperty headerTemplateProperty, + AvaloniaProperty itemsProperty, + AvaloniaProperty isExpandedProperty) + : base(owner) { - Contract.Requires(owner != null); - Contract.Requires(contentProperty != null); - Contract.Requires(itemsProperty != null); - Contract.Requires(isExpandedProperty != null); - - ItemsProperty = itemsProperty; + HeaderProperty = headerProperty ?? throw new ArgumentNullException(nameof(headerProperty)); + HeaderTemplateProperty = headerTemplateProperty ?? throw new ArgumentNullException(nameof(headerTemplateProperty)); + ItemsProperty = itemsProperty ?? throw new ArgumentNullException(nameof(itemsProperty)); IsExpandedProperty = isExpandedProperty; UpdateIndex(); } @@ -45,77 +32,27 @@ public TreeItemContainerGenerator( /// /// Gets the container index for the tree. /// - public TreeContainerIndex Index { get; private set; } + public TreeContainerIndex? Index { get; private set; } /// - /// Gets the item container's Items property. + /// Gets the container's Header property. /// - protected AvaloniaProperty ItemsProperty { get; } + protected AvaloniaProperty HeaderProperty { get; } /// - /// Gets the item container's IsExpanded property. + /// Gets the container's HeaderTemplate property. /// - protected AvaloniaProperty IsExpandedProperty { get; } - - /// - protected override IControl CreateContainer(object item) - { - var container = item as T; - - if (item == null) - { - return null; - } - else if (container != null) - { - Index?.Add(item, container); - return container; - } - else - { - var template = GetTreeDataTemplate(item, ItemTemplate); - var result = new T(); - - result.SetValue(ContentProperty, template.Build(item), BindingPriority.Style); - - var itemsSelector = template.ItemsSelector(item); - - if (itemsSelector != null) - { - BindingOperations.Apply(result, ItemsProperty, itemsSelector, null); - } - - if (!(item is IControl)) - { - result.DataContext = item; - } - - Index?.Add(item, result); - - return result; - } - } - - public override IEnumerable Clear() - { - var items = base.Clear(); - Index?.Remove(0, items); - return items; - } - - public override IEnumerable Dematerialize(int startingIndex, int count) - { - Index?.Remove(startingIndex, GetContainerRange(startingIndex, count)); - return base.Dematerialize(startingIndex, count); - } + protected AvaloniaProperty HeaderTemplateProperty { get; } - public override IEnumerable RemoveRange(int startingIndex, int count) - { - Index?.Remove(startingIndex, GetContainerRange(startingIndex, count)); - return base.RemoveRange(startingIndex, count); - } + /// + /// Gets the item container's Items property. + /// + protected AvaloniaProperty ItemsProperty { get; } - public override bool TryRecycle(int oldIndex, int newIndex, object item) => false; + /// + /// Gets the item container's IsExpanded property. + /// + protected AvaloniaProperty IsExpandedProperty { get; } public void UpdateIndex() { @@ -127,30 +64,56 @@ public void UpdateIndex() else { var treeView = Owner.GetSelfAndLogicalAncestors().OfType().FirstOrDefault(); - + if (treeView != _treeView) { - Clear(); Index = treeView?.ItemContainerGenerator?.Index; _treeView = treeView; } } } - - class WrapperTreeDataTemplate : ITreeDataTemplate + protected override IControl CreateContainer(ElementFactoryGetArgs args) { - private readonly IDataTemplate _inner; - public WrapperTreeDataTemplate(IDataTemplate inner) => _inner = inner; - public IControl Build(object param) => _inner.Build(param); - public bool Match(object data) => _inner.Match(data); - public InstancedBinding ItemsSelector(object item) => null; + if (args.Data is T c) + { + return c; + } + + var result = new T(); + var template = GetTreeDataTemplate(args.Data, Owner.ItemTemplate); + var itemsSelector = template.ItemsSelector(args.Data); + + result.Bind( + HeaderProperty, + result.GetBindingObservable(Control.DataContextProperty), + BindingPriority.Style); + result.Bind( + HeaderTemplateProperty, + Owner.GetBindingObservable(ItemsControl.ItemTemplateProperty), + BindingPriority.Style); + + if (itemsSelector != null) + { + BindingOperations.Apply(result, ItemsProperty, itemsSelector, null); + } + + return result; } - private ITreeDataTemplate GetTreeDataTemplate(object item, IDataTemplate primary) + private ITreeDataTemplate GetTreeDataTemplate(object item, IDataTemplate? primary) { var template = Owner.FindDataTemplate(item, primary) ?? FuncDataTemplate.Default; var treeTemplate = template as ITreeDataTemplate ?? new WrapperTreeDataTemplate(template); return treeTemplate; } + + class WrapperTreeDataTemplate : ITreeDataTemplate + { + private readonly IDataTemplate _inner; + public WrapperTreeDataTemplate(IDataTemplate inner) => _inner = inner; + public IControl Build(object param) => _inner.Build(param); + public bool Match(object data) => _inner.Match(data); + public InstancedBinding? ItemsSelector(object item) => null; + } } } diff --git a/src/Avalonia.Controls/ItemVirtualizationMode.cs b/src/Avalonia.Controls/ItemVirtualizationMode.cs deleted file mode 100644 index 1950bb7f204..00000000000 --- a/src/Avalonia.Controls/ItemVirtualizationMode.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Avalonia.Controls -{ - /// - /// Describes the item virtualization method to use for a list. - /// - public enum ItemVirtualizationMode - { - /// - /// Do not virtualize items. - /// - None, - - /// - /// Virtualize items without smooth scrolling. - /// - Simple, - } -} diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index a3dfe336416..a83c16f003e 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -8,10 +8,13 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; +using Avalonia.Data; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Metadata; -using Avalonia.VisualTree; + +#nullable enable namespace Avalonia.Controls { @@ -20,17 +23,11 @@ namespace Avalonia.Controls /// public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener { - /// - /// The default value for the property. - /// - private static readonly FuncTemplate DefaultPanel = - new FuncTemplate(() => new StackPanel()); - /// /// Defines the property. /// - public static readonly DirectProperty ItemsProperty = - AvaloniaProperty.RegisterDirect(nameof(Items), o => o.Items, (o, v) => o.Items = v); + public static readonly DirectProperty ItemsProperty = + AvaloniaProperty.RegisterDirect(nameof(Items), o => o.Items, (o, v) => o.Items = v); /// /// Defines the property. @@ -39,71 +36,84 @@ public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionCh AvaloniaProperty.RegisterDirect(nameof(ItemCount), o => o.ItemCount); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty> ItemsPanelProperty = - AvaloniaProperty.Register>(nameof(ItemsPanel), DefaultPanel); + public static readonly StyledProperty ItemTemplateProperty = + AvaloniaProperty.Register(nameof(ItemTemplate)); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty ItemTemplateProperty = - AvaloniaProperty.Register(nameof(ItemTemplate)); + public static readonly DirectProperty ItemsViewProperty = + AvaloniaProperty.RegisterDirect(nameof(ItemsView), o => o.ItemsView); - private IEnumerable _items = new AvaloniaList(); + /// + /// Defines the property. + /// + public static readonly StyledProperty LayoutProperty = + ItemsRepeater.LayoutProperty.AddOwner(); + + private bool _itemsInitialized; + private IItemContainerGenerator? _itemContainerGenerator; + private IEnumerable? _items; + private ItemsSourceView? _itemsView; private int _itemCount; - private IItemContainerGenerator _itemContainerGenerator; /// - /// Initializes static members of the class. + /// Initializes a new instance of the class. /// + public ItemsControl() + { + UpdatePseudoClasses(); + } + static ItemsControl() { - ItemsProperty.Changed.AddClassHandler((x, e) => x.ItemsChanged(e)); - ItemTemplateProperty.Changed.AddClassHandler((x, e) => x.ItemTemplateChanged(e)); + LayoutProperty.OverrideDefaultValue(new NonVirtualizingStackLayout()); } /// - /// Initializes a new instance of the class. + /// Gets the for the control. /// - public ItemsControl() + public IItemContainerGenerator ItemContainerGenerator { - PseudoClasses.Add(":empty"); - SubscribeToItems(_items); + get => _itemContainerGenerator ??= CreateItemContainerGenerator(); } /// - /// Gets the for the control. + /// Gets or sets the items to display. /// - public IItemContainerGenerator ItemContainerGenerator + [Content] + public IEnumerable? Items { get { - if (_itemContainerGenerator == null) + if (!_itemsInitialized) { - _itemContainerGenerator = CreateItemContainerGenerator(); - - if (_itemContainerGenerator != null) - { - _itemContainerGenerator.ItemTemplate = ItemTemplate; - _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e); - _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e); - _itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e); - } + _items = new AvaloniaList(); + _itemsInitialized = true; + CreateItemsView(); } - return _itemContainerGenerator; + return _items; } - } + set + { + if (_items != value) + { + var oldItems = _items; + var oldItemsView = _itemsView; - /// - /// Gets or sets the items to display. - /// - [Content] - public IEnumerable Items - { - get { return _items; } - set { SetAndRaise(ItemsProperty, ref _items, value); } + _items = value; + _itemsInitialized = true; + CreateItemsView(); + + RaisePropertyChanged(ItemsProperty, + new Optional(oldItems), + new BindingValue(_items)); + RaisePropertyChanged(ItemsViewProperty, oldItemsView, _itemsView); + } + } } /// @@ -111,43 +121,54 @@ public IEnumerable Items /// public int ItemCount { - get => _itemCount; + get => _itemsView?.Count ?? 0; private set => SetAndRaise(ItemCountProperty, ref _itemCount, value); } /// - /// Gets or sets the panel used to display the items. + /// Gets an over . /// - public ITemplate ItemsPanel + public ItemsSourceView ItemsView { - get { return GetValue(ItemsPanelProperty); } - set { SetValue(ItemsPanelProperty, value); } + get + { + if (!_itemsInitialized) + { + _items = new AvaloniaList(); + _itemsInitialized = true; + CreateItemsView(); + } + + return _itemsView!; + } } /// /// Gets or sets the data template used to display the items in the control. /// - public IDataTemplate ItemTemplate + public IDataTemplate? ItemTemplate { - get { return GetValue(ItemTemplateProperty); } - set { SetValue(ItemTemplateProperty, value); } + get => GetValue(ItemTemplateProperty); + set => SetValue(ItemTemplateProperty, value); } /// - /// Gets the items presenter control. + /// Gets or sets the layout used to size and position elements in the . /// - public IItemsPresenter Presenter + /// + /// The layout used to size and position elements. The default is a with + /// vertical orientation. + /// + public AttachedLayout Layout { - get; - protected set; + get => GetValue(LayoutProperty); + set => SetValue(LayoutProperty, value); } - /// - void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter) - { - Presenter = presenter; - ItemContainerGenerator.Clear(); - } + /// + /// Gets the items presenter control. + /// + public IItemsPresenter? Presenter { get; protected set; } void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) { @@ -159,123 +180,99 @@ void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyC void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) { - ItemsCollectionChanged(sender, e); + ItemsViewCollectionChanged(e); } /// - /// Gets the item at the specified index in a collection. + /// Occurs each time a container is cleared and made available to be re-used. /// - /// The collection. - /// The index. - /// The index of the item or -1 if the item was not found. - protected static object ElementAt(IEnumerable items, int index) - { - if (index != -1 && index < items.Count()) - { - return items.ElementAt(index) ?? null; - } - else - { - return null; - } - } + /// + /// This event is raised immediately each time a a container is cleared, such as when it + /// falls outside the range of realized items. Elements are cleared when they become + /// available for re-use. + /// + public event EventHandler? ContainerClearing; /// - /// Gets the index of an item in a collection. + /// Occurs each time a container is prepared for use. /// - /// The collection. - /// The item. - /// The index of the item or -1 if the item was not found. - protected static int IndexOf(IEnumerable items, object item) - { - if (items != null && item != null) - { - var list = items as IList; + /// + /// The prepared container might be newly created or an existing element that is being re- + /// used. + /// + public event EventHandler? ContainerPrepared; - if (list != null) - { - return list.IndexOf(item); - } - else - { - int index = 0; + /// + /// Occurs for each realized container when the index for the item it represents has changed. + /// + /// + /// This event is raised for each realized container where the index for the item it + /// represents has changed. For example, when another item is added or removed in the data + /// source, the index for items that come after in the ordering will be impacted. + /// + public event EventHandler? ContainerIndexChanged; - foreach (var i in items) - { - if (Equals(i, item)) - { - return index; - } + /// + /// Gets the container control for the specified index in , if realized. + /// + /// The item index. + /// The container control, or null if the item is not realized. + public IControl? TryGetContainer(int index) => Presenter?.TryGetElement(index); - ++index; - } - } - } + /// + /// Gets the index in of the specified container control. + /// + /// The container control. + /// The index of the container control, or -1 if the control is not a container. + public int GetContainerIndex(IControl container) + { + container = container ?? throw new ArgumentNullException(nameof(container)); - return -1; + return Presenter?.GetElementIndex(container) ?? -1; + } + + /// + IElementFactory IItemsPresenterHost.ElementFactory => ItemContainerGenerator; + + /// + void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter) + { + var oldValue = Presenter; + Presenter = presenter; + ItemsPresenterChanged(oldValue, presenter); } /// /// Creates the for the control. /// - /// - /// An or null. - /// - /// - /// Certain controls such as don't actually create item - /// containers; however they want it to be ItemsControls so that they have an Items - /// property etc. In this case, a derived class can override this method to return null - /// in order to disable the creation of item containers. - /// - protected virtual IItemContainerGenerator CreateItemContainerGenerator() - { - return new ItemContainerGenerator(this); - } + protected virtual IItemContainerGenerator CreateItemContainerGenerator() => new ItemContainerGenerator(this); /// - /// Called when new containers are materialized for the by its - /// . + /// Raises the event. /// - /// The details of the containers. - protected virtual void OnContainersMaterialized(ItemContainerEventArgs e) - { - foreach (var container in e.Containers) - { - // If the item is its own container, then it will be added to the logical tree when - // it was added to the Items collection. - if (container.ContainerControl != null && container.ContainerControl != container.Item) - { - LogicalChildren.Add(container.ContainerControl); - } - } - } + /// The event args. + /// + /// If you override this method, be sure to call the base method implementation. + /// + protected virtual void OnContainerPrepared(ElementPreparedEventArgs e) => ContainerPrepared?.Invoke(this, e); /// - /// Called when containers are dematerialized for the by its - /// . + /// Raises the event. /// - /// The details of the containers. - protected virtual void OnContainersDematerialized(ItemContainerEventArgs e) - { - foreach (var container in e.Containers) - { - // If the item is its own container, then it will be removed from the logical tree - // when it is removed from the Items collection. - if (container?.ContainerControl != container?.Item) - { - LogicalChildren.Remove(container.ContainerControl); - } - } - } + /// The event args. + /// + /// If you override this method, be sure to call the base method implementation. + /// + protected virtual void OnContainerClearing(ElementClearingEventArgs e) => ContainerClearing?.Invoke(this, e); /// - /// Called when containers are recycled for the by its - /// . + /// Raises the event. /// - /// The details of the containers. - protected virtual void OnContainersRecycled(ItemContainerEventArgs e) - { - } + /// The event args. + /// + /// If you override this method, be sure to call the base method implementation. + /// + protected virtual void OnContainerIndexChanged(ElementIndexChangedEventArgs e) => ContainerIndexChanged?.Invoke(this, e); /// /// Handles directional navigation within the . @@ -287,34 +284,16 @@ protected override void OnKeyDown(KeyEventArgs e) { var focus = FocusManager.Instance; var direction = e.Key.ToNavigationDirection(); - var container = Presenter?.Panel as INavigableContainer; - if (container == null || - focus.Current == null || - direction == null || - direction.Value.IsTab()) + if (direction.HasValue) { - return; - } - - IVisual current = focus.Current; + var next = focus.FindNextElement(direction.Value); - while (current != null) - { - if (current.VisualParent == container && current is IInputElement inputElement) + if (next is IControl c && GetContainerIndex(c) != -1) { - IInputElement next = GetNextControl(container, direction.Value, inputElement, false); - - if (next != null) - { - focus.Focus(next, NavigationMethod.Directional, e.KeyModifiers); - e.Handled = true; - } - - break; + focus.Focus(next, NavigationMethod.Directional); + e.Handled = true; } - - current = current.VisualParent; } } @@ -322,41 +301,27 @@ protected override void OnKeyDown(KeyEventArgs e) } /// - /// Called when the property changes. + /// Called when the changes. /// - /// The event args. - protected virtual void ItemsChanged(AvaloniaPropertyChangedEventArgs e) + /// + /// The old items view. Will be null on first invocation and non-null thereafter. + /// + /// The new items view. + protected virtual void ItemsViewChanged(ItemsSourceView? oldView, ItemsSourceView newView) { - var oldValue = e.OldValue as IEnumerable; - var newValue = e.NewValue as IEnumerable; - - if (oldValue is INotifyCollectionChanged incc) - { - CollectionChangedEventManager.Instance.RemoveListener(incc, this); - } - - UpdateItemCount(); - RemoveControlItemsFromLogicalChildren(oldValue); - AddControlItemsToLogicalChildren(newValue); - - if (Presenter != null) - { - Presenter.Items = newValue; - } - - SubscribeToItems(newValue); + LogicalChildren.Clear(); + AddControlItemsToLogicalChildren(newView); + UpdatePseudoClasses(); + ItemCount = ItemsView.Count; } /// /// Called when the event is - /// raised on . + /// raised on . /// - /// The event sender. /// The event args. - protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + protected virtual void ItemsViewCollectionChanged(NotifyCollectionChangedEventArgs e) { - UpdateItemCount(); - switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -366,13 +331,64 @@ protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionCha case NotifyCollectionChangedAction.Remove: RemoveControlItemsFromLogicalChildren(e.OldItems); break; + + case NotifyCollectionChangedAction.Reset: + LogicalChildren.Clear(); + AddControlItemsToLogicalChildren(ItemsView); + break; + } + + UpdatePseudoClasses(); + ItemCount = ItemsView.Count; + } + + protected virtual void ItemsPresenterChanged(IItemsPresenter? oldValue, IItemsPresenter? newValue) + { + void Prepared(object sender, ElementPreparedEventArgs e) => OnContainerPrepared(e); + void Clearing(object sender, ElementClearingEventArgs e) => OnContainerClearing(e); + void IndexChanged(object sender, ElementIndexChangedEventArgs e) => OnContainerIndexChanged(e); + + if (oldValue != null) + { + oldValue.LogicalChildren.CollectionChanged -= PresenterLogicalChildrenChanged; + oldValue.ElementPrepared -= Prepared; + oldValue.ElementClearing -= Clearing; + oldValue.ElementIndexChanged -= IndexChanged; + LogicalChildren.RemoveAll(oldValue.LogicalChildren); + } + + if (newValue != null) + { + newValue.LogicalChildren.CollectionChanged += PresenterLogicalChildrenChanged; + newValue.ElementPrepared += Prepared; + newValue.ElementClearing += Clearing; + newValue.ElementIndexChanged += IndexChanged; + LogicalChildren.AddRange(newValue.LogicalChildren); + } + } + + private void CreateItemsView() + { + var oldView = _itemsView; + + if (_itemsView is object && _itemsView != ItemsSourceView.Empty) + { + _itemsView.RemoveListener(this); + _itemsView?.Dispose(); } - Presenter?.ItemsChanged(e); + if (_items is object) + { + _itemsView = new ItemsSourceView(_items); + _itemsView.AddListener(this); + } + else + { + _itemsView = ItemsSourceView.Empty; + } - var collection = sender as ICollection; - PseudoClasses.Set(":empty", collection == null || collection.Count == 0); - PseudoClasses.Set(":singleitem", collection != null && collection.Count == 1); + ItemsViewChanged(oldView, _itemsView); + RaisePropertyChanged(ItemsViewProperty, oldView, _itemsView); } /// @@ -383,16 +399,11 @@ private void AddControlItemsToLogicalChildren(IEnumerable items) { var toAdd = new List(); - if (items != null) + foreach (var i in items) { - foreach (var i in items) + if (i is IControl control && !LogicalChildren.Contains(control)) { - var control = i as IControl; - - if (control != null && !LogicalChildren.Contains(control)) - { - toAdd.Add(control); - } + toAdd.Add(control); } } @@ -407,70 +418,71 @@ private void RemoveControlItemsFromLogicalChildren(IEnumerable items) { var toRemove = new List(); - if (items != null) + foreach (var i in items) { - foreach (var i in items) + if (i is IControl control) { - var control = i as IControl; - - if (control != null) - { - toRemove.Add(control); - } + toRemove.Add(control); } } LogicalChildren.RemoveAll(toRemove); } - /// - /// Subscribes to an collection. - /// - /// The items collection. - private void SubscribeToItems(IEnumerable items) + private void PresenterLogicalChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) { - PseudoClasses.Set(":empty", items == null || items.Count() == 0); - PseudoClasses.Set(":singleitem", items != null && items.Count() == 1); - - if (items is INotifyCollectionChanged incc) + void Add(IList items) { - CollectionChangedEventManager.Instance.AddListener(incc, this); + foreach (ILogical l in items) + { + LogicalChildren.Add(l); + } } - } - /// - /// Called when the changes. - /// - /// The event args. - private void ItemTemplateChanged(AvaloniaPropertyChangedEventArgs e) - { - if (_itemContainerGenerator != null) + void Remove(IList items) { - _itemContainerGenerator.ItemTemplate = (IDataTemplate)e.NewValue; - // TODO: Rebuild the item containers. + foreach (ILogical l in items) + { + LogicalChildren.Remove(l); + } } - } - private void UpdateItemCount() - { - if (Items == null) + if (Presenter is null) { - ItemCount = 0; + throw new AvaloniaInternalException( + "Received presenter logical children changed notification, but Presenter is null."); } - else if (Items is IList list) - { - ItemCount = list.Count; - } - else + + switch (e.Action) { - ItemCount = Items.Count(); + case NotifyCollectionChangedAction.Add: + Add(e.NewItems); + break; + case NotifyCollectionChangedAction.Remove: + Remove(e.OldItems); + break; + case NotifyCollectionChangedAction.Replace: + Remove(e.OldItems); + Add(e.NewItems); + break; + case NotifyCollectionChangedAction.Reset: + LogicalChildren.Clear(); + LogicalChildren.AddRange(Presenter.LogicalChildren); + break; } } - protected static IInputElement GetNextControl( + private void UpdatePseudoClasses() + { + var count = _itemsView?.Count ?? 0; + PseudoClasses.Set(":empty", count == 0); + PseudoClasses.Set(":singleitem", count == 1); + } + + protected static IInputElement? GetNextControl( INavigableContainer container, NavigationDirection direction, - IInputElement from, + IInputElement? from, bool wrap) { IInputElement result; diff --git a/src/Avalonia.Controls/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs index b2663f3213f..b77663f335c 100644 --- a/src/Avalonia.Controls/ItemsSourceView.cs +++ b/src/Avalonia.Controls/ItemsSourceView.cs @@ -25,7 +25,7 @@ namespace Avalonia.Controls /// view of the Items. That way, each component does not need to know if the source is an /// IEnumerable, an IList, or something else. /// - public class ItemsSourceView : INotifyCollectionChanged, IDisposable + public class ItemsSourceView : INotifyCollectionChanged, IDisposable, IReadOnlyList { /// /// Gets an empty @@ -160,6 +160,10 @@ internal void RemoveListener(ICollectionChangedListener listener) } } + public Enumerator GetEnumerator() => new Enumerator(_inner); + IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args) { CollectionChanged?.Invoke(this, args); @@ -178,6 +182,17 @@ private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs { OnItemsSourceChanged(e); } + + public struct Enumerator : IEnumerator + { + private IEnumerator _innerEnumerator; + public Enumerator(IList inner) => _innerEnumerator = inner.GetEnumerator(); + public object Current => _innerEnumerator.Current; + object IEnumerator.Current => Current; + public void Dispose() => (_innerEnumerator as IDisposable)?.Dispose(); + public bool MoveNext() => _innerEnumerator.MoveNext(); + void IEnumerator.Reset() => _innerEnumerator.Reset(); + } } public class ItemsSourceView : ItemsSourceView, IReadOnlyList diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index f7e86d697a7..ab3481cc452 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -1,12 +1,13 @@ using System.Collections; using Avalonia.Controls.Generators; -using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Selection; -using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.VisualTree; +#nullable enable + namespace Avalonia.Controls { /// @@ -14,17 +15,11 @@ namespace Avalonia.Controls /// public class ListBox : SelectingItemsControl { - /// - /// The default value for the property. - /// - private static readonly FuncTemplate DefaultPanel = - new FuncTemplate(() => new VirtualizingStackPanel()); - /// /// Defines the property. /// - public static readonly DirectProperty ScrollProperty = - AvaloniaProperty.RegisterDirect(nameof(Scroll), o => o.Scroll); + public static readonly DirectProperty ScrollProperty = + AvaloniaProperty.RegisterDirect(nameof(Scroll), o => o.Scroll); /// /// Defines the property. @@ -44,27 +39,17 @@ public class ListBox : SelectingItemsControl public static readonly new StyledProperty SelectionModeProperty = SelectingItemsControl.SelectionModeProperty; - /// - /// Defines the property. - /// - public static readonly StyledProperty VirtualizationModeProperty = - ItemsPresenter.VirtualizationModeProperty.AddOwner(); - - private IScrollable _scroll; + private IScrollable? _scroll; - /// - /// Initializes static members of the class. - /// static ListBox() { - ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); - VirtualizationModeProperty.OverrideDefaultValue(ItemVirtualizationMode.Simple); + LayoutProperty.OverrideDefaultValue(new StackLayout()); } /// /// Gets the scroll information for the . /// - public IScrollable Scroll + public IScrollable? Scroll { get { return _scroll; } private set { SetAndRaise(ScrollProperty, ref _scroll, value); } @@ -97,15 +82,6 @@ public IScrollable Scroll set { base.SelectionMode = value; } } - /// - /// Gets or sets the virtualization mode for the items. - /// - public ItemVirtualizationMode VirtualizationMode - { - get { return GetValue(VirtualizationModeProperty); } - set { SetValue(VirtualizationModeProperty, value); } - } - /// /// Selects all items in the . /// @@ -120,7 +96,7 @@ public ItemVirtualizationMode VirtualizationMode protected override IItemContainerGenerator CreateItemContainerGenerator() { return new ItemContainerGenerator( - this, + this, ListBoxItem.ContentProperty, ListBoxItem.ContentTemplateProperty); } diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index 4da044fec1d..b705def3f70 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -14,9 +14,6 @@ namespace Avalonia.Controls /// public class Menu : MenuBase, IMainMenu { - private static readonly ITemplate DefaultPanel = - new FuncTemplate(() => new StackPanel { Orientation = Orientation.Horizontal }); - private LightDismissOverlayLayer? _overlay; /// @@ -37,7 +34,10 @@ public Menu(IMenuInteractionHandler interactionHandler) static Menu() { - ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel); + LayoutProperty.OverrideDefaultValue(new NonVirtualizingStackLayout + { + Orientation = Orientation.Horizontal, + }); } /// diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index 04349282804..eddf7e8205a 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; +using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; @@ -82,26 +83,16 @@ public bool IsOpen get { var index = SelectedIndex; - return (index != -1) ? - (IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) : - null; + return (index != -1) ? (IMenuItem)TryGetContainer(index) : null; } set { - SelectedIndex = ItemContainerGenerator.IndexFromContainer(value); + SelectedIndex = value is object ? GetContainerIndex(value) : -1; } } /// - IEnumerable IMenuElement.SubItems - { - get - { - return ItemContainerGenerator.Containers - .Select(x => x.ContainerControl) - .OfType(); - } - } + IEnumerable IMenuElement.SubItems => Presenter.RealizedElements.OfType(); /// /// Gets the interaction handler for the menu. @@ -139,10 +130,9 @@ public event EventHandler MenuClosed /// bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); - /// protected override IItemContainerGenerator CreateItemContainerGenerator() { - return new ItemContainerGenerator(this, MenuItem.HeaderProperty, null); + return new ItemContainerGenerator(this, MenuItem.HeaderProperty, MenuItem.HeaderTemplateProperty); } /// diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index b4d3272471d..dedc33a5db3 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -91,12 +91,6 @@ public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable public static readonly RoutedEvent SubmenuOpenedEvent = RoutedEvent.Register(nameof(SubmenuOpened), RoutingStrategies.Bubble); - /// - /// The default value for the property. - /// - private static readonly ITemplate DefaultPanel = - new FuncTemplate(() => new StackPanel()); - private ICommand? _command; private bool _commandCanExecute = true; private Popup _popup; @@ -113,7 +107,6 @@ static MenuItem() HeaderProperty.Changed.AddClassHandler((x, e) => x.HeaderChanged(e)); IconProperty.Changed.AddClassHandler((x, e) => x.IconChanged(e)); IsSelectedProperty.Changed.AddClassHandler((x, e) => x.IsSelectedChanged(e)); - ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); ClickEvent.AddClassHandler((x, e) => x.OnClick(e)); SubmenuOpenedEvent.AddClassHandler((x, e) => x.OnSubmenuOpened(e)); IsSubMenuOpenProperty.Changed.AddClassHandler((x, e) => x.SubMenuOpenChanged(e)); @@ -287,26 +280,16 @@ public bool IsSubMenuOpen get { var index = SelectedIndex; - return (index != -1) ? - (IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) : - null; + return (index != -1) ? (IMenuItem)TryGetContainer(index) : null; } set { - SelectedIndex = ItemContainerGenerator.IndexFromContainer(value); + SelectedIndex = value is object ? GetContainerIndex(value) : -1; } } /// - IEnumerable IMenuElement.SubItems - { - get - { - return ItemContainerGenerator.Containers - .Select(x => x.ContainerControl) - .OfType(); - } - } + IEnumerable IMenuElement.SubItems => Presenter.RealizedElements.OfType(); /// /// Opens the submenu. @@ -592,8 +575,7 @@ private void PopupOpened(object sender, EventArgs e) if (selected != -1) { - var container = ItemContainerGenerator.ContainerFromIndex(selected); - container?.Focus(); + TryGetContainer(selected)?.Focus(); } } diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs deleted file mode 100644 index 7888249bdd7..00000000000 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ /dev/null @@ -1,276 +0,0 @@ -using System.Collections.Specialized; -using System.Linq; -using System.Reactive.Linq; -using System.Threading.Tasks; -using Avalonia.Animation; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Utils; -using Avalonia.Data; - -namespace Avalonia.Controls.Presenters -{ - /// - /// Displays pages inside an . - /// - public class CarouselPresenter : ItemsPresenterBase - { - /// - /// Defines the property. - /// - public static readonly StyledProperty IsVirtualizedProperty = - Carousel.IsVirtualizedProperty.AddOwner(); - - /// - /// Defines the property. - /// - public static readonly DirectProperty SelectedIndexProperty = - SelectingItemsControl.SelectedIndexProperty.AddOwner( - o => o.SelectedIndex, - (o, v) => o.SelectedIndex = v); - - /// - /// Defines the property. - /// - public static readonly StyledProperty PageTransitionProperty = - Carousel.PageTransitionProperty.AddOwner(); - - private int _selectedIndex = -1; - private Task _currentTransition; - private int _queuedTransitionIndex = -1; - - /// - /// Initializes static members of the class. - /// - static CarouselPresenter() - { - IsVirtualizedProperty.Changed.AddClassHandler((x, e) => x.IsVirtualizedChanged(e)); - SelectedIndexProperty.Changed.AddClassHandler((x, e) => x.SelectedIndexChanged(e)); - } - - /// - /// Gets or sets a value indicating whether the items in the carousel are virtualized. - /// - /// - /// When the carousel is virtualized, only the active page is held in memory. - /// - public bool IsVirtualized - { - get { return GetValue(IsVirtualizedProperty); } - set { SetValue(IsVirtualizedProperty, value); } - } - - /// - /// Gets or sets the index of the selected page. - /// - public int SelectedIndex - { - get - { - return _selectedIndex; - } - - set - { - var old = SelectedIndex; - var effective = (value >= 0 && value < Items?.Cast().Count()) ? value : -1; - - if (old != effective) - { - _selectedIndex = effective; - RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue); - } - } - } - - /// - /// Gets or sets a transition to use when switching pages. - /// - public IPageTransition PageTransition - { - get { return GetValue(PageTransitionProperty); } - set { SetValue(PageTransitionProperty, value); } - } - - /// - protected override void ItemsChanged(NotifyCollectionChangedEventArgs e) - { - if (!IsVirtualized) - { - base.ItemsChanged(e); - - if (Items == null || SelectedIndex >= Items.Count()) - { - SelectedIndex = Items.Count() - 1; - } - - foreach (var c in ItemContainerGenerator.Containers) - { - c.ContainerControl.IsVisible = c.Index == SelectedIndex; - } - } - else if (SelectedIndex != -1 && Panel != null) - { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - if (e.NewStartingIndex > SelectedIndex) - { - return; - } - break; - case NotifyCollectionChangedAction.Remove: - if (e.OldStartingIndex > SelectedIndex) - { - return; - } - break; - case NotifyCollectionChangedAction.Replace: - if (e.OldStartingIndex > SelectedIndex || - e.OldStartingIndex + e.OldItems.Count - 1 < SelectedIndex) - { - return; - } - break; - case NotifyCollectionChangedAction.Move: - if (e.OldStartingIndex > SelectedIndex && - e.NewStartingIndex > SelectedIndex) - { - return; - } - break; - } - - if (Items == null || SelectedIndex >= Items.Count()) - { - SelectedIndex = Items.Count() - 1; - } - - Panel.Children.Clear(); - ItemContainerGenerator.Clear(); - - if (SelectedIndex != -1) - { - GetOrCreateContainer(SelectedIndex); - } - } - } - - protected override void PanelCreated(IPanel panel) - { - ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - /// - /// Moves to the selected page, animating if a is set. - /// - /// The index of the old page. - /// The index of the new page. - /// A task tracking the animation. - private async Task MoveToPage(int fromIndex, int toIndex) - { - if (fromIndex != toIndex) - { - var generator = ItemContainerGenerator; - IControl from = null; - IControl to = null; - - if (fromIndex != -1) - { - from = ItemContainerGenerator.ContainerFromIndex(fromIndex); - } - - if (toIndex != -1) - { - to = GetOrCreateContainer(toIndex); - } - - if (PageTransition != null && (from != null || to != null)) - { - await PageTransition.Start((Visual)from, (Visual)to, fromIndex < toIndex); - } - else if (to != null) - { - to.IsVisible = true; - } - - if (from != null) - { - if (IsVirtualized) - { - Panel.Children.Remove(from); - generator.Dematerialize(fromIndex, 1); - } - else - { - from.IsVisible = false; - } - } - } - } - - private IControl GetOrCreateContainer(int index) - { - var container = ItemContainerGenerator.ContainerFromIndex(index); - - if (container == null && IsVirtualized) - { - var item = Items.Cast().ElementAt(index); - var materialized = ItemContainerGenerator.Materialize(index, item); - Panel.Children.Add(materialized.ContainerControl); - container = materialized.ContainerControl; - } - - return container; - } - - /// - /// Called when the property changes. - /// - /// The event args. - private void IsVirtualizedChanged(AvaloniaPropertyChangedEventArgs e) - { - if (Panel != null) - { - ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - } - - /// - /// Called when the property changes. - /// - /// The event args. - private async void SelectedIndexChanged(AvaloniaPropertyChangedEventArgs e) - { - if (Panel != null) - { - if (_currentTransition == null) - { - int fromIndex = (int)e.OldValue; - int toIndex = (int)e.NewValue; - - for (;;) - { - _currentTransition = MoveToPage(fromIndex, toIndex); - await _currentTransition; - - if (_queuedTransitionIndex != -1) - { - fromIndex = toIndex; - toIndex = _queuedTransitionIndex; - _queuedTransitionIndex = -1; - } - else - { - _currentTransition = null; - break; - } - } - } - else - { - _queuedTransitionIndex = (int)e.NewValue; - } - } - } - } -} diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs index 873f0d756f4..2fb4cf76b87 100644 --- a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs @@ -1,16 +1,83 @@ +using System; using System.Collections; -using System.Collections.Specialized; +using System.Collections.Generic; + +#nullable enable namespace Avalonia.Controls.Presenters { public interface IItemsPresenter : IPresenter { - IEnumerable Items { get; set; } + /// + /// Gets the currently realized elements. + /// + IEnumerable RealizedElements { get; } + + /// + /// Gets or sets the items to display in the items presenter. + /// + IEnumerable? Items { get; set; } + + /// + /// Retrieves the index of the item from the data source that corresponds to the specified + /// . + /// + /// + /// The element that corresponds to the item to get the index of. + /// + /// + /// The index of the item from the data source that corresponds to the specified UIElement, + /// or -1 if the element is not supported. + /// + int GetElementIndex(IControl element); + + /// + /// Retrieves the realized that corresponds to the item at the + /// specified index in the data source. + /// + /// The index of the item. + /// + /// The that corresponds to the item at the specified index if the + /// item is realized, or null if the item is not realized. + /// + IControl? TryGetElement(int index); + + /// + /// Scrolls the specified item into view. + /// + /// The index of the item. + /// + /// True if the control was scrolled into view; otherwise false. + /// + bool ScrollIntoView(int index); - IPanel Panel { get; } + /// + /// Occurs each time an element is prepared for use. + /// + /// + /// The prepared element might be newly created or an existing element that is being re- + /// used. + /// + event EventHandler? ElementPrepared; - void ItemsChanged(NotifyCollectionChangedEventArgs e); + /// + /// Occurs each time an element is cleared and made available to be re-used. + /// + /// + /// This event is raised immediately each time an element is cleared, such as when it falls + /// outside the range of realized items. Elements are cleared when they become available + /// for re-use. + /// + event EventHandler? ElementClearing; - void ScrollIntoView(int index); + /// + /// Occurs for each realized element when the index for the item it represents has changed. + /// + /// + /// This event is raised for each realized element where the index for the item it + /// represents has changed. For example, when another item is added or removed in the data + /// source, the index for items that come after in the ordering will be impacted. + /// + event EventHandler ElementIndexChanged; } } diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenterHost.cs b/src/Avalonia.Controls/Presenters/IItemsPresenterHost.cs index ba9ee0fe319..9eed77fe059 100644 --- a/src/Avalonia.Controls/Presenters/IItemsPresenterHost.cs +++ b/src/Avalonia.Controls/Presenters/IItemsPresenterHost.cs @@ -1,5 +1,8 @@ +using Avalonia.LogicalTree; using Avalonia.Styling; +#nullable enable + namespace Avalonia.Controls.Presenters { /// @@ -13,8 +16,13 @@ namespace Avalonia.Controls.Presenters /// parent control's template is instantiated so they register themselves using this /// interface. /// - public interface IItemsPresenterHost : ITemplatedControl + public interface IItemsPresenterHost : ITemplatedControl, ILogical { + /// + /// Gets the element factory for the items presenter. + /// + IElementFactory ElementFactory { get; } + /// /// Registers an with a host control. /// diff --git a/src/Avalonia.Controls/Presenters/ItemContainerSync.cs b/src/Avalonia.Controls/Presenters/ItemContainerSync.cs deleted file mode 100644 index 6e72908e6b5..00000000000 --- a/src/Avalonia.Controls/Presenters/ItemContainerSync.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using Avalonia.Controls.Generators; -using Avalonia.Controls.Utils; - -namespace Avalonia.Controls.Presenters -{ - internal static class ItemContainerSync - { - public static void ItemsChanged( - ItemsPresenterBase owner, - IEnumerable items, - NotifyCollectionChangedEventArgs e) - { - var generator = owner.ItemContainerGenerator; - var panel = owner.Panel; - - if (panel == null) - { - return; - } - - void Add() - { - if (e.NewStartingIndex + e.NewItems.Count < items.Count()) - { - generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count); - } - - AddContainers(owner, e.NewStartingIndex, e.NewItems); - } - - void Remove() - { - RemoveContainers(panel, generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count)); - } - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - Add(); - break; - - case NotifyCollectionChangedAction.Remove: - Remove(); - break; - - case NotifyCollectionChangedAction.Replace: - RemoveContainers(panel, generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count)); - var containers = AddContainers(owner, e.NewStartingIndex, e.NewItems); - - var i = e.NewStartingIndex; - - foreach (var container in containers) - { - panel.Children[i++] = container.ContainerControl; - } - - break; - - case NotifyCollectionChangedAction.Move: - Remove(); - Add(); - break; - - case NotifyCollectionChangedAction.Reset: - RemoveContainers(panel, generator.Clear()); - - if (items != null) - { - AddContainers(owner, 0, items); - } - - break; - } - } - - private static IList AddContainers( - ItemsPresenterBase owner, - int index, - IEnumerable items) - { - var generator = owner.ItemContainerGenerator; - var result = new List(); - var panel = owner.Panel; - - foreach (var item in items) - { - var i = generator.Materialize(index++, item); - - if (i.ContainerControl != null) - { - if (i.Index < panel.Children.Count) - { - // TODO: This will insert at the wrong place when there are null items. - panel.Children.Insert(i.Index, i.ContainerControl); - } - else - { - panel.Children.Add(i.ContainerControl); - } - } - - result.Add(i); - } - - return result; - } - - private static void RemoveContainers( - IPanel panel, - IEnumerable items) - { - foreach (var i in items) - { - if (i.ContainerControl != null) - { - panel.Children.Remove(i.ContainerControl); - } - } - } - } -} diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs deleted file mode 100644 index be2b1e86f07..00000000000 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ /dev/null @@ -1,303 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Specialized; -using System.Reactive.Linq; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Utils; -using Avalonia.Input; -using Avalonia.Layout; -using Avalonia.VisualTree; - -namespace Avalonia.Controls.Presenters -{ - /// - /// Base class for classes which handle virtualization for an . - /// - internal abstract class ItemVirtualizer : IVirtualizingController, IDisposable - { - private double _crossAxisOffset; - private IDisposable _subscriptions; - - /// - /// Initializes a new instance of the class. - /// - /// - public ItemVirtualizer(ItemsPresenter owner) - { - Owner = owner; - Items = owner.Items; - ItemCount = owner.Items.Count(); - - var panel = VirtualizingPanel; - - if (panel != null) - { - _subscriptions = panel.GetObservable(Panel.BoundsProperty) - .Skip(1) - .Subscribe(_ => InvalidateScroll()); - } - } - - /// - /// Gets the which owns the virtualizer. - /// - public ItemsPresenter Owner { get; } - - /// - /// Gets the which will host the items. - /// - public IVirtualizingPanel VirtualizingPanel => Owner.Panel as IVirtualizingPanel; - - /// - /// Gets the items to display. - /// - public IEnumerable Items { get; private set; } - - /// - /// Gets the number of items in . - /// - public int ItemCount { get; private set; } - - /// - /// Gets or sets the index of the first item displayed in the panel. - /// - public int FirstIndex { get; protected set; } - - /// - /// Gets or sets the index of the first item beyond those displayed in the panel. - /// - public int NextIndex { get; protected set; } - - /// - /// Gets a value indicating whether the items should be scroll horizontally or vertically. - /// - public bool Vertical => VirtualizingPanel?.ScrollDirection == Orientation.Vertical; - - /// - /// Gets a value indicating whether logical scrolling is enabled. - /// - public abstract bool IsLogicalScrollEnabled { get; } - - /// - /// Gets the value of the scroll extent. - /// - public abstract double ExtentValue { get; } - - /// - /// Gets or sets the value of the current scroll offset. - /// - public abstract double OffsetValue { get; set; } - - /// - /// Gets the value of the scrollable viewport. - /// - public abstract double ViewportValue { get; } - - /// - /// Gets the as a . - /// - public Size Extent - { - get - { - if (IsLogicalScrollEnabled) - { - return Vertical ? - new Size(Owner.Panel.DesiredSize.Width, ExtentValue) : - new Size(ExtentValue, Owner.Panel.DesiredSize.Height); - } - - return default; - } - } - - /// - /// Gets the as a . - /// - public Size Viewport - { - get - { - if (IsLogicalScrollEnabled) - { - return Vertical ? - new Size(Owner.Panel.Bounds.Width, ViewportValue) : - new Size(ViewportValue, Owner.Panel.Bounds.Height); - } - - return default; - } - } - - /// - /// Gets or sets the as a . - /// - public Vector Offset - { - get - { - if (IsLogicalScrollEnabled) - { - return Vertical ? new Vector(_crossAxisOffset, OffsetValue) : new Vector(OffsetValue, _crossAxisOffset); - } - - return default; - } - - set - { - if (!IsLogicalScrollEnabled) - { - throw new NotSupportedException("Logical scrolling disabled."); - } - - var oldCrossAxisOffset = _crossAxisOffset; - - if (Vertical) - { - OffsetValue = value.Y; - _crossAxisOffset = value.X; - } - else - { - OffsetValue = value.X; - _crossAxisOffset = value.Y; - } - - if (_crossAxisOffset != oldCrossAxisOffset) - { - Owner.InvalidateArrange(); - } - } - } - - /// - /// Creates an based on an item presenter's - /// . - /// - /// The items presenter. - /// An . - public static ItemVirtualizer Create(ItemsPresenter owner) - { - if (owner.Panel == null) - { - return null; - } - - var virtualizingPanel = owner.Panel as IVirtualizingPanel; - var scrollContentPresenter = owner.Parent as IScrollable; - ItemVirtualizer result = null; - - if (virtualizingPanel != null && scrollContentPresenter is object) - { - switch (owner.VirtualizationMode) - { - case ItemVirtualizationMode.Simple: - result = new ItemVirtualizerSimple(owner); - break; - } - } - - if (result == null) - { - result = new ItemVirtualizerNone(owner); - } - - if (virtualizingPanel != null) - { - virtualizingPanel.Controller = result; - } - - return result; - } - - /// - /// Carries out a measure for the related . - /// - /// The size available to the control. - /// The desired size for the control. - public virtual Size MeasureOverride(Size availableSize) - { - Owner.Panel.Measure(availableSize); - return Owner.Panel.DesiredSize; - } - - /// - /// Carries out an arrange for the related . - /// - /// The size available to the control. - /// The actual size used. - public virtual Size ArrangeOverride(Size finalSize) - { - if (VirtualizingPanel != null) - { - VirtualizingPanel.CrossAxisOffset = _crossAxisOffset; - Owner.Panel.Arrange(new Rect(finalSize)); - } - else - { - var origin = Vertical ? new Point(-_crossAxisOffset, 0) : new Point(0, _crossAxisOffset); - Owner.Panel.Arrange(new Rect(origin, finalSize)); - } - - return finalSize; - } - - /// - public virtual void UpdateControls() - { - } - - /// - /// Gets the next control in the specified direction. - /// - /// The movement direction. - /// The control from which movement begins. - /// The control. - public virtual IControl GetControlInDirection(NavigationDirection direction, IControl from) - { - return null; - } - - /// - /// Called when the items for the presenter change, either because - /// has been set, the items collection has been - /// modified, or the panel has been created. - /// - /// The items. - /// A description of the change. - public virtual void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) - { - Items = items; - ItemCount = items.Count(); - } - - /// - /// Scrolls the specified item into view. - /// - /// The index of the item. - public virtual void ScrollIntoView(int index) - { - } - - /// - public virtual void Dispose() - { - _subscriptions?.Dispose(); - _subscriptions = null; - - if (VirtualizingPanel != null) - { - VirtualizingPanel.Controller = null; - VirtualizingPanel.Children.Clear(); - } - - Owner.ItemContainerGenerator.Clear(); - } - - /// - /// Invalidates the current scroll. - /// - protected void InvalidateScroll() => ((ILogicalScrollable)Owner).RaiseScrollInvalidated(EventArgs.Empty); - } -} diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs deleted file mode 100644 index 275f4d418ef..00000000000 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using Avalonia.Controls.Generators; -using Avalonia.Controls.Utils; - -namespace Avalonia.Controls.Presenters -{ - /// - /// Represents an item virtualizer for an that doesn't actually - /// virtualize items - it just creates a container for every item. - /// - internal class ItemVirtualizerNone : ItemVirtualizer - { - public ItemVirtualizerNone(ItemsPresenter owner) - : base(owner) - { - if (Items != null && owner.Panel != null) - { - AddContainers(0, Items); - } - } - - /// - public override bool IsLogicalScrollEnabled => false; - - /// - /// This property should never be accessed because is - /// false. - /// - public override double ExtentValue - { - get { throw new NotSupportedException(); } - } - - /// - /// This property should never be accessed because is - /// false. - /// - public override double OffsetValue - { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - - /// - /// This property should never be accessed because is - /// false. - /// - public override double ViewportValue - { - get { throw new NotSupportedException(); } - } - - /// - public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) - { - base.ItemsChanged(items, e); - ItemContainerSync.ItemsChanged(Owner, items, e); - Owner.InvalidateMeasure(); - } - - /// - /// Scrolls the specified item into view. - /// - /// The index of the item. - public override void ScrollIntoView(int index) - { - if (index != -1) - { - var container = Owner.ItemContainerGenerator.ContainerFromIndex(index); - container?.BringIntoView(); - } - } - - private IList AddContainers(int index, IEnumerable items) - { - var generator = Owner.ItemContainerGenerator; - var result = new List(); - var panel = Owner.Panel; - - foreach (var item in items) - { - var i = generator.Materialize(index++, item); - - if (i.ContainerControl != null) - { - if (i.Index < panel.Children.Count) - { - // TODO: This will insert at the wrong place when there are null items. - panel.Children.Insert(i.Index, i.ContainerControl); - } - else - { - panel.Children.Add(i.ContainerControl); - } - } - - result.Add(i); - } - - return result; - } - - private void RemoveContainers(IEnumerable items) - { - var panel = Owner.Panel; - - foreach (var i in items) - { - if (i.ContainerControl != null) - { - panel.Children.Remove(i.ContainerControl); - } - } - } - } -} diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs deleted file mode 100644 index 3fac440c408..00000000000 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ /dev/null @@ -1,582 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Specialized; -using System.Linq; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Utils; -using Avalonia.Input; -using Avalonia.Layout; -using Avalonia.Utilities; -using Avalonia.VisualTree; - -namespace Avalonia.Controls.Presenters -{ - /// - /// Handles virtualization in an for - /// . - /// - internal class ItemVirtualizerSimple : ItemVirtualizer - { - private int _anchor; - - /// - /// Initializes a new instance of the class. - /// - /// - public ItemVirtualizerSimple(ItemsPresenter owner) - : base(owner) - { - // Don't need to add children here as UpdateControls should be called by the panel - // measure/arrange. - } - - /// - public override bool IsLogicalScrollEnabled => true; - - /// - public override double ExtentValue => ItemCount; - - /// - public override double OffsetValue - { - get - { - var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0; - return FirstIndex + offset; - } - - set - { - var panel = VirtualizingPanel; - var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0; - var delta = (int)(value - (FirstIndex + offset)); - - if (delta != 0) - { - var newLastIndex = (NextIndex - 1) + delta; - - if (newLastIndex < ItemCount) - { - if (panel.PixelOffset > 0) - { - panel.PixelOffset = 0; - delta += 1; - } - - if (delta != 0) - { - RecycleContainersForMove(delta); - } - } - else - { - // We're moving to a partially obscured item at the end of the list so - // offset the panel by the height of the first item. - var firstIndex = ItemCount - panel.Children.Count; - RecycleContainersForMove(firstIndex - FirstIndex); - - double pixelOffset; - var child = panel.Children[0]; - - if (child.IsArrangeValid) - { - pixelOffset = VirtualizingPanel.ScrollDirection == Orientation.Vertical ? - child.Bounds.Height : - child.Bounds.Width; - } - else - { - pixelOffset = VirtualizingPanel.ScrollDirection == Orientation.Vertical ? - child.DesiredSize.Height : - child.DesiredSize.Width; - } - - panel.PixelOffset = pixelOffset; - } - } - } - } - - /// - public override double ViewportValue - { - get - { - // If we can't fit the last item in the panel fully, subtract 1 from the viewport. - var overflow = VirtualizingPanel.PixelOverflow > 0 ? 1 : 0; - return VirtualizingPanel.Children.Count - overflow; - } - } - - /// - public override Size MeasureOverride(Size availableSize) - { - var scrollable = (ILogicalScrollable)Owner; - var visualRoot = Owner.GetVisualRoot(); - var maxAvailableSize = (visualRoot as WindowBase)?.PlatformImpl?.MaxAutoSizeHint - ?? (visualRoot as TopLevel)?.ClientSize; - - // If infinity is passed as the available size and we're virtualized then we need to - // fill the available space, but to do that we *don't* want to materialize all our - // items! Take a look at the root of the tree for a MaxClientSize and use that as - // the available size. - if (VirtualizingPanel.ScrollDirection == Orientation.Vertical) - { - if (availableSize.Height == double.PositiveInfinity) - { - if (maxAvailableSize.HasValue) - { - availableSize = availableSize.WithHeight(maxAvailableSize.Value.Height); - } - } - - if (scrollable.CanHorizontallyScroll) - { - availableSize = availableSize.WithWidth(double.PositiveInfinity); - } - } - else - { - if (availableSize.Width == double.PositiveInfinity) - { - if (maxAvailableSize.HasValue) - { - availableSize = availableSize.WithWidth(maxAvailableSize.Value.Width); - } - } - - if (scrollable.CanVerticallyScroll) - { - availableSize = availableSize.WithHeight(double.PositiveInfinity); - } - } - - Owner.Panel.Measure(availableSize); - return Owner.Panel.DesiredSize; - } - - /// - public override void UpdateControls() - { - CreateAndRemoveContainers(); - InvalidateScroll(); - } - - /// - public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) - { - base.ItemsChanged(items, e); - - var panel = VirtualizingPanel; - - if (items != null) - { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - CreateAndRemoveContainers(); - - if (e.NewStartingIndex < NextIndex) - { - RecycleContainers(); - } - - panel.ForceInvalidateMeasure(); - break; - - case NotifyCollectionChangedAction.Remove: - if ((e.OldStartingIndex >= FirstIndex && e.OldStartingIndex < NextIndex) || - panel.Children.Count > ItemCount) - { - RecycleContainersOnRemove(); - } - - panel.ForceInvalidateMeasure(); - break; - - case NotifyCollectionChangedAction.Move: - case NotifyCollectionChangedAction.Replace: - RecycleContainers(); - break; - - case NotifyCollectionChangedAction.Reset: - RecycleContainersOnRemove(); - CreateAndRemoveContainers(); - panel.ForceInvalidateMeasure(); - break; - } - } - else - { - Owner.ItemContainerGenerator.Clear(); - VirtualizingPanel.Children.Clear(); - FirstIndex = NextIndex = 0; - } - - // If we are scrolled to view a partially visible last item but controls were added - // then we need to return to a non-offset scroll position. - if (panel.PixelOffset != 0 && FirstIndex + panel.Children.Count < ItemCount) - { - panel.PixelOffset = 0; - RecycleContainersForMove(1); - } - - InvalidateScroll(); - } - - public override IControl GetControlInDirection(NavigationDirection direction, IControl from) - { - var generator = Owner.ItemContainerGenerator; - var panel = VirtualizingPanel; - var itemIndex = generator.IndexFromContainer(from); - var vertical = VirtualizingPanel.ScrollDirection == Orientation.Vertical; - - if (itemIndex == -1) - { - return null; - } - - var newItemIndex = -1; - - switch (direction) - { - case NavigationDirection.First: - newItemIndex = 0; - break; - - case NavigationDirection.Last: - newItemIndex = ItemCount - 1; - break; - - case NavigationDirection.Up: - if (vertical) - { - newItemIndex = itemIndex - 1; - } - - break; - case NavigationDirection.Down: - if (vertical) - { - newItemIndex = itemIndex + 1; - } - - break; - - case NavigationDirection.Left: - if (!vertical) - { - newItemIndex = itemIndex - 1; - } - break; - - case NavigationDirection.Right: - if (!vertical) - { - newItemIndex = itemIndex + 1; - } - break; - - case NavigationDirection.PageUp: - newItemIndex = Math.Max(0, itemIndex - (int)ViewportValue); - break; - - case NavigationDirection.PageDown: - newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue); - break; - } - - return ScrollIntoViewCore(newItemIndex); - } - - /// - public override void ScrollIntoView(int index) - { - if (index != -1) - { - ScrollIntoViewCore(index); - } - } - - /// - /// Creates and removes containers such that we have at most enough containers to fill - /// the panel. - /// - private void CreateAndRemoveContainers() - { - var generator = Owner.ItemContainerGenerator; - var panel = VirtualizingPanel; - - if (!panel.IsFull && Items != null && panel.IsAttachedToVisualTree) - { - var index = NextIndex; - var step = 1; - - while (!panel.IsFull && index >= 0) - { - if (index >= ItemCount) - { - // We can fit more containers in the panel, but we're at the end of the - // items. If we're scrolled to the top (FirstIndex == 0), then there are - // no more items to create. Otherwise, go backwards adding containers to - // the beginning of the panel. - if (FirstIndex == 0) - { - break; - } - else - { - index = FirstIndex - 1; - step = -1; - } - } - - var materialized = generator.Materialize(index, Items.ElementAt(index)); - - if (step == 1) - { - panel.Children.Add(materialized.ContainerControl); - } - else - { - panel.Children.Insert(0, materialized.ContainerControl); - } - - index += step; - } - - if (step == 1) - { - NextIndex = index; - } - else - { - NextIndex = ItemCount; - FirstIndex = index + 1; - } - } - - if (panel.OverflowCount > 0) - { - if (_anchor <= FirstIndex) - { - RemoveContainers(panel.OverflowCount); - } - } - } - - /// - /// Updates the containers in the panel to make sure they are displaying the correct item - /// based on . - /// - /// - /// This method requires that + the number of - /// materialized containers is not more than . - /// - private void RecycleContainers() - { - var panel = VirtualizingPanel; - var generator = Owner.ItemContainerGenerator; - var containers = generator.Containers.ToList(); - var itemIndex = FirstIndex; - - foreach (var container in containers) - { - var item = Items.ElementAt(itemIndex); - - if (!object.Equals(container.Item, item)) - { - if (!generator.TryRecycle(itemIndex, itemIndex, item)) - { - throw new NotImplementedException(); - } - } - - ++itemIndex; - } - } - - /// - /// Recycles containers when a move occurs. - /// - /// The delta of the move. - /// - /// If the move is less than a page, then this method moves the containers for the items - /// that are still visible to the correct place, and recycles and moves the others. For - /// example: if there are 20 items and 10 containers visible and the user scrolls 5 - /// items down, then the bottom 5 containers will be moved to the top and the top 5 will - /// be moved to the bottom and recycled to display the newly visible item. Updates - /// and - /// with their new values. - /// - private void RecycleContainersForMove(int delta) - { - var panel = VirtualizingPanel; - var generator = Owner.ItemContainerGenerator; - - //validate delta it should never overflow last index or generate index < 0 - delta = MathUtilities.Clamp(delta, -FirstIndex, ItemCount - FirstIndex - panel.Children.Count); - - var sign = delta < 0 ? -1 : 1; - var count = Math.Min(Math.Abs(delta), panel.Children.Count); - var move = count < panel.Children.Count; - var first = delta < 0 && move ? panel.Children.Count + delta : 0; - - for (var i = 0; i < count; ++i) - { - var oldItemIndex = FirstIndex + first + i; - var newItemIndex = oldItemIndex + delta + ((panel.Children.Count - count) * sign); - - var item = Items.ElementAt(newItemIndex); - - if (!generator.TryRecycle(oldItemIndex, newItemIndex, item)) - { - throw new NotImplementedException(); - } - } - - if (move) - { - if (delta > 0) - { - panel.Children.MoveRange(first, count, panel.Children.Count); - } - else - { - panel.Children.MoveRange(first, count, 0); - } - } - - FirstIndex += delta; - NextIndex += delta; - } - - /// - /// Recycles containers due to items being removed. - /// - private void RecycleContainersOnRemove() - { - var panel = VirtualizingPanel; - - if (NextIndex <= ItemCount) - { - // Items have been removed but FirstIndex..NextIndex is still a valid range in the - // items, so just recycle the containers to adapt to the new state. - RecycleContainers(); - } - else - { - // Items have been removed and now the range FirstIndex..NextIndex goes out of - // the item bounds. Remove any excess containers, try to scroll up and then recycle - // the containers to make sure they point to the correct item. - var newFirstIndex = Math.Max(0, FirstIndex - (NextIndex - ItemCount)); - var delta = newFirstIndex - FirstIndex; - var newNextIndex = NextIndex + delta; - - if (newNextIndex > ItemCount) - { - RemoveContainers(newNextIndex - ItemCount); - } - - if (delta != 0) - { - RecycleContainersForMove(delta); - } - - RecycleContainers(); - } - } - - /// - /// Removes the specified number of containers from the end of the panel and updates - /// . - /// - /// The number of containers to remove. - private void RemoveContainers(int count) - { - var index = VirtualizingPanel.Children.Count - count; - - VirtualizingPanel.Children.RemoveRange(index, count); - Owner.ItemContainerGenerator.Dematerialize(FirstIndex + index, count); - NextIndex -= count; - } - - /// - /// Scrolls the item with the specified index into view. - /// - /// The item index. - /// The container that was brought into view. - private IControl ScrollIntoViewCore(int index) - { - var panel = VirtualizingPanel; - var generator = Owner.ItemContainerGenerator; - var newOffset = -1.0; - - if (index >= 0 && index < ItemCount) - { - if (index <= FirstIndex) - { - newOffset = index; - } - else if (index >= NextIndex) - { - newOffset = index - Math.Ceiling(ViewportValue - 1); - } - - if (newOffset != -1) - { - OffsetValue = newOffset; - } - - var container = generator.ContainerFromIndex(index); - var layoutManager = (Owner.GetVisualRoot() as ILayoutRoot)?.LayoutManager; - - // We need to do a layout here because it's possible that the container we moved to - // is only partially visible due to differing item sizes. If the container is only - // partially visible, scroll again. Don't do this if there's no layout manager: - // it means we're running a unit test. - if (container != null && layoutManager != null) - { - _anchor = index; - layoutManager.ExecuteLayoutPass(); - _anchor = -1; - - if (newOffset != -1 && newOffset != OffsetValue) - { - OffsetValue = newOffset; - } - - if (panel.ScrollDirection == Orientation.Vertical) - { - if (container.Bounds.Y < panel.Bounds.Y || container.Bounds.Bottom > panel.Bounds.Bottom) - { - OffsetValue += 1; - } - } - else - { - if (container.Bounds.X < panel.Bounds.X || container.Bounds.Right > panel.Bounds.Right) - { - OffsetValue += 1; - } - } - } - - return container; - } - - return null; - } - - /// - /// Ensures an offset value is within the value range. - /// - /// The value. - /// The coerced value. - private double CoerceOffset(double value) - { - var max = Math.Max(ExtentValue - ViewportValue, 0); - return MathUtilities.Clamp(value, 0, max); - } - } -} diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 80c9e972d53..dba131d0546 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -1,179 +1,80 @@ -using System; -using System.Collections.Specialized; -using Avalonia.Controls.Primitives; +using System.Collections.Generic; +using System.Linq; using Avalonia.Input; -using static Avalonia.Utilities.MathUtilities; +using Avalonia.Layout; +using Avalonia.VisualTree; namespace Avalonia.Controls.Presenters { /// - /// Displays items inside an . + /// Displays items in a . /// - public class ItemsPresenter : ItemsPresenterBase, ILogicalScrollable + public class ItemsPresenter : ItemsRepeater, IItemsPresenter { - /// - /// Defines the property. - /// - public static readonly StyledProperty VirtualizationModeProperty = - AvaloniaProperty.Register( - nameof(VirtualizationMode), - defaultValue: ItemVirtualizationMode.None); + private IItemsPresenterHost _host; - private bool _canHorizontallyScroll; - private bool _canVerticallyScroll; - private EventHandler _scrollInvalidated; - - /// - /// Initializes static members of the class. - /// - static ItemsPresenter() + public IEnumerable RealizedElements { - KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue( - typeof(ItemsPresenter), - KeyboardNavigationMode.Once); - - VirtualizationModeProperty.Changed - .AddClassHandler((x, e) => x.VirtualizationModeChanged(e)); - } - - /// - /// Gets or sets the virtualization mode for the items. - /// - public ItemVirtualizationMode VirtualizationMode - { - get { return GetValue(VirtualizationModeProperty); } - set { SetValue(VirtualizationModeProperty, value); } - } - - /// - /// Gets or sets a value indicating whether the content can be scrolled horizontally. - /// - bool ILogicalScrollable.CanHorizontallyScroll - { - get { return _canHorizontallyScroll; } - set + get { - _canHorizontallyScroll = value; - InvalidateMeasure(); - } - } + foreach (var child in Children) + { + var virtInfo = GetVirtualizationInfo(child); - /// - /// Gets or sets a value indicating whether the content can be scrolled horizontally. - /// - bool ILogicalScrollable.CanVerticallyScroll - { - get { return _canVerticallyScroll; } - set - { - _canVerticallyScroll = value; - InvalidateMeasure(); + if (virtInfo?.IsRealized == true) + { + yield return child; + } + } } } - /// - bool ILogicalScrollable.IsLogicalScrollEnabled - { - get { return Virtualizer?.IsLogicalScrollEnabled ?? false; } - } - - /// - Size IScrollable.Extent => Virtualizer?.Extent ?? Size.Empty; - /// - Vector IScrollable.Offset + public bool ScrollIntoView(int index) { - get { return Virtualizer?.Offset ?? new Vector(); } - set + var layoutManager = (VisualRoot as ILayoutRoot)?.LayoutManager; + + if (index >= 0 && index < ItemsSourceView.Count && layoutManager != null) { - if (Virtualizer != null) + var element = GetOrCreateElement(index); + + if (element != null) { - Virtualizer.Offset = CoerceOffset(value); + layoutManager.ExecuteLayoutPass(); + element.BringIntoView(); + return true; } } - } - - /// - Size IScrollable.Viewport => Virtualizer?.Viewport ?? Bounds.Size; - - /// - event EventHandler ILogicalScrollable.ScrollInvalidated - { - add => _scrollInvalidated += value; - remove => _scrollInvalidated -= value; - } - - /// - Size ILogicalScrollable.ScrollSize => new Size(ScrollViewer.DefaultSmallChange, 1); - - /// - Size ILogicalScrollable.PageScrollSize => Virtualizer?.Viewport ?? new Size(16, 16); - internal ItemVirtualizer Virtualizer { get; private set; } - - /// - bool ILogicalScrollable.BringIntoView(IControl target, Rect targetRect) - { return false; } - /// - IControl ILogicalScrollable.GetControlInDirection(NavigationDirection direction, IControl from) - { - return Virtualizer?.GetControlInDirection(direction, from); - } - - /// - void ILogicalScrollable.RaiseScrollInvalidated(EventArgs e) - { - _scrollInvalidated?.Invoke(this, e); - } - - public override void ScrollIntoView(int index) - { - Virtualizer?.ScrollIntoView(index); - } - - /// - protected override Size MeasureOverride(Size availableSize) - { - return Virtualizer?.MeasureOverride(availableSize) ?? Size.Empty; - } - - protected override Size ArrangeOverride(Size finalSize) + protected override void OnGotFocus(GotFocusEventArgs e) { - return Virtualizer?.ArrangeOverride(finalSize) ?? Size.Empty; - } + base.OnGotFocus(e); - /// - protected override void PanelCreated(IPanel panel) - { - Virtualizer?.Dispose(); - Virtualizer = ItemVirtualizer.Create(this); - _scrollInvalidated?.Invoke(this, EventArgs.Empty); + var child = ((IVisual)e.Source).GetSelfAndVisualAncestors() + .FirstOrDefault(x => x.VisualParent == this); - KeyboardNavigation.SetTabNavigation( - (InputElement)Panel, - KeyboardNavigation.GetTabNavigation(this)); + if (child != null) + { + KeyboardNavigation.SetTabOnceActiveElement(this, (IInputElement)child); + } } - protected override void ItemsChanged(NotifyCollectionChangedEventArgs e) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - Virtualizer?.ItemsChanged(Items, e); - } + if (change.Property == TemplatedParentProperty) + { + _host = change.NewValue.GetValueOrDefault(); - private Vector CoerceOffset(Vector value) - { - var scrollable = (ILogicalScrollable)this; - var maxX = Math.Max(scrollable.Extent.Width - scrollable.Viewport.Width, 0); - var maxY = Math.Max(scrollable.Extent.Height - scrollable.Viewport.Height, 0); - return new Vector(Clamp(value.X, 0, maxX), Clamp(value.Y, 0, maxY)); - } + if (_host is object) + { + _host?.RegisterItemsPresenter(this); + ItemTemplate = _host.ElementFactory; + } + } - private void VirtualizationModeChanged(AvaloniaPropertyChangedEventArgs e) - { - Virtualizer?.Dispose(); - Virtualizer = ItemVirtualizer.Create(this); - _scrollInvalidated?.Invoke(this, EventArgs.Empty); + base.OnPropertyChanged(change); } } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs deleted file mode 100644 index 52f173fc711..00000000000 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Specialized; -using Avalonia.Collections; -using Avalonia.Controls.Generators; -using Avalonia.Controls.Templates; -using Avalonia.Controls.Utils; -using Avalonia.Styling; - -namespace Avalonia.Controls.Presenters -{ - /// - /// Base class for controls that present items inside an . - /// - public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl - { - /// - /// Defines the property. - /// - public static readonly DirectProperty ItemsProperty = - ItemsControl.ItemsProperty.AddOwner(o => o.Items, (o, v) => o.Items = v); - - /// - /// Defines the property. - /// - public static readonly StyledProperty> ItemsPanelProperty = - ItemsControl.ItemsPanelProperty.AddOwner(); - - /// - /// Defines the property. - /// - public static readonly StyledProperty ItemTemplateProperty = - ItemsControl.ItemTemplateProperty.AddOwner(); - - private IEnumerable _items; - private IDisposable _itemsSubscription; - private bool _createdPanel; - private IItemContainerGenerator _generator; - - /// - /// Initializes static members of the class. - /// - static ItemsPresenterBase() - { - TemplatedParentProperty.Changed.AddClassHandler((x,e) => x.TemplatedParentChanged(e)); - } - - /// - /// Gets or sets the items to be displayed. - /// - public IEnumerable Items - { - get - { - return _items; - } - - set - { - _itemsSubscription?.Dispose(); - _itemsSubscription = null; - - if (!IsHosted && _createdPanel && value is INotifyCollectionChanged incc) - { - _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged); - } - - SetAndRaise(ItemsProperty, ref _items, value); - - if (_createdPanel) - { - ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - } - } - - /// - /// Gets the item container generator. - /// - public IItemContainerGenerator ItemContainerGenerator - { - get - { - if (_generator == null) - { - _generator = CreateItemContainerGenerator(); - } - - return _generator; - } - - internal set - { - if (_generator != null) - { - throw new InvalidOperationException("ItemContainerGenerator already created."); - } - - _generator = value; - } - } - - /// - /// Gets or sets a template which creates the used to display the items. - /// - public ITemplate ItemsPanel - { - get { return GetValue(ItemsPanelProperty); } - set { SetValue(ItemsPanelProperty, value); } - } - - /// - /// Gets or sets the data template used to display the items in the control. - /// - public IDataTemplate ItemTemplate - { - get { return GetValue(ItemTemplateProperty); } - set { SetValue(ItemTemplateProperty, value); } - } - - /// - /// Gets the panel used to display the items. - /// - public IPanel Panel - { - get; - private set; - } - - protected bool IsHosted => TemplatedParent is IItemsPresenterHost; - - /// - public override sealed void ApplyTemplate() - { - if (!_createdPanel) - { - CreatePanel(); - } - } - - /// - public virtual void ScrollIntoView(int index) - { - } - - /// - void IItemsPresenter.ItemsChanged(NotifyCollectionChangedEventArgs e) - { - if (Panel != null) - { - ItemsChanged(e); - } - } - - /// - /// Creates the for the control. - /// - /// - /// An or null. - /// - protected virtual IItemContainerGenerator CreateItemContainerGenerator() - { - var i = TemplatedParent as ItemsControl; - var result = i?.ItemContainerGenerator; - - if (result == null) - { - result = new ItemContainerGenerator(this); - result.ItemTemplate = ItemTemplate; - } - - return result; - } - - /// - protected override Size MeasureOverride(Size availableSize) - { - Panel.Measure(availableSize); - return Panel.DesiredSize; - } - - /// - protected override Size ArrangeOverride(Size finalSize) - { - Panel.Arrange(new Rect(finalSize)); - return finalSize; - } - - /// - /// Called when the is created. - /// - /// The panel. - protected virtual void PanelCreated(IPanel panel) - { - } - - /// - /// Called when the items for the presenter change, either because - /// has been set, the items collection has been modified, or the panel has been created. - /// - /// A description of the change. - /// - /// The panel is guaranteed to be created when this method is called. - /// - protected virtual void ItemsChanged(NotifyCollectionChangedEventArgs e) - { - ItemContainerSync.ItemsChanged(this, Items, e); - } - - /// - /// Creates the when is called for the first - /// time. - /// - private void CreatePanel() - { - Panel = ItemsPanel.Build(); - Panel.SetValue(TemplatedParentProperty, TemplatedParent); - - LogicalChildren.Clear(); - VisualChildren.Clear(); - LogicalChildren.Add(Panel); - VisualChildren.Add(Panel); - - _createdPanel = true; - - if (!IsHosted && _itemsSubscription == null && Items is INotifyCollectionChanged incc) - { - _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged); - } - - PanelCreated(Panel); - } - - /// - /// Called when the collection changes. - /// - /// The sender. - /// The event args. - private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (_createdPanel) - { - ItemsChanged(e); - } - } - - private void TemplatedParentChanged(AvaloniaPropertyChangedEventArgs e) - { - (e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this); - } - } -} diff --git a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs index be3c100ecfb..73eec33da51 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs @@ -1,6 +1,7 @@ using Avalonia.Collections; using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; using Avalonia.LogicalTree; namespace Avalonia.Controls.Primitives @@ -16,6 +17,12 @@ public class HeaderedItemsControl : ItemsControl, IContentPresenterHost public static readonly StyledProperty HeaderProperty = HeaderedContentControl.HeaderProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty HeaderTemplateProperty = + AvaloniaProperty.Register(nameof(HeaderTemplate)); + /// /// Initializes static members of the class. /// @@ -33,6 +40,15 @@ public object Header set { SetValue(HeaderProperty, value); } } + /// + /// Gets or sets the data template used to display the header of the control. + /// + public IDataTemplate HeaderTemplate + { + get { return GetValue(HeaderTemplateProperty); } + set { SetValue(HeaderTemplateProperty, value); } + } + /// /// Gets the header presenter from the control's template. /// diff --git a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs index 78b5771b80d..b0d19ea5b46 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs @@ -1,8 +1,10 @@ using Avalonia.Collections; -using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; using Avalonia.LogicalTree; +#nullable enable + namespace Avalonia.Controls.Primitives { /// @@ -13,9 +15,15 @@ public class HeaderedSelectingItemsControl : SelectingItemsControl, IContentPres /// /// Defines the property. /// - public static readonly StyledProperty HeaderProperty = + public static readonly StyledProperty HeaderProperty = HeaderedContentControl.HeaderProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty HeaderTemplateProperty = + HeaderedContentControl.HeaderTemplateProperty.AddOwner(); + /// /// Initializes static members of the class. /// @@ -27,16 +35,25 @@ static HeaderedSelectingItemsControl() /// /// Gets or sets the content of the control's header. /// - public object Header + public object? Header { get { return GetValue(HeaderProperty); } set { SetValue(HeaderProperty, value); } } + /// + /// Gets or sets the data template used to display the header content of the control. + /// + public IDataTemplate? HeaderTemplate + { + get => GetValue(HeaderTemplateProperty); + set => SetValue(HeaderTemplateProperty, value); + } + /// /// Gets the header presenter from the control's template. /// - public IContentPresenter HeaderPresenter + public IContentPresenter? HeaderPresenter { get; private set; diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 5f8c5da2f8a..ddfd4a1149c 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -4,7 +4,6 @@ using System.Collections.Specialized; using System.ComponentModel; using System.Linq; -using Avalonia.Controls.Generators; using Avalonia.Controls.Selection; using Avalonia.Controls.Utils; using Avalonia.Data; @@ -260,7 +259,7 @@ public override void EndInit() /// Scrolls the specified item into view. /// /// The item. - public void ScrollIntoView(object item) => ScrollIntoView(IndexOf(Items, item)); + public void ScrollIntoView(object item) => ScrollIntoView(ItemsView.IndexOf(item)); /// /// Tries to get the container that was the source of an event. @@ -273,8 +272,9 @@ public override void EndInit() while (parent != null) { - if (parent is IControl control && control.LogicalParent == this - && ItemContainerGenerator?.IndexFromContainer(control) != -1) + if (parent is IControl control && + control.LogicalParent == this && + GetContainerIndex(control) != -1) { return control; } @@ -285,9 +285,11 @@ public override void EndInit() return null; } - protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + protected override void ItemsViewChanged(ItemsSourceView? oldView, ItemsSourceView newView) { - base.ItemsCollectionChanged(sender, e); + base.ItemsViewChanged(oldView, newView); + + AddSelectedContainersToSelection(0, ItemsView); if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0) { @@ -295,56 +297,47 @@ protected override void ItemsCollectionChanged(object sender, NotifyCollectionCh } } - /// - protected override void OnContainersMaterialized(ItemContainerEventArgs e) + protected override void ItemsViewCollectionChanged(NotifyCollectionChangedEventArgs e) { - base.OnContainersMaterialized(e); + base.ItemsViewCollectionChanged(e); - foreach (var container in e.Containers) + if (e.Action == NotifyCollectionChangedAction.Add || + e.Action == NotifyCollectionChangedAction.Replace) { - if ((container.ContainerControl as ISelectable)?.IsSelected == true) - { - Selection.Select(container.Index); - MarkContainerSelected(container.ContainerControl, true); - } - else - { - var selected = Selection.IsSelected(container.Index); - MarkContainerSelected(container.ContainerControl, selected); - } + AddSelectedContainersToSelection(e.NewStartingIndex, e.NewItems); + } + + if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0) + { + SelectedIndex = 0; } } /// - protected override void OnContainersDematerialized(ItemContainerEventArgs e) + protected override void OnContainerPrepared(ElementPreparedEventArgs e) { - base.OnContainersDematerialized(e); + base.OnContainerPrepared(e); + MarkContainerSelected(e.Element, _selection?.IsSelected(e.Index) ?? false); + } - var panel = (InputElement)Presenter.Panel; + protected override void OnContainerClearing(ElementClearingEventArgs e) + { + base.OnContainerClearing(e); + MarkContainerSelected(e.Element, false); - if (panel != null) + if (Presenter is InputElement inputElement) { - foreach (var container in e.Containers) + if (KeyboardNavigation.GetTabOnceActiveElement(inputElement) == e.Element) { - if (KeyboardNavigation.GetTabOnceActiveElement(panel) == container.ContainerControl) - { - KeyboardNavigation.SetTabOnceActiveElement(panel, null); - break; - } + KeyboardNavigation.SetTabOnceActiveElement(inputElement, null); } } } - protected override void OnContainersRecycled(ItemContainerEventArgs e) + protected override void OnContainerIndexChanged(ElementIndexChangedEventArgs e) { - foreach (var i in e.Containers) - { - if (i.ContainerControl != null && i.Item != null) - { - bool selected = Selection.IsSelected(i.Index); - MarkContainerSelected(i.ContainerControl, selected); - } - } + base.OnContainerIndexChanged(e); + MarkContainerSelected(e.Element, _selection?.IsSelected(e.NewIndex) ?? false); } /// @@ -438,7 +431,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs /// True if the selection was moved; otherwise false. protected bool MoveSelection(NavigationDirection direction, bool wrap) { - var from = SelectedIndex != -1 ? ItemContainerGenerator.ContainerFromIndex(SelectedIndex) : null; + var from = SelectedIndex != -1 ? TryGetContainer(SelectedIndex) : null; return MoveSelection(from, direction, wrap); } @@ -451,10 +444,10 @@ protected bool MoveSelection(NavigationDirection direction, bool wrap) /// True if the selection was moved; otherwise false. protected bool MoveSelection(IControl? from, NavigationDirection direction, bool wrap) { - if (Presenter?.Panel is INavigableContainer container && + if (Presenter is INavigableContainer container && GetNextControl(container, direction, from, wrap) is IControl next) { - var index = ItemContainerGenerator.IndexFromContainer(next); + var index = GetContainerIndex(next); if (index != -1) { @@ -530,11 +523,11 @@ protected void UpdateSelection( Selection.Select(index); } - if (Presenter?.Panel != null) + if (Presenter != null) { - var container = ItemContainerGenerator.ContainerFromIndex(index); + var container = TryGetContainer(index); KeyboardNavigation.SetTabOnceActiveElement( - (InputElement)Presenter.Panel, + (InputElement)Presenter, container); } } @@ -554,7 +547,7 @@ protected void UpdateSelection( bool toggleModifier = false, bool rightButton = false) { - var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1; + var index = GetContainerIndex(container); if (index != -1) { @@ -630,7 +623,7 @@ private void OnSelectionModelSelectionChanged(object sender, SelectionModelSelec { void Mark(int index, bool selected) { - var container = ItemContainerGenerator.ContainerFromIndex(index); + var container = TryGetContainer(index); if (container != null) { @@ -688,7 +681,7 @@ private void ContainerSelectionChanged(RoutedEventArgs e) if (control != null && selectable != null && control.LogicalParent == this && - ItemContainerGenerator?.IndexFromContainer(control) != -1) + GetContainerIndex(control) != -1) { UpdateSelection(control, selectable.IsSelected); } @@ -736,53 +729,24 @@ private bool MarkContainerSelected(IControl container, bool selected) private void MarkContainersUnselected() { - foreach (var container in ItemContainerGenerator.Containers) - { - MarkContainerSelected(container.ContainerControl, false); - } - } - - /// - /// Sets an item container's 'selected' class or . - /// - /// The index of the item. - /// Whether the item should be selected or deselected. - private void MarkItemSelected(int index, bool selected) - { - var container = ItemContainerGenerator?.ContainerFromIndex(index); - - if (container != null) - { - MarkContainerSelected(container, selected); - } - } - - /// - /// Sets an item container's 'selected' class or . - /// - /// The item. - /// Whether the item should be selected or deselected. - private int MarkItemSelected(object item, bool selected) - { - var index = IndexOf(Items, item); - - if (index != -1) + if (Presenter is IPanel panel) { - MarkItemSelected(index, selected); + foreach (var container in panel.Children) + { + MarkContainerSelected(container, false); + } } - - return index; } private void UpdateContainerSelection() { - if (Presenter?.Panel is IPanel panel) + if (Presenter is IPanel panel) { foreach (var container in panel.Children) { MarkContainerSelected( container, - Selection.IsSelected(ItemContainerGenerator.IndexFromContainer(container))); + Selection.IsSelected(GetContainerIndex(container)) != false); } } } @@ -845,5 +809,18 @@ private void DeinitializeSelectionModel(ISelectionModel? model) model.SelectionChanged -= OnSelectionModelSelectionChanged; } } + + private void AddSelectedContainersToSelection(int index, IEnumerable items) + { + foreach (var i in items) + { + if ((i as ISelectable)?.IsSelected == true) + { + Selection.Select(index); + } + + ++index; + } + } } } diff --git a/src/Avalonia.Controls/Primitives/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs index f8f7674da81..742549202fd 100644 --- a/src/Avalonia.Controls/Primitives/TabStrip.cs +++ b/src/Avalonia.Controls/Primitives/TabStrip.cs @@ -8,14 +8,14 @@ namespace Avalonia.Controls.Primitives { public class TabStrip : SelectingItemsControl { - private static readonly FuncTemplate DefaultPanel = - new FuncTemplate(() => new WrapPanel { Orientation = Orientation.Horizontal }); - static TabStrip() { + LayoutProperty.OverrideDefaultValue(new NonVirtualizingStackLayout + { + Orientation = Orientation.Horizontal + }); SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false); - ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); } protected override IItemContainerGenerator CreateItemContainerGenerator() diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index 416b1e28248..5c3b3764a52 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -635,7 +635,7 @@ IControl GetElement() // Clear flag virtInfo.MustClearDataContext = false; - if (data != element) + if (!(data is IControl)) { // Prepare the element element.DataContext = data; diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index bdb0fa32704..c716a160ec0 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -146,7 +146,7 @@ public Rect GetLayoutVisibleWindow() { var visibleWindow = _visibleWindow; - if (_makeAnchorElement != null) + if (_makeAnchorElement != null && _isAnchorOutsideRealizedRange) { // The anchor is not necessarily laid out yet. Its position should default // to zero and the layout origin is expected to change once layout is done. diff --git a/src/Avalonia.Controls/Selection/IndexPath.cs b/src/Avalonia.Controls/Selection/IndexPath.cs new file mode 100644 index 00000000000..0df501086d6 --- /dev/null +++ b/src/Avalonia.Controls/Selection/IndexPath.cs @@ -0,0 +1,249 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections.Generic; +using System.Linq; + +#nullable enable + +namespace Avalonia.Controls.Selection +{ + public readonly struct IndexPath : IComparable, IEquatable + { + public static readonly IndexPath Unselected = default; + + private readonly int _index; + private readonly int[]? _path; + + public IndexPath(int index) + { + _index = index + 1; + _path = null; + } + + public IndexPath(int groupIndex, int itemIndex) + { + _index = 0; + _path = new[] { groupIndex, itemIndex }; + } + + public IndexPath(params int[] indexes) + { + _index = 0; + _path = indexes; + } + + public IndexPath(IEnumerable? indexes) + { + if (indexes != null) + { + _index = 0; + _path = indexes.ToArray(); + } + else + { + _index = 0; + _path = null; + } + } + + private IndexPath(int[] basePath, int index) + { + basePath = basePath ?? throw new ArgumentNullException(nameof(basePath)); + + _index = 0; + _path = new int[basePath.Length + 1]; + Array.Copy(basePath, _path, basePath.Length); + _path[basePath.Length] = index; + } + + public int GetSize() => _path?.Length ?? (_index == 0 ? 0 : 1); + + public int GetAt(int index) + { + if (index >= GetSize()) + { + throw new IndexOutOfRangeException(); + } + + return _path?[index] ?? (_index - 1); + } + + public int CompareTo(IndexPath other) + { + var rhsPath = other; + int compareResult = 0; + int lhsCount = GetSize(); + int rhsCount = rhsPath.GetSize(); + + if (lhsCount == 0 || rhsCount == 0) + { + // one of the paths are empty, compare based on size + compareResult = (lhsCount - rhsCount); + } + else + { + // both paths are non-empty, but can be of different size + for (int i = 0; i < Math.Min(lhsCount, rhsCount); i++) + { + if (GetAt(i) < rhsPath.GetAt(i)) + { + compareResult = -1; + break; + } + else if (GetAt(i) > rhsPath.GetAt(i)) + { + compareResult = 1; + break; + } + } + + // if both match upto min(lhsCount, rhsCount), compare based on size + compareResult = compareResult == 0 ? (lhsCount - rhsCount) : compareResult; + } + + if (compareResult != 0) + { + compareResult = compareResult > 0 ? 1 : -1; + } + + return compareResult; + } + + public IndexPath CloneWithChildIndex(int childIndex) + { + if (_path != null) + { + return new IndexPath(_path, childIndex); + } + else if (_index != 0) + { + return new IndexPath(_index - 1, childIndex); + } + else + { + return new IndexPath(childIndex); + } + } + + public override string ToString() + { + if (_path != null) + { + return "R" + string.Join(".", _path); + } + else if (_index != 0) + { + return "R" + (_index - 1); + } + else + { + return "R"; + } + } + + public override bool Equals(object? obj) => obj is IndexPath other && Equals(other); + + public bool Equals(IndexPath other) => CompareTo(other) == 0; + + public override int GetHashCode() + { + var hashCode = -504981047; + + if (_path != null) + { + foreach (var i in _path) + { + hashCode = hashCode * -1521134295 + i.GetHashCode(); + } + } + else + { + hashCode = hashCode * -1521134295 + _index.GetHashCode(); + } + + return hashCode; + } + + internal bool IsAncestorOf(in IndexPath other) + { + if (other.GetSize() <= GetSize()) + { + return false; + } + + var size = GetSize(); + + for (int i = 0; i < size; i++) + { + if (GetAt(i) != other.GetAt(i)) + { + return false; + } + } + + return true; + } + + internal int? GetLeaf() + { + if (GetSize() > 0) + { + return GetAt(GetSize() - 1); + } + + return null; + } + + internal IndexPath GetParent() + { + if (GetSize() == 0) + { + throw new InvalidOperationException("Cannot get parent of root index."); + } + + if (_path is null) + { + return default; + } + else if (_path.Length == 2) + { + return new IndexPath(_path[0]); + } + else + { + var path = new int[_path.Length - 1]; + Array.Copy(_path, path, _path.Length - 1); + return new IndexPath(path); + } + } + + internal int[] ToArray() + { + var result = new int[GetSize()]; + + if (_path is object) + { + _path.CopyTo(result, 0); + } + else if (result.Length > 0) + { + result[0] = _index - 1; + } + + return result; + } + + public static bool operator <(IndexPath x, IndexPath y) { return x.CompareTo(y) < 0; } + public static bool operator >(IndexPath x, IndexPath y) { return x.CompareTo(y) > 0; } + public static bool operator <=(IndexPath x, IndexPath y) { return x.CompareTo(y) <= 0; } + public static bool operator >=(IndexPath x, IndexPath y) { return x.CompareTo(y) >= 0; } + public static bool operator ==(IndexPath x, IndexPath y) { return x.CompareTo(y) == 0; } + public static bool operator !=(IndexPath x, IndexPath y) { return x.CompareTo(y) != 0; } + public static bool operator ==(IndexPath? x, IndexPath? y) { return (x ?? default).CompareTo(y ?? default) == 0; } + public static bool operator !=(IndexPath? x, IndexPath? y) { return (x ?? default).CompareTo(y ?? default) != 0; } + } +} diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index 9e087b7fd32..bf2d9ced07f 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -72,12 +72,10 @@ IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInp { case NavigationDirection.Up: case NavigationDirection.Previous: - case NavigationDirection.PageUp: result = GetControlInDirection(NavigationDirection.Last, null); break; case NavigationDirection.Down: case NavigationDirection.Next: - case NavigationDirection.PageDown: result = GetControlInDirection(NavigationDirection.First, null); break; } @@ -88,12 +86,10 @@ IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInp { case NavigationDirection.Left: case NavigationDirection.Previous: - case NavigationDirection.PageUp: result = GetControlInDirection(NavigationDirection.Last, null); break; case NavigationDirection.Right: case NavigationDirection.Next: - case NavigationDirection.PageDown: result = GetControlInDirection(NavigationDirection.First, null); break; } diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index f81e355a7d2..dce5683f832 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -4,11 +4,14 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.VisualTree; +#nullable enable + namespace Avalonia.Controls { /// @@ -43,30 +46,27 @@ public class TabControl : SelectingItemsControl, IContentPresenterHost /// /// The selected content property /// - public static readonly StyledProperty SelectedContentProperty = - AvaloniaProperty.Register(nameof(SelectedContent)); + public static readonly StyledProperty SelectedContentProperty = + AvaloniaProperty.Register(nameof(SelectedContent)); /// /// The selected content template property /// - public static readonly StyledProperty SelectedContentTemplateProperty = - AvaloniaProperty.Register(nameof(SelectedContentTemplate)); - - /// - /// The default value for the property. - /// - private static readonly FuncTemplate DefaultPanel = - new FuncTemplate(() => new WrapPanel()); + public static readonly StyledProperty SelectedContentTemplateProperty = + AvaloniaProperty.Register(nameof(SelectedContentTemplate)); /// /// Initializes static members of the class. /// static TabControl() { + LayoutProperty.OverrideDefaultValue(new NonVirtualizingStackLayout + { + Orientation = Orientation.Horizontal + }); + SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); - ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); AffectsMeasure(TabStripPlacementProperty); - SelectedIndexProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent(e)); } /// @@ -111,7 +111,7 @@ public IDataTemplate ContentTemplate /// /// The content of the selected tab. /// - public object SelectedContent + public object? SelectedContent { get { return GetValue(SelectedContentProperty); } internal set { SetValue(SelectedContentProperty, value); } @@ -123,15 +123,13 @@ public object SelectedContent /// /// The content template of the selected tab. /// - public IDataTemplate SelectedContentTemplate + public IDataTemplate? SelectedContentTemplate { get { return GetValue(SelectedContentTemplateProperty); } internal set { SetValue(SelectedContentTemplateProperty, value); } } - internal ItemsPresenter ItemsPresenterPart { get; private set; } - - internal IContentPresenter ContentPart { get; private set; } + internal IContentPresenter? ContentPart { get; private set; } /// IAvaloniaList IContentPresenterHost.LogicalChildren => LogicalChildren; @@ -142,46 +140,43 @@ bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) return RegisterContentPresenter(presenter); } - protected override void OnContainersMaterialized(ItemContainerEventArgs e) + protected override void OnContainerPrepared(ElementPreparedEventArgs e) { - base.OnContainersMaterialized(e); + base.OnContainerPrepared(e); - if (SelectedContent != null || SelectedIndex == -1) + if (SelectedContent is object || SelectedIndex == -1) { return; } - var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(SelectedIndex); - - if (container == null) + if (e.Element is IContentControl c) { - return; + UpdateSelectedContent(c); } - - UpdateSelectedContent(container); } - private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - var index = (int)e.NewValue; + base.OnPropertyChanged(change); - if (index == -1) + if (change.Property == SelectedIndexProperty) { - SelectedContentTemplate = null; + var newIndex = change.NewValue.GetValueOrDefault(-1); - SelectedContent = null; - - return; - } + if (newIndex == -1) + { + SelectedContentTemplate = null; + SelectedContent = null; + return; + } - var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(index); + var container = TryGetContainer(newIndex); - if (container == null) - { - return; + if (container is IContentControl c) + { + UpdateSelectedContent(c); + } } - - UpdateSelectedContent(container); } private void UpdateSelectedContent(IContentControl item) @@ -217,17 +212,13 @@ protected override IItemContainerGenerator CreateItemContainerGenerator() return new TabItemContainerGenerator(this); } - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) - { - ItemsPresenterPart = e.NameScope.Get("PART_ItemsPresenter"); - } - /// protected override void OnGotFocus(GotFocusEventArgs e) { base.OnGotFocus(e); - if (e.NavigationMethod == NavigationMethod.Directional) + if (e.NavigationMethod == NavigationMethod.Directional && + e.Source is object) { e.Handled = UpdateSelectionFromEventSource(e.Source); } @@ -238,7 +229,9 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && e.Pointer.Type == PointerType.Mouse) + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && + e.Pointer.Type == PointerType.Mouse && + e.Source is object) { e.Handled = UpdateSelectionFromEventSource(e.Source); } @@ -246,7 +239,9 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) protected override void OnPointerReleased(PointerReleasedEventArgs e) { - if (e.InitialPressMouseButton == MouseButton.Left && e.Pointer.Type != PointerType.Mouse) + if (e.InitialPressMouseButton == MouseButton.Left && + e.Pointer.Type != PointerType.Mouse && + e.Source is object) { var container = GetContainerFromEventSource(e.Source); if (container != null diff --git a/src/Avalonia.Controls/TreeElementIndexChangedEventArgs.cs b/src/Avalonia.Controls/TreeElementIndexChangedEventArgs.cs new file mode 100644 index 00000000000..fee09fc8554 --- /dev/null +++ b/src/Avalonia.Controls/TreeElementIndexChangedEventArgs.cs @@ -0,0 +1,38 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using Avalonia.Controls.Selection; + +namespace Avalonia.Controls +{ + /// + /// Provides notification that the index for an element in a tree has changed. + /// + public class TreeElementIndexChangedEventArgs : EventArgs + { + public TreeElementIndexChangedEventArgs(IControl element, IndexPath oldIndex, IndexPath newIndex) + { + Element = element; + OldIndex = oldIndex; + NewIndex = newIndex; + } + + /// + /// Get the element for which the index changed. + /// + public IControl Element { get; private set; } + + /// + /// Gets the index of the element after the change. + /// + public IndexPath NewIndex { get; private set; } + + /// + /// Gets the index of the element before the change. + /// + public IndexPath OldIndex { get; private set; } + } +} diff --git a/src/Avalonia.Controls/TreeElementPreparedEventArgs.cs b/src/Avalonia.Controls/TreeElementPreparedEventArgs.cs new file mode 100644 index 00000000000..095185af501 --- /dev/null +++ b/src/Avalonia.Controls/TreeElementPreparedEventArgs.cs @@ -0,0 +1,31 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using Avalonia.Controls.Selection; + +namespace Avalonia.Controls +{ + /// + /// Provides notification that a tree element has been prepared for use. + /// + public class TreeElementPreparedEventArgs + { + public TreeElementPreparedEventArgs(IControl element, IndexPath index) + { + Element = element; + Index = index; + } + + /// + /// Gets the prepared element. + /// + public IControl Element { get; private set; } + + /// + /// Gets the index of the item the element was prepared for. + /// + public IndexPath Index { get; private set; } + } +} diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index b4c30e0149f..921e0357ccb 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -1,22 +1,25 @@ - using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using System.ComponentModel; using System.Linq; using System.Reactive.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Selection; using Avalonia.Controls.Utils; using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.Threading; using Avalonia.VisualTree; +#nullable enable + namespace Avalonia.Controls { /// @@ -33,7 +36,7 @@ public class TreeView : ItemsControl, ICustomKeyboardNavigation /// /// Defines the property. /// - public static readonly DirectProperty SelectedItemProperty = + public static readonly DirectProperty SelectedItemProperty = SelectingItemsControl.SelectedItemProperty.AddOwner( o => o.SelectedItem, (o, v) => o.SelectedItem = v); @@ -52,9 +55,15 @@ public class TreeView : ItemsControl, ICustomKeyboardNavigation public static readonly StyledProperty SelectionModeProperty = ListBox.SelectionModeProperty.AddOwner(); + /// + /// Defines the property. + /// + public static RoutedEvent SelectionChangedEvent = + SelectingItemsControl.SelectionChangedEvent; + private static readonly IList Empty = Array.Empty(); - private object _selectedItem; - private IList _selectedItems; + private object? _selectedItem; + private IList? _selectedItems; private bool _syncingSelectedItems; /// @@ -62,7 +71,10 @@ public class TreeView : ItemsControl, ICustomKeyboardNavigation /// static TreeView() { - // HACK: Needed or SelectedItem property will not be found in Release build. + LayoutProperty.OverrideDefaultValue(new NonVirtualizingStackLayout + { + Orientation = Orientation.Vertical, + }); } /// @@ -105,7 +117,7 @@ public SelectionMode SelectionMode /// Note that setting this property only currently works if the item is expanded to be visible. /// To select non-expanded nodes use `Selection.SelectedIndex`. /// - public object SelectedItem + public object? SelectedItem { get => _selectedItem; set @@ -160,6 +172,36 @@ public IList SelectedItems } } + /// + /// Occurs each time a container in the tree is cleared and made available to be re-used. + /// + /// + /// This event is analogous to except that it + /// is raised for elements at all levels of the tree whereas `ContainerClearing` is only + /// raised for root s. + /// + public event EventHandler? TreeContainerClearing; + + /// + /// Occurs each time a container is prepared for use. + /// + /// + /// This event is analogous to except that it + /// is raised for elements at all levels of the tree whereas `ContainerPrepared` is only + /// raised for root s. + /// + public event EventHandler? TreeContainerPrepared; + + /// + /// Occurs for each realized container when the index for the item it represents has changed. + /// + /// + /// This event is analogous to except that it + /// is raised for elements at all levels of the tree whereas `ContainerPrepared` is only + /// raised for root s. + /// + public event EventHandler? TreeContainerIndexChanged; + /// /// Expands the specified all descendent s. /// @@ -168,11 +210,11 @@ public void ExpandSubTree(TreeViewItem item) { item.IsExpanded = true; - var panel = item.Presenter.Panel; + var presenter = item.Presenter; - if (panel != null) + if (presenter is object) { - foreach (var child in panel.Children) + foreach (var child in presenter.RealizedElements) { if (child is TreeViewItem treeViewItem) { @@ -230,8 +272,8 @@ private void SelectSingleItem(object item) /// The event args. private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { - IList added = null; - IList removed = null; + IList? added = null; + IList? removed = null; switch (e.Action) { @@ -334,6 +376,23 @@ private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChang } } + public TreeViewItem? TreeContainerFromIndex(IndexPath path) + { + if (path.GetSize() == 0) + { + return null; + } + + var control = (ItemsControl)this; + + for (var i = 0; i < path.GetSize() && control != null; ++i) + { + control = control.TryGetContainer(path.GetAt(i)) as ItemsControl; + } + + return control as TreeViewItem; + } + private void MarkItemSelected(object item, bool selected) { var container = ItemContainerGenerator.Index.ContainerFromItem(item); @@ -369,16 +428,17 @@ private void UnsubscribeFromSelectedItems() incc.CollectionChanged -= SelectedItemsCollectionChanged; } } - (bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element, + + (bool handled, IInputElement? next) ICustomKeyboardNavigation.GetNext(IInputElement element, NavigationDirection direction) { if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) { if (!this.IsVisualAncestorOf(element)) { - IControl result = _selectedItem != null ? + var result = _selectedItem != null ? ItemContainerGenerator.Index.ContainerFromItem(_selectedItem) : - ItemContainerGenerator.ContainerFromIndex(0); + TryGetContainer(0); return (true, result); } @@ -388,23 +448,85 @@ private void UnsubscribeFromSelectedItems() return (false, null); } + internal protected void RaiseTreeContainerPrepared(IndexPath parentIndex, ElementPreparedEventArgs e) + { + TreeContainerPrepared?.Invoke( + this, + new TreeElementPreparedEventArgs( + e.Element, + parentIndex.CloneWithChildIndex(e.Index))); + } + + internal protected void RaiseTreeContainerClearing(ElementClearingEventArgs e) + { + TreeContainerClearing?.Invoke(this, e); + } + + internal protected void RaiseTreeContainerIndexChanged(IndexPath parentIndex, ElementIndexChangedEventArgs e) + { + TreeContainerIndexChanged?.Invoke( + this, + new TreeElementIndexChangedEventArgs( + e.Element, + parentIndex.CloneWithChildIndex(e.OldIndex), + parentIndex.CloneWithChildIndex(e.NewIndex))); + } + /// protected override IItemContainerGenerator CreateItemContainerGenerator() { var result = new TreeItemContainerGenerator( this, TreeViewItem.HeaderProperty, - TreeViewItem.ItemTemplateProperty, + TreeViewItem.HeaderTemplateProperty, TreeViewItem.ItemsProperty, TreeViewItem.IsExpandedProperty); - result.Index.Materialized += ContainerMaterialized; + result.Index!.Materialized += ContainerMaterialized; return result; } + protected override void OnContainerPrepared(ElementPreparedEventArgs e) + { + base.OnContainerPrepared(e); + + if (e.Element is TreeViewItem item) + { + item.IndexPath = new IndexPath(e.Index); + ItemContainerGenerator.Index.Add(ItemsView[e.Index], e.Element); + } + + RaiseTreeContainerPrepared(default, e); + } + + protected override void OnContainerClearing(ElementClearingEventArgs e) + { + base.OnContainerClearing(e); + + RaiseTreeContainerClearing(e); + + if (e.Element is TreeViewItem item) + { + item.IndexPath = default; + ItemContainerGenerator.Index.Remove(e.Element); + } + } + + protected override void OnContainerIndexChanged(ElementIndexChangedEventArgs e) + { + base.OnContainerIndexChanged(e); + + if (e.Element is TreeViewItem item) + { + item.IndexPath = new IndexPath(e.NewIndex); + } + + RaiseTreeContainerIndexChanged(default, e); + } + /// protected override void OnGotFocus(GotFocusEventArgs e) { - if (e.NavigationMethod == NavigationMethod.Directional) + if (e.NavigationMethod == NavigationMethod.Directional && e.Source is object) { e.Handled = UpdateSelectionFromEventSource( e.Source, @@ -417,7 +539,7 @@ protected override void OnKeyDown(KeyEventArgs e) { var direction = e.Key.ToNavigationDirection(); - if (direction?.IsDirectional() == true && !e.Handled) + if (direction?.IsDirectional() == true && !e.Handled && e.Source is object) { if (SelectedItem != null) { @@ -432,9 +554,9 @@ protected override void OnKeyDown(KeyEventArgs e) e.Handled = true; } } - else + else if (ItemsView.Count > 0) { - SelectedItem = ElementAt(Items, 0); + SelectedItem = ItemsView[0]; } } @@ -443,7 +565,7 @@ protected override void OnKeyDown(KeyEventArgs e) var keymap = AvaloniaLocator.Current.GetService(); bool Match(List gestures) => gestures.Any(g => g.Matches(e)); - if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll)) + if (SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll)) { SelectAll(); e.Handled = true; @@ -462,24 +584,25 @@ private TreeViewItem GetContainerInDirection( { return null; } - var index = parentGenerator.IndexFromContainer(from); var parent = from.Parent as ItemsControl; - TreeViewItem result = null; + TreeViewItem? result = null; switch (direction) { case NavigationDirection.Up: if (index > 0) { - var previous = (TreeViewItem)parentGenerator.ContainerFromIndex(index - 1); - result = previous.IsExpanded && previous.ItemCount > 0 ? - (TreeViewItem)previous.ItemContainerGenerator.ContainerFromIndex(previous.ItemCount - 1) : - previous; + result = parent?.TryGetContainer(index - 1) as TreeViewItem; + + if (result?.IsExpanded == true && result.ItemCount > 0) + { + result = result.TryGetContainer(result.ItemCount - 1) as TreeViewItem; + } } else { - result = from.Parent as TreeViewItem; + result = parent as TreeViewItem; } break; @@ -487,11 +610,11 @@ private TreeViewItem GetContainerInDirection( case NavigationDirection.Down: if (from.IsExpanded && intoChildren && from.ItemCount > 0) { - result = (TreeViewItem)from.ItemContainerGenerator.ContainerFromIndex(0); + result = from.TryGetContainer(0) as TreeViewItem; } - else if (index < parent?.ItemCount - 1) + else if (parent is object && index < parent.ItemCount - 1) { - result = (TreeViewItem)parentGenerator.ContainerFromIndex(index + 1); + result = parent.TryGetContainer(index + 1) as TreeViewItem; } else if (parent is TreeViewItem parentItem) { @@ -547,7 +670,7 @@ protected void UpdateSelectionFromContainer( return; } - IControl selectedContainer = null; + IControl? selectedContainer = null; if (SelectedItem != null) { @@ -598,7 +721,7 @@ protected void UpdateSelectionFromContainer( } } - private static IItemContainerGenerator GetParentContainerGenerator(TreeViewItem item) + private static IItemContainerGenerator? GetParentContainerGenerator(TreeViewItem item) { if (item == null) { @@ -623,45 +746,47 @@ private static IItemContainerGenerator GetParentContainerGenerator(TreeViewItem /// Nodes to find. /// Node to find. /// Found first node. - private static TreeViewItem FindFirstNode(TreeView treeView, TreeViewItem nodeA, TreeViewItem nodeB) + private static TreeViewItem? FindFirstNode(TreeView treeView, TreeViewItem nodeA, TreeViewItem nodeB) { - return FindInContainers(treeView.ItemContainerGenerator, nodeA, nodeB); + return FindInContainers(treeView.Presenter, nodeA, nodeB); } - private static TreeViewItem FindInContainers(ITreeItemContainerGenerator containerGenerator, + private static TreeViewItem? FindInContainers( + IItemsPresenter? presenter, TreeViewItem nodeA, TreeViewItem nodeB) { - IEnumerable containers = containerGenerator.Containers; - - foreach (ItemContainerInfo container in containers) + if (presenter is object) { - TreeViewItem node = FindFirstNode(container.ContainerControl as TreeViewItem, nodeA, nodeB); - - if (node != null) + foreach (var container in presenter.RealizedElements) { - return node; + var node = FindFirstNode(container as TreeViewItem, nodeA, nodeB); + + if (node != null) + { + return node; + } } } return null; } - private static TreeViewItem FindFirstNode(TreeViewItem node, TreeViewItem nodeA, TreeViewItem nodeB) + private static TreeViewItem? FindFirstNode(TreeViewItem? node, TreeViewItem nodeA, TreeViewItem nodeB) { if (node == null) { return null; } - TreeViewItem match = node == nodeA ? nodeA : node == nodeB ? nodeB : null; + var match = node == nodeA ? nodeA : node == nodeB ? nodeB : null; if (match != null) { return match; } - return FindInContainers(node.ItemContainerGenerator, nodeA, nodeB); + return FindInContainers(node.Presenter, nodeA, nodeB); } /// @@ -763,7 +888,7 @@ protected bool UpdateSelectionFromEventSource( /// /// The control that raised the event. /// The container or null if the event did not originate in a container. - protected TreeViewItem GetContainerFromEventSource(IInteractive eventSource) + protected TreeViewItem? GetContainerFromEventSource(IInteractive eventSource) { var item = ((IVisual)eventSource).GetSelfAndVisualAncestors() .OfType() diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 4942d4d3136..bdf1683b723 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -2,8 +2,9 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; +using Avalonia.Controls.Selection; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.LogicalTree; namespace Avalonia.Controls @@ -35,10 +36,6 @@ public class TreeViewItem : HeaderedItemsControl, ISelectable AvaloniaProperty.RegisterDirect( nameof(Level), o => o.Level); - private static readonly ITemplate DefaultPanel = - new FuncTemplate(() => new StackPanel()); - - private TreeView _treeView; private IControl _header; private bool _isExpanded; private int _level; @@ -51,7 +48,10 @@ static TreeViewItem() SelectableMixin.Attach(IsSelectedProperty); PressedMixin.Attach(); FocusableProperty.OverrideDefaultValue(true); - ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); + LayoutProperty.OverrideDefaultValue(new NonVirtualizingStackLayout + { + Orientation = Orientation.Vertical, + }); ParentProperty.Changed.AddClassHandler((o, e) => o.OnParentChanged(e)); RequestBringIntoViewEvent.AddClassHandler((x, e) => x.OnRequestBringIntoView(e)); } @@ -89,37 +89,87 @@ public int Level public new ITreeItemContainerGenerator ItemContainerGenerator => (ITreeItemContainerGenerator)base.ItemContainerGenerator; - /// + /// + /// Gets the tree view that the item is a part of. + /// + internal TreeView TreeView { get; private set; } + + internal IndexPath IndexPath { get; set; } + protected override IItemContainerGenerator CreateItemContainerGenerator() { return new TreeItemContainerGenerator( this, TreeViewItem.HeaderProperty, - TreeViewItem.ItemTemplateProperty, + TreeViewItem.HeaderTemplateProperty, TreeViewItem.ItemsProperty, TreeViewItem.IsExpandedProperty); } + protected override void OnContainerPrepared(ElementPreparedEventArgs e) + { + base.OnContainerPrepared(e); + + if (e.Element is TreeViewItem item) + { + item.IndexPath = IndexPath.CloneWithChildIndex(e.Index); + } + + ItemContainerGenerator.Index.Add(ItemsView[e.Index], e.Element); + TreeView?.RaiseTreeContainerPrepared(IndexPath, e); + } + + protected override void OnContainerClearing(ElementClearingEventArgs e) + { + base.OnContainerClearing(e); + + ItemContainerGenerator.Index.Remove(e.Element); + TreeView?.RaiseTreeContainerClearing(e); + + if (e.Element is TreeViewItem item) + { + item.IndexPath = default; + } + } + + protected override void OnContainerIndexChanged(ElementIndexChangedEventArgs e) + { + base.OnContainerIndexChanged(e); + + if (e.Element is TreeViewItem item) + { + item.IndexPath = IndexPath.CloneWithChildIndex(e.NewIndex); + } + + TreeView?.RaiseTreeContainerIndexChanged(IndexPath, e); + } + /// protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnAttachedToLogicalTree(e); - - _treeView = this.GetLogicalAncestors().OfType().FirstOrDefault(); - - Level = CalculateDistanceFromLogicalParent(this) - 1; + + (Level, TreeView) = FindOwner(); ItemContainerGenerator.UpdateIndex(); - if (ItemTemplate == null && _treeView?.ItemTemplate != null) + if (ItemTemplate == null && TreeView?.ItemTemplate != null) { - ItemTemplate = _treeView.ItemTemplate; + ItemTemplate = TreeView.ItemTemplate; } } protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnDetachedFromLogicalTree(e); + ItemContainerGenerator.UpdateIndex(); + + var (_, owner) = FindOwner(); + + if (TreeView is object && owner is null) + { + TreeView = null; + } } protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e) @@ -168,27 +218,36 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) _header = e.NameScope.Find("PART_Header"); } - private static int CalculateDistanceFromLogicalParent(ILogical logical, int @default = -1) where T : class + private (int distance, TreeView owner) FindOwner() { - var result = 0; + var c = Parent; + var i = 0; - while (logical != null && !(logical is T)) + while (c != null) { - ++result; - logical = logical.LogicalParent; + if (c is TreeView treeView) + { + return (i, treeView); + } + + c = c.Parent; + ++i; } - return logical != null ? result : @default; + return (-1, null); } private void OnParentChanged(AvaloniaPropertyChangedEventArgs e) { if (!((ILogical)this).IsAttachedToLogicalTree && e.NewValue is null) { + var oldIndex = ItemContainerGenerator.Index; + // If we're not attached to the logical tree, then OnDetachedFromLogicalTree isn't going to be // called when the item is removed. This results in the item not being removed from the index, // causing #3551. In this case, update the index when Parent is changed to null. ItemContainerGenerator.UpdateIndex(); + TreeView = null; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml index 264a0de3594..0dbee9691e3 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml @@ -38,8 +38,7 @@ BorderBrush="{DynamicResource ThemeControlMidBrush}" BorderThickness="0,0,0,1" FontFamily="/Assets/Fonts/SourceSansPro-Regular.ttf" - Items="{Binding History}" - VirtualizationMode="None"> + Items="{Binding History}"> diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs index 1b61986ce60..6f8a642c272 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs @@ -1,6 +1,5 @@ using System.Linq; using Avalonia.Controls; -using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Diagnostics.ViewModels; using Avalonia.Input; @@ -17,8 +16,8 @@ internal class TreePageView : UserControl public TreePageView() { - InitializeComponent(); - _tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized; + this.InitializeComponent(); + _tree.TreeContainerPrepared += TreeContainerPrepared; _adorner = new Panel { @@ -91,9 +90,9 @@ private void InitializeComponent() _tree = this.FindControl("tree"); } - private void TreeViewItemMaterialized(object sender, ItemContainerEventArgs e) + private void TreeContainerPrepared(object sender, TreeElementPreparedEventArgs e) { - var item = (TreeViewItem)e.Containers[0].ContainerControl; + var item = (TreeViewItem)e.Element; item.TemplateApplied += TreeViewItemTemplateApplied; } diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.xaml b/src/Avalonia.Dialogs/ManagedFileChooser.xaml index 227cc1afc05..e5a3c273c60 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooser.xaml +++ b/src/Avalonia.Dialogs/ManagedFileChooser.xaml @@ -109,7 +109,6 @@ + /// Retrieves the element that should receive focus based on the specified navigation direction. + /// + /// The direction to move in. + /// The next element, or null if no element was found. + public IInputElement FindNextElement(NavigationDirection direction) + { + var container = Current?.VisualParent; + + if (container is null) + { + return null; + } + + if (container is ICustomKeyboardNavigation custom) + { + var (handled, next) = custom.GetNext(Current, direction); + + if (handled) + { + return next; + } + } + + static IInputElement GetFirst(IVisual container) + { + for (var i = 0; i < container.VisualChildren.Count; ++i) + { + if (container.VisualChildren[i] is IInputElement ie && ie.CanFocus()) + { + return ie; + } + } + + return null; + } + + static IInputElement GetLast(IVisual container) + { + for (var i = container.VisualChildren.Count - 1; i >= 0; --i) + { + if (container.VisualChildren[i] is IInputElement ie && ie.CanFocus()) + { + return ie; + } + } + + return null; + } + + return direction switch + { + NavigationDirection.Next => TabNavigation.GetNextInTabOrder(Current, direction), + NavigationDirection.Previous => TabNavigation.GetNextInTabOrder(Current, direction), + NavigationDirection.First => GetFirst(container), + NavigationDirection.Last => GetLast(container), + _ => FindInDirection(container, Current, direction), + }; + } + /// /// Focuses a control. /// @@ -178,6 +239,44 @@ private static IEnumerable GetFocusScopeAncestors(IInputElement con } } + + private IInputElement FindInDirection( + IVisual container, + IInputElement from, + NavigationDirection direction) + { + static double Distance(NavigationDirection direction, IInputElement from, IInputElement to) + { + return direction switch + { + NavigationDirection.Left => from.Bounds.Right - to.Bounds.Right, + NavigationDirection.Right => to.Bounds.X - from.Bounds.X, + NavigationDirection.Up => from.Bounds.Bottom - to.Bounds.Bottom, + NavigationDirection.Down => to.Bounds.Y - from.Bounds.Y, + _ => throw new NotSupportedException("direction must be Up, Down, Left or Right"), + }; + } + + IInputElement result = null; + var resultDistance = double.MaxValue; + + foreach (var visual in container.VisualChildren) + { + if (visual is IInputElement child && child != from && child.CanFocus()) + { + var distance = Distance(direction, from, child); + + if (distance > 0 && distance < resultDistance) + { + result = child; + resultDistance = distance; + } + } + } + + return result; + } + /// /// Global handler for pointer pressed events. /// diff --git a/src/Avalonia.Input/IFocusManager.cs b/src/Avalonia.Input/IFocusManager.cs index 9122cc428db..cdde0d35aa1 100644 --- a/src/Avalonia.Input/IFocusManager.cs +++ b/src/Avalonia.Input/IFocusManager.cs @@ -15,6 +15,13 @@ public interface IFocusManager /// IFocusScope Scope { get; } + /// + /// Retrieves the element that should receive focus based on the specified navigation direction. + /// + /// The direction to move in. + /// The next element, or null if no element was found. + IInputElement FindNextElement(NavigationDirection direction); + /// /// Focuses a control. /// diff --git a/src/Avalonia.Input/NavigationDirection.cs b/src/Avalonia.Input/NavigationDirection.cs index 9b9af0b0a62..0c4a4921817 100644 --- a/src/Avalonia.Input/NavigationDirection.cs +++ b/src/Avalonia.Input/NavigationDirection.cs @@ -44,16 +44,6 @@ public enum NavigationDirection /// Move the focus down. /// Down, - - /// - /// Move the focus up a page. - /// - PageUp, - - /// - /// Move the focus down a page. - /// - PageDown, } public static class NavigationDirectionExtensions @@ -84,7 +74,7 @@ public static bool IsTab(this NavigationDirection direction) public static bool IsDirectional(this NavigationDirection direction) { return direction > NavigationDirection.Previous || - direction <= NavigationDirection.PageDown; + direction <= NavigationDirection.Down; } /// @@ -116,10 +106,6 @@ public static bool IsDirectional(this NavigationDirection direction) return NavigationDirection.First; case Key.End: return NavigationDirection.Last; - case Key.PageUp: - return NavigationDirection.PageUp; - case Key.PageDown: - return NavigationDirection.PageDown; default: return null; } diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index fc988a8d6c3..30a52aa12ce 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -163,26 +163,7 @@ public virtual void ExecuteLayoutPass() /// public virtual void ExecuteInitialLayoutPass() { - if (_disposed) - { - return; - } - - try - { - _running = true; - Measure(_owner); - Arrange(_owner); - } - finally - { - _running = false; - } - - // Running the initial layout pass may have caused some control to be invalidated - // so run a full layout pass now (this usually due to scrollbars; its not known - // whether they will need to be shown until the layout pass has run and if the - // first guess was incorrect the layout will need to be updated). + InvalidateMeasure(_owner); ExecuteLayoutPass(); } diff --git a/src/Avalonia.Themes.Default/Carousel.xaml b/src/Avalonia.Themes.Default/Carousel.xaml index 955a49a974d..ff512ae6f5b 100644 --- a/src/Avalonia.Themes.Default/Carousel.xaml +++ b/src/Avalonia.Themes.Default/Carousel.xaml @@ -4,12 +4,12 @@ - + PageTransition="{TemplateBinding PageTransition}"/>--> diff --git a/src/Avalonia.Themes.Default/ComboBox.xaml b/src/Avalonia.Themes.Default/ComboBox.xaml index cced76e850d..01dc95a99d6 100644 --- a/src/Avalonia.Themes.Default/ComboBox.xaml +++ b/src/Avalonia.Themes.Default/ComboBox.xaml @@ -71,10 +71,9 @@ BorderThickness="1"> + Layout="{TemplateBinding Layout}"/> diff --git a/src/Avalonia.Themes.Default/ContextMenu.xaml b/src/Avalonia.Themes.Default/ContextMenu.xaml index 9b84253c8ac..e60a4a9b5c0 100644 --- a/src/Avalonia.Themes.Default/ContextMenu.xaml +++ b/src/Avalonia.Themes.Default/ContextMenu.xaml @@ -12,8 +12,8 @@ Padding="{TemplateBinding Padding}"> diff --git a/src/Avalonia.Themes.Default/ItemsControl.xaml b/src/Avalonia.Themes.Default/ItemsControl.xaml index 8bb0fc297cf..b2241f5045e 100644 --- a/src/Avalonia.Themes.Default/ItemsControl.xaml +++ b/src/Avalonia.Themes.Default/ItemsControl.xaml @@ -6,8 +6,8 @@ BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}"> diff --git a/src/Avalonia.Themes.Default/ListBox.xaml b/src/Avalonia.Themes.Default/ListBox.xaml index e91d8a6772a..70933e7dcdc 100644 --- a/src/Avalonia.Themes.Default/ListBox.xaml +++ b/src/Avalonia.Themes.Default/ListBox.xaml @@ -15,11 +15,10 @@ HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}" VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"> + Layout="{TemplateBinding Layout}" + Margin="{TemplateBinding Padding}"/> diff --git a/src/Avalonia.Themes.Default/Menu.xaml b/src/Avalonia.Themes.Default/Menu.xaml index a1a5afb4a98..f45d22c857e 100644 --- a/src/Avalonia.Themes.Default/Menu.xaml +++ b/src/Avalonia.Themes.Default/Menu.xaml @@ -6,11 +6,11 @@ BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}"> - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index 7d7d06dd553..863e987c686 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -66,9 +66,9 @@ BorderThickness="{TemplateBinding BorderThickness}"> @@ -116,9 +116,9 @@ BorderThickness="{TemplateBinding BorderThickness}"> diff --git a/src/Avalonia.Themes.Default/TabControl.xaml b/src/Avalonia.Themes.Default/TabControl.xaml index ed2e67df288..b12a360cd76 100644 --- a/src/Avalonia.Themes.Default/TabControl.xaml +++ b/src/Avalonia.Themes.Default/TabControl.xaml @@ -12,8 +12,8 @@ - - diff --git a/src/Avalonia.Themes.Fluent/TreeView.xaml b/src/Avalonia.Themes.Fluent/TreeView.xaml index 292317eb346..d8a8e7837c0 100644 --- a/src/Avalonia.Themes.Fluent/TreeView.xaml +++ b/src/Avalonia.Themes.Fluent/TreeView.xaml @@ -14,9 +14,9 @@ HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}" VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"> + Items="{TemplateBinding ItemsView}" + Layout="{TemplateBinding Layout}" + Margin="{TemplateBinding Padding}"/> diff --git a/src/Avalonia.Themes.Fluent/TreeViewItem.xaml b/src/Avalonia.Themes.Fluent/TreeViewItem.xaml index a938394b564..3f2a9d2799a 100644 --- a/src/Avalonia.Themes.Fluent/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Fluent/TreeViewItem.xaml @@ -93,8 +93,8 @@ + Items="{TemplateBinding ItemsView}" + Layout="{TemplateBinding Layout}" /> diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index 051f6c3fd3b..e3e1380fc48 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -9,307 +9,307 @@ namespace Avalonia.Controls.UnitTests { - public class CarouselTests - { - [Fact] - public void First_Item_Should_Be_Selected_By_Default() - { - var target = new Carousel - { - Template = new FuncControlTemplate(CreateTemplate), - Items = new[] - { - "Foo", - "Bar" - } - }; - - target.ApplyTemplate(); - - Assert.Equal(0, target.SelectedIndex); - Assert.Equal("Foo", target.SelectedItem); - } - - [Fact] - public void LogicalChild_Should_Be_Selected_Item() - { - var target = new Carousel - { - Template = new FuncControlTemplate(CreateTemplate), - Items = new[] - { - "Foo", - "Bar" - } - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - Assert.Single(target.GetLogicalChildren()); - - var child = GetContainerTextBlock(target.GetLogicalChildren().Single()); - - Assert.Equal("Foo", child.Text); - } - - [Fact] - public void Should_Remove_NonCurrent_Page_When_IsVirtualized_True() - { - var target = new Carousel - { - Template = new FuncControlTemplate(CreateTemplate), - Items = new[] { "foo", "bar" }, - IsVirtualized = true, - SelectedIndex = 0, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - Assert.Single(target.ItemContainerGenerator.Containers); - target.SelectedIndex = 1; - Assert.Single(target.ItemContainerGenerator.Containers); - } - - [Fact] - public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes() - { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; - - var target = new Carousel - { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - Assert.Equal(3, target.GetLogicalChildren().Count()); - - var child = GetContainerTextBlock(target.GetLogicalChildren().First()); - - Assert.Equal("Foo", child.Text); - - var newItems = items.ToList(); - newItems.RemoveAt(0); - - target.Items = newItems; - - child = GetContainerTextBlock(target.GetLogicalChildren().First()); - - Assert.Equal("Bar", child.Text); - } - - [Fact] - public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes_And_Virtualized() - { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; - - var target = new Carousel - { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = true, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - Assert.Single(target.GetLogicalChildren()); - - var child = GetContainerTextBlock(target.GetLogicalChildren().Single()); - - Assert.Equal("Foo", child.Text); - - var newItems = items.ToList(); - newItems.RemoveAt(0); - - target.Items = newItems; - - child = GetContainerTextBlock(target.GetLogicalChildren().Single()); - - Assert.Equal("Bar", child.Text); - } - - [Fact] - public void Selected_Item_Changes_To_First_Item_When_Item_Added() - { - var items = new ObservableCollection(); - var target = new Carousel - { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + ////public class CarouselTests + ////{ + //// [Fact] + //// public void First_Item_Should_Be_Selected_By_Default() + //// { + //// var target = new Carousel + //// { + //// Template = new FuncControlTemplate(CreateTemplate), + //// Items = new[] + //// { + //// "Foo", + //// "Bar" + //// } + //// }; + + //// target.ApplyTemplate(); + + //// Assert.Equal(0, target.SelectedIndex); + //// Assert.Equal("Foo", target.SelectedItem); + //// } + + //// [Fact] + //// public void LogicalChild_Should_Be_Selected_Item() + //// { + //// var target = new Carousel + //// { + //// Template = new FuncControlTemplate(CreateTemplate), + //// Items = new[] + //// { + //// "Foo", + //// "Bar" + //// } + //// }; + + //// target.ApplyTemplate(); + //// target.Presenter.ApplyTemplate(); + + //// Assert.Single(target.GetLogicalChildren()); + + //// var child = GetContainerTextBlock(target.GetLogicalChildren().Single()); + + //// Assert.Equal("Foo", child.Text); + //// } + + //// [Fact] + //// public void Should_Remove_NonCurrent_Page_When_IsVirtualized_True() + //// { + //// var target = new Carousel + //// { + //// Template = new FuncControlTemplate(CreateTemplate), + //// Items = new[] { "foo", "bar" }, + //// IsVirtualized = true, + //// SelectedIndex = 0, + //// }; + + //// target.ApplyTemplate(); + //// target.Presenter.ApplyTemplate(); + + //// Assert.Single(target.ItemContainerGenerator.Containers); + //// target.SelectedIndex = 1; + //// Assert.Single(target.ItemContainerGenerator.Containers); + //// } + + //// [Fact] + //// public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes() + //// { + //// var items = new ObservableCollection + //// { + //// "Foo", + //// "Bar", + //// "FooBar" + //// }; + + //// var target = new Carousel + //// { + //// Template = new FuncControlTemplate(CreateTemplate), + //// Items = items, + //// IsVirtualized = false + //// }; + + //// target.ApplyTemplate(); + //// target.Presenter.ApplyTemplate(); + + //// Assert.Equal(3, target.GetLogicalChildren().Count()); + + //// var child = GetContainerTextBlock(target.GetLogicalChildren().First()); + + //// Assert.Equal("Foo", child.Text); + + //// var newItems = items.ToList(); + //// newItems.RemoveAt(0); + + //// target.Items = newItems; + + //// child = GetContainerTextBlock(target.GetLogicalChildren().First()); + + //// Assert.Equal("Bar", child.Text); + //// } + + //// [Fact] + //// public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes_And_Virtualized() + //// { + //// var items = new ObservableCollection + //// { + //// "Foo", + //// "Bar", + //// "FooBar" + //// }; + + //// var target = new Carousel + //// { + //// Template = new FuncControlTemplate(CreateTemplate), + //// Items = items, + //// IsVirtualized = true, + //// }; + + //// target.ApplyTemplate(); + //// target.Presenter.ApplyTemplate(); + + //// Assert.Single(target.GetLogicalChildren()); + + //// var child = GetContainerTextBlock(target.GetLogicalChildren().Single()); + + //// Assert.Equal("Foo", child.Text); + + //// var newItems = items.ToList(); + //// newItems.RemoveAt(0); + + //// target.Items = newItems; + + //// child = GetContainerTextBlock(target.GetLogicalChildren().Single()); + + //// Assert.Equal("Bar", child.Text); + //// } + + //// [Fact] + //// public void Selected_Item_Changes_To_First_Item_When_Item_Added() + //// { + //// var items = new ObservableCollection(); + //// var target = new Carousel + //// { + //// Template = new FuncControlTemplate(CreateTemplate), + //// Items = items, + //// IsVirtualized = false + //// }; + + //// target.ApplyTemplate(); + //// target.Presenter.ApplyTemplate(); - Assert.Equal(-1, target.SelectedIndex); - Assert.Empty(target.GetLogicalChildren()); + //// Assert.Equal(-1, target.SelectedIndex); + //// Assert.Empty(target.GetLogicalChildren()); - items.Add("Foo"); + //// items.Add("Foo"); - Assert.Equal(0, target.SelectedIndex); - Assert.Single(target.GetLogicalChildren()); - } + //// Assert.Equal(0, target.SelectedIndex); + //// Assert.Single(target.GetLogicalChildren()); + //// } - [Fact] - public void Selected_Index_Changes_To_None_When_Items_Assigned_Null() - { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; + //// [Fact] + //// public void Selected_Index_Changes_To_None_When_Items_Assigned_Null() + //// { + //// var items = new ObservableCollection + //// { + //// "Foo", + //// "Bar", + //// "FooBar" + //// }; - var target = new Carousel - { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false - }; + //// var target = new Carousel + //// { + //// Template = new FuncControlTemplate(CreateTemplate), + //// Items = items, + //// IsVirtualized = false + //// }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + //// target.ApplyTemplate(); + //// target.Presenter.ApplyTemplate(); - Assert.Equal(3, target.GetLogicalChildren().Count()); + //// Assert.Equal(3, target.GetLogicalChildren().Count()); - var child = GetContainerTextBlock(target.GetLogicalChildren().First()); + //// var child = GetContainerTextBlock(target.GetLogicalChildren().First()); - Assert.Equal("Foo", child.Text); + //// Assert.Equal("Foo", child.Text); - target.Items = null; + //// target.Items = null; - var numChildren = target.GetLogicalChildren().Count(); + //// var numChildren = target.GetLogicalChildren().Count(); - Assert.Equal(0, numChildren); - Assert.Equal(-1, target.SelectedIndex); - } - - [Fact] - public void Selected_Index_Is_Maintained_Carousel_Created_With_Non_Zero_SelectedIndex() - { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; - - var target = new Carousel - { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false, - SelectedIndex = 2 - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - Assert.Equal("FooBar", target.SelectedItem); - - var child = GetContainerTextBlock(target.GetVisualDescendants().LastOrDefault()); - - Assert.IsType(child); - Assert.Equal("FooBar", ((TextBlock)child).Text); - } - - [Fact] - public void Selected_Item_Changes_To_Next_First_Item_When_Item_Removed_From_Beggining_Of_List() - { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; - - var target = new Carousel - { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - Assert.Equal(3, target.GetLogicalChildren().Count()); - - var child = GetContainerTextBlock(target.GetLogicalChildren().First()); - - Assert.Equal("Foo", child.Text); - - items.RemoveAt(0); - - child = GetContainerTextBlock(target.GetLogicalChildren().First()); - - Assert.IsType(child); - Assert.Equal("Bar", ((TextBlock)child).Text); - } - - [Fact] - public void Selected_Item_Changes_To_First_Item_If_SelectedItem_Is_Removed_From_Middle() - { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; - - var target = new Carousel - { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - target.SelectedIndex = 1; - - items.RemoveAt(1); - - Assert.Equal(0, target.SelectedIndex); - Assert.Equal("Foo", target.SelectedItem); - } - - private Control CreateTemplate(Carousel control, INameScope scope) - { - return new CarouselPresenter - { - Name = "PART_ItemsPresenter", - [~CarouselPresenter.IsVirtualizedProperty] = control[~Carousel.IsVirtualizedProperty], - [~CarouselPresenter.ItemsProperty] = control[~Carousel.ItemsProperty], - [~CarouselPresenter.ItemsPanelProperty] = control[~Carousel.ItemsPanelProperty], - [~CarouselPresenter.SelectedIndexProperty] = control[~Carousel.SelectedIndexProperty], - [~CarouselPresenter.PageTransitionProperty] = control[~Carousel.PageTransitionProperty], - }.RegisterInNameScope(scope); - } - - private static TextBlock GetContainerTextBlock(object control) - { - var contentPresenter = Assert.IsType(control); - contentPresenter.UpdateChild(); - return Assert.IsType(contentPresenter.Child); - } - } + //// Assert.Equal(0, numChildren); + //// Assert.Equal(-1, target.SelectedIndex); + //// } + + //// [Fact] + //// public void Selected_Index_Is_Maintained_Carousel_Created_With_Non_Zero_SelectedIndex() + //// { + //// var items = new ObservableCollection + //// { + //// "Foo", + //// "Bar", + //// "FooBar" + //// }; + + //// var target = new Carousel + //// { + //// Template = new FuncControlTemplate(CreateTemplate), + //// Items = items, + //// IsVirtualized = false, + //// SelectedIndex = 2 + //// }; + + //// target.ApplyTemplate(); + //// target.Presenter.ApplyTemplate(); + + //// Assert.Equal("FooBar", target.SelectedItem); + + //// var child = GetContainerTextBlock(target.GetVisualDescendants().LastOrDefault()); + + //// Assert.IsType(child); + //// Assert.Equal("FooBar", ((TextBlock)child).Text); + //// } + + //// [Fact] + //// public void Selected_Item_Changes_To_Next_First_Item_When_Item_Removed_From_Beggining_Of_List() + //// { + //// var items = new ObservableCollection + //// { + //// "Foo", + //// "Bar", + //// "FooBar" + //// }; + + //// var target = new Carousel + //// { + //// Template = new FuncControlTemplate(CreateTemplate), + //// Items = items, + //// IsVirtualized = false + //// }; + + //// target.ApplyTemplate(); + //// target.Presenter.ApplyTemplate(); + + //// Assert.Equal(3, target.GetLogicalChildren().Count()); + + //// var child = GetContainerTextBlock(target.GetLogicalChildren().First()); + + //// Assert.Equal("Foo", child.Text); + + //// items.RemoveAt(0); + + //// child = GetContainerTextBlock(target.GetLogicalChildren().First()); + + //// Assert.IsType(child); + //// Assert.Equal("Bar", ((TextBlock)child).Text); + //// } + + //// [Fact] + //// public void Selected_Item_Changes_To_First_Item_If_SelectedItem_Is_Removed_From_Middle() + //// { + //// var items = new ObservableCollection + //// { + //// "Foo", + //// "Bar", + //// "FooBar" + //// }; + + //// var target = new Carousel + //// { + //// Template = new FuncControlTemplate(CreateTemplate), + //// Items = items, + //// IsVirtualized = false + //// }; + + //// target.ApplyTemplate(); + //// target.Presenter.ApplyTemplate(); + + //// target.SelectedIndex = 1; + + //// items.RemoveAt(1); + + //// Assert.Equal(0, target.SelectedIndex); + //// Assert.Equal("Foo", target.SelectedItem); + //// } + + //// private Control CreateTemplate(Carousel control, INameScope scope) + //// { + //// return new CarouselPresenter + //// { + //// Name = "PART_ItemsPresenter", + //// [~CarouselPresenter.IsVirtualizedProperty] = control[~Carousel.IsVirtualizedProperty], + //// [~CarouselPresenter.ItemsProperty] = control[~Carousel.ItemsProperty], + //// [~CarouselPresenter.ItemsPanelProperty] = control[~Carousel.ItemsPanelProperty], + //// [~CarouselPresenter.SelectedIndexProperty] = control[~Carousel.SelectedIndexProperty], + //// [~CarouselPresenter.PageTransitionProperty] = control[~Carousel.PageTransitionProperty], + //// }.RegisterInNameScope(scope); + //// } + + //// private static TextBlock GetContainerTextBlock(object control) + //// { + //// var contentPresenter = Assert.IsType(control); + //// contentPresenter.UpdateChild(); + //// return Assert.IsType(contentPresenter.Child); + //// } + ////} } diff --git a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs deleted file mode 100644 index ff3ad30a884..00000000000 --- a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Reactive.Linq; -using Avalonia.Controls.Generators; -using Avalonia.Controls.Presenters; -using Avalonia.Data; -using Xunit; - -namespace Avalonia.Controls.UnitTests.Generators -{ - public class ItemContainerGeneratorTests - { - [Fact] - public void Materialize_Should_Create_Containers() - { - var items = new[] { "foo", "bar", "baz" }; - var owner = new Decorator(); - var target = new ItemContainerGenerator(owner); - var containers = Materialize(target, 0, items); - var result = containers - .Select(x => x.ContainerControl) - .OfType() - .Select(x => x.Content) - .ToList(); - - Assert.Equal(items, result); - } - - [Fact] - public void ContainerFromIndex_Should_Return_Materialized_Containers() - { - var items = new[] { "foo", "bar", "baz" }; - var owner = new Decorator(); - var target = new ItemContainerGenerator(owner); - var containers = Materialize(target, 0, items); - - Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0)); - Assert.Equal(containers[1].ContainerControl, target.ContainerFromIndex(1)); - Assert.Equal(containers[2].ContainerControl, target.ContainerFromIndex(2)); - } - - [Fact] - public void IndexFromContainer_Should_Return_Index() - { - var items = new[] { "foo", "bar", "baz" }; - var owner = new Decorator(); - var target = new ItemContainerGenerator(owner); - var containers = Materialize(target, 0, items); - - Assert.Equal(0, target.IndexFromContainer(containers[0].ContainerControl)); - Assert.Equal(1, target.IndexFromContainer(containers[1].ContainerControl)); - Assert.Equal(2, target.IndexFromContainer(containers[2].ContainerControl)); - } - - [Fact] - public void Dematerialize_Should_Remove_Container() - { - var items = new[] { "foo", "bar", "baz" }; - var owner = new Decorator(); - var target = new ItemContainerGenerator(owner); - var containers = Materialize(target, 0, items); - - target.Dematerialize(1, 1); - - Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0)); - Assert.Null(target.ContainerFromIndex(1)); - Assert.Equal(containers[2].ContainerControl, target.ContainerFromIndex(2)); - } - - [Fact] - public void Dematerialize_Should_Return_Removed_Containers() - { - var items = new[] { "foo", "bar", "baz" }; - var owner = new Decorator(); - var target = new ItemContainerGenerator(owner); - var containers = Materialize(target, 0, items); - var expected = target.Containers.Take(2).ToList(); - var result = target.Dematerialize(0, 2); - - Assert.Equal(expected, result); - } - - [Fact] - public void InsertSpace_Should_Alter_Successive_Container_Indexes() - { - var items = new[] { "foo", "bar", "baz" }; - var owner = new Decorator(); - var target = new ItemContainerGenerator(owner); - var containers = Materialize(target, 0, items); - - target.InsertSpace(1, 3); - - Assert.Equal(3, target.Containers.Count()); - Assert.Equal(new[] { 0, 4, 5 }, target.Containers.Select(x => x.Index)); - } - - [Fact] - public void RemoveRange_Should_Alter_Successive_Container_Indexes() - { - var items = new[] { "foo", "bar", "baz" }; - var owner = new Decorator(); - var target = new ItemContainerGenerator(owner); - var containers = Materialize(target, 0, items); - - var removed = target.RemoveRange(1, 1).Single(); - - Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0)); - Assert.Equal(containers[2].ContainerControl, target.ContainerFromIndex(1)); - Assert.Equal(containers[1], removed); - Assert.Equal(new[] { 0, 1 }, target.Containers.Select(x => x.Index)); - } - - [Fact] - public void Style_Binding_Should_Be_Able_To_Override_Content() - { - var owner = new Decorator(); - var target = new ItemContainerGenerator(owner); - var container = (ContentPresenter)target.Materialize(0, "foo").ContainerControl; - - Assert.Equal("foo", container.Content); - - container.Bind( - ContentPresenter.ContentProperty, - Observable.Never().StartWith("bar"), - BindingPriority.Style); - - Assert.Equal("bar", container.Content); - } - - [Fact] - public void Style_Binding_Should_Be_Able_To_Override_Content_Typed() - { - var owner = new Decorator(); - var target = new ItemContainerGenerator(owner, ListBoxItem.ContentProperty, null); - var container = (ListBoxItem)target.Materialize(0, "foo").ContainerControl; - - Assert.Equal("foo", container.Content); - - container.Bind( - ContentPresenter.ContentProperty, - Observable.Never().StartWith("bar"), - BindingPriority.Style); - - Assert.Equal("bar", container.Content); - } - - private IList Materialize( - IItemContainerGenerator generator, - int index, - string[] items) - { - var result = new List(); - - foreach (var item in items) - { - var container = generator.Materialize(index++, item); - result.Add(container); - } - - return result; - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs deleted file mode 100644 index aab7c52f34b..00000000000 --- a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls.Generators; -using Xunit; - -namespace Avalonia.Controls.UnitTests.Generators -{ - public class ItemContainerGeneratorTypedTests - { - [Fact] - public void Materialize_Should_Create_Containers() - { - var items = new[] { "foo", "bar", "baz" }; - var owner = new Decorator(); - var target = new ItemContainerGenerator(owner, ListBoxItem.ContentProperty, null); - var containers = Materialize(target, 0, items); - var result = containers - .Select(x => x.ContainerControl) - .OfType() - .Select(x => x.Content) - .ToList(); - - Assert.Equal(items, result); - } - - private IList Materialize( - IItemContainerGenerator generator, - int index, - string[] items) - { - var result = new List(); - - foreach (var item in items) - { - var container = generator.Materialize(index++, item); - result.Add(container); - } - - return result; - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 684486cbae6..b6cf7ca4b92 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -1,15 +1,13 @@ +using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Input; using Avalonia.LogicalTree; -using Avalonia.VisualTree; -using Xunit; -using System.Collections.ObjectModel; using Avalonia.UnitTests; -using Avalonia.Input; -using System.Collections.Generic; +using Xunit; namespace Avalonia.Controls.UnitTests { @@ -25,28 +23,14 @@ public void Should_Use_ItemTemplate_To_Create_Control() }; target.Items = new[] { "Foo" }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Layout(target); - var container = (ContentPresenter)target.Presenter.Panel.Children[0]; + var container = (ContentPresenter)target.Presenter.RealizedElements.First(); container.UpdateChild(); Assert.IsType(container.Child); } - [Fact] - public void Panel_Should_Have_TemplatedParent_Set_To_ItemsControl() - { - var target = new ItemsControl(); - - target.Template = GetTemplate(); - target.Items = new[] { "Foo" }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - Assert.Equal(target, target.Presenter.Panel.TemplatedParent); - } - [Fact] public void Container_Should_Have_TemplatedParent_Set_To_Null() { @@ -54,10 +38,9 @@ public void Container_Should_Have_TemplatedParent_Set_To_Null() target.Template = GetTemplate(); target.Items = new[] { "Foo" }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Layout(target); - var container = (ContentPresenter)target.Presenter.Panel.Children[0]; + var container = (ContentPresenter)target.Presenter.RealizedElements.First(); Assert.Null(container.TemplatedParent); } @@ -78,11 +61,9 @@ public void Container_Should_Have_LogicalParent_Set_To_ItemsControl() target.Items = new[] { "Foo" }; - root.ApplyTemplate(); - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Layout(target); - var container = (ContentPresenter)target.Presenter.Panel.Children[0]; + var container = (ContentPresenter)target.Presenter.RealizedElements.First(); Assert.Equal(target, container.Parent); } @@ -115,9 +96,7 @@ public void Added_Container_Should_Have_LogicalParent_Set_To_ItemsControl() }; var root = new TestRoot(true, target); - - root.Measure(new Size(100, 100)); - root.Arrange(new Rect(0, 0, 100, 100)); + Layout(root); items.Add(item); @@ -194,8 +173,7 @@ public void Adding_String_Item_Should_Make_ContentPresenter_Appear_In_LogicalChi target.Template = GetTemplate(); target.Items = new[] { "Foo" }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Layout(target); var logical = (ILogical)target; Assert.Equal(1, logical.LogicalChildren.Count); @@ -210,8 +188,7 @@ public void Setting_Items_To_Null_Should_Remove_LogicalChildren() target.Template = GetTemplate(); target.Items = new[] { "Foo" }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Layout(target); Assert.NotEmpty(target.GetLogicalChildren()); @@ -285,19 +262,19 @@ public void Adding_Items_Should_Fire_LogicalChildren_CollectionChanged() target.Template = GetTemplate(); target.Items = items; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Layout(target); ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = e.Action == NotifyCollectionChangedAction.Add; items.Add("Bar"); + Layout(target.Presenter); Assert.True(called); } [Fact] - public void Removing_Items_Should_Fire_LogicalChildren_CollectionChanged() + public void Removing_Items_Should_Not_Fire_LogicalChildren_CollectionChanged() { var target = new ItemsControl(); var items = new AvaloniaList { "Foo", "Bar" }; @@ -305,15 +282,17 @@ public void Removing_Items_Should_Fire_LogicalChildren_CollectionChanged() target.Template = GetTemplate(); target.Items = items; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Layout(target); ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = e.Action == NotifyCollectionChangedAction.Remove; items.Remove("Bar"); + Layout(target.Presenter); - Assert.True(called); + // In this case, the control will be marked for recycling and so should remain in the + // logical children collection for performance reasons. + Assert.False(called); } [Fact] @@ -345,15 +324,14 @@ public void Should_Clear_Containers_When_ItemsPresenter_Changes() Template = GetTemplate(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Layout(target); - Assert.Equal(2, target.ItemContainerGenerator.Containers.Count()); + Assert.Equal(2, target.Presenter.RealizedElements.Count()); target.Template = GetTemplate(); target.ApplyTemplate(); - Assert.Empty(target.ItemContainerGenerator.Containers); + Assert.Empty(target.Presenter.RealizedElements); } [Fact] @@ -417,37 +395,39 @@ public void Setting_Presenter_Explicitly_Should_Set_Item_Parent() [Fact] public void DataContexts_Should_Be_Correctly_Set() { - var items = new object[] + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) { - "Foo", - new Item("Bar"), - new TextBlock { Text = "Baz" }, - new ListBoxItem { Content = "Qux" }, - }; + var items = new object[] + { + "Foo", + new Item("Bar"), + new TextBlock { Text = "Baz" }, + new ListBoxItem { Content = "Qux" }, + }; - var target = new ItemsControl - { - Template = GetTemplate(), - DataContext = "Base", - DataTemplates = + var target = new ItemsControl { - new FuncDataTemplate((x, __) => new Button { Content = x }) - }, - Items = items, - }; + Template = GetTemplate(), + DataContext = "Base", + DataTemplates = + { + new FuncDataTemplate((x, __) => new Button { Content = x }) + }, + Items = items, + }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Layout(target); - var dataContexts = target.Presenter.Panel.Children - .Do(x => (x as ContentPresenter)?.UpdateChild()) - .Cast() - .Select(x => x.DataContext) - .ToList(); + var dataContexts = target.Presenter.RealizedElements + .Do(x => (x as ContentPresenter)?.UpdateChild()) + .Cast() + .Select(x => x.DataContext) + .ToList(); - Assert.Equal( - new object[] { items[0], items[1], "Base", "Base" }, - dataContexts); + Assert.Equal( + new object[] { items[0], items[1], "Base", "Base" }, + dataContexts); + } } [Fact] @@ -464,10 +444,9 @@ public void Control_Item_Should_Not_Be_NameScope() Items = items, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Layout(target); - var item = target.Presenter.Panel.LogicalChildren[0]; + var item = target.Presenter.RealizedElements.First(); Assert.Null(NameScope.GetNameScope((TextBlock)item)); } @@ -478,8 +457,8 @@ public void Focuses_Next_Item_On_Key_Down() { var items = new object[] { - new Button(), - new Button(), + new Button { Height = 10 }, + new Button { Height = 10 }, }; var target = new ItemsControl @@ -490,9 +469,8 @@ public void Focuses_Next_Item_On_Key_Down() var root = new TestRoot { Child = target }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - target.Presenter.Panel.Children[0].Focus(); + Layout(root); + target.Presenter.RealizedElements.First().Focus(); target.RaiseEvent(new KeyEventArgs { @@ -501,7 +479,7 @@ public void Focuses_Next_Item_On_Key_Down() }); Assert.Equal( - target.Presenter.Panel.Children[1], + target.Presenter.RealizedElements.ElementAt(1), FocusManager.Instance.Current); } } @@ -513,9 +491,9 @@ public void Does_Not_Focus_Non_Focusable_Item_On_Key_Down() { var items = new object[] { - new Button(), - new Button { Focusable = false }, - new Button(), + new Button { Height = 10 }, + new Button { Height = 10, Focusable = false }, + new Button { Height = 10 }, }; var target = new ItemsControl @@ -526,9 +504,8 @@ public void Does_Not_Focus_Non_Focusable_Item_On_Key_Down() var root = new TestRoot { Child = target }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - target.Presenter.Panel.Children[0].Focus(); + Layout(root); + target.Presenter.RealizedElements.First().Focus(); target.RaiseEvent(new KeyEventArgs { @@ -537,41 +514,11 @@ public void Does_Not_Focus_Non_Focusable_Item_On_Key_Down() }); Assert.Equal( - target.Presenter.Panel.Children[2], + target.Presenter.RealizedElements.ElementAt(2), FocusManager.Instance.Current); } } - [Fact] - public void Presenter_Items_Should_Be_In_Sync() - { - var target = new ItemsControl - { - Template = GetTemplate(), - Items = new object[] - { - new Button(), - new Button(), - }, - }; - - var root = new TestRoot { Child = target }; - var otherPanel = new StackPanel(); - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - target.ItemContainerGenerator.Materialized += (s, e) => - { - Assert.IsType(e.Containers[0].Item); - }; - - target.Items = new[] - { - new Canvas() - }; - } - [Fact] public void Detaching_Then_Reattaching_To_Logical_Tree_Twice_Does_Not_Throw() { @@ -596,6 +543,12 @@ public void Detaching_Then_Reattaching_To_Logical_Tree_Twice_Does_Not_Throw() root.Child = target; } + private static void Layout(IControl target) + { + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + } + private class Item { public Item(string value) @@ -616,7 +569,8 @@ private FuncControlTemplate GetTemplate() Child = new ItemsPresenter { Name = "PART_ItemsPresenter", - [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], + [~ItemsPresenter.LayoutProperty] = parent[~ItemsControl.LayoutProperty], + [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsViewProperty], }.RegisterInNameScope(scope) }; }); diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 2e2ccf73260..c88b73fa410 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -1,9 +1,11 @@ +using System; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.UnitTests; @@ -12,33 +14,33 @@ namespace Avalonia.Controls.UnitTests { - public class ListBoxTests + public partial class ListBoxTests { private MouseTestHelper _mouse = new MouseTestHelper(); [Fact] public void Should_Use_ItemTemplate_To_Create_Item_Content() { + using var app = Start(); + var target = new ListBox { - Template = ListBoxTemplate(), Items = new[] { "Foo" }, ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()), }; Prepare(target); - var container = (ListBoxItem)target.Presenter.Panel.Children[0]; + var container = (ListBoxItem)target.Presenter.RealizedElements.First(); Assert.IsType(container.Presenter.Child); } [Fact] public void ListBox_Should_Find_ItemsPresenter_In_ScrollViewer() { - var target = new ListBox - { - Template = ListBoxTemplate(), - }; + using var app = Start(); + + var target = new ListBox(); Prepare(target); @@ -48,11 +50,9 @@ public void ListBox_Should_Find_ItemsPresenter_In_ScrollViewer() [Fact] public void ListBox_Should_Find_Scrollviewer_In_Template() { - var target = new ListBox - { - Template = ListBoxTemplate(), - }; + using var app = Start(); + var target = new ListBox(); ScrollViewer viewer = null; target.TemplateApplied += (sender, e) => @@ -68,93 +68,82 @@ public void ListBox_Should_Find_Scrollviewer_In_Template() [Fact] public void ListBoxItem_Containers_Should_Be_Generated() { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) - { - var items = new[] { "Foo", "Bar", "Baz " }; - var target = new ListBox - { - Template = ListBoxTemplate(), - Items = items, - }; + using var app = Start(); - Prepare(target); + var items = new[] { "Foo", "Bar", "Baz " }; + var target = new ListBox { Items = items }; - var text = target.Presenter.Panel.Children - .OfType() - .Select(x => x.Presenter.Child) - .OfType() - .Select(x => x.Text) - .ToList(); + Prepare(target); - Assert.Equal(items, text); - } + var text = target.Presenter.RealizedElements + .OfType() + .Select(x => x.Presenter.Child) + .OfType() + .Select(x => x.Text) + .ToList(); + + Assert.Equal(items, text); } [Fact] public void LogicalChildren_Should_Be_Set_For_DataTemplate_Generated_Items() { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) - { - var target = new ListBox - { - Template = ListBoxTemplate(), - Items = new[] { "Foo", "Bar", "Baz " }, - }; + using var app = Start(); + + var target = new ListBox { Items = new[] { "Foo", "Bar", "Baz " } }; - Prepare(target); + Prepare(target); - Assert.Equal(3, target.GetLogicalChildren().Count()); + Assert.Equal(3, target.GetLogicalChildren().Count()); - foreach (var child in target.GetLogicalChildren()) - { - Assert.IsType(child); - } + foreach (var child in target.GetLogicalChildren()) + { + Assert.IsType(child); } } [Fact] public void DataContexts_Should_Be_Correctly_Set() { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + using var app = Start(); + + var items = new object[] { - var items = new object[] - { - "Foo", - new Item("Bar"), - new TextBlock { Text = "Baz" }, - new ListBoxItem { Content = "Qux" }, - }; + "Foo", + new Item("Bar"), + new TextBlock { Text = "Baz" }, + new ListBoxItem { Content = "Qux" }, + }; - var target = new ListBox + var target = new ListBox + { + DataContext = "Base", + DataTemplates = { - Template = ListBoxTemplate(), - DataContext = "Base", - DataTemplates = - { - new FuncDataTemplate((x, _) => new Button { Content = x }) - }, - Items = items, - }; + new FuncDataTemplate((x, _) => new Button { Content = x }) + }, + Items = items, + }; - Prepare(target); + Prepare(target); - var dataContexts = target.Presenter.Panel.Children - .Cast() - .Select(x => x.DataContext) - .ToList(); + var dataContexts = target.Presenter.RealizedElements + .Cast() + .Select(x => x.DataContext) + .ToList(); - Assert.Equal( - new object[] { items[0], items[1], "Base", "Base" }, - dataContexts); - } + Assert.Equal( + new object[] { items[0], items[1], "Base", "Base" }, + dataContexts); } [Fact] public void Selection_Should_Be_Cleared_On_Recycled_Items() { + using var app = Start(); + var target = new ListBox { - Template = ListBoxTemplate(), Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(), ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), SelectedIndex = 0, @@ -163,22 +152,24 @@ public void Selection_Should_Be_Cleared_On_Recycled_Items() Prepare(target); // Make sure we're virtualized and first item is selected. - Assert.Equal(10, target.Presenter.Panel.Children.Count); - Assert.True(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected); + Assert.Equal(11, target.Presenter.RealizedElements.Count()); + Assert.True(((ListBoxItem)target.Presenter.RealizedElements.First()).IsSelected); // Scroll down a page. - target.Scroll.Offset = new Vector(0, 10); + target.Scroll.Offset = new Vector(0, 100); + Layout(target); // Make sure recycled item isn't now selected. - Assert.False(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected); + Assert.False(((ListBoxItem)target.Presenter.RealizedElements.First()).IsSelected); } [Fact] public void ScrollViewer_Should_Have_Correct_Extent_And_Viewport() { + using var app = Start(); + var target = new ListBox { - Template = ListBoxTemplate(), Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(), ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), SelectedIndex = 0, @@ -186,18 +177,19 @@ public void ScrollViewer_Should_Have_Correct_Extent_And_Viewport() Prepare(target); - Assert.Equal(new Size(20, 20), target.Scroll.Extent); - Assert.Equal(new Size(100, 10), target.Scroll.Viewport); + Assert.Equal(new Size(100, 200), target.Scroll.Extent); + Assert.Equal(new Size(100, 100), target.Scroll.Viewport); } [Fact] public void Containers_Correct_After_Clear_Add_Remove() { + using var app = Start(); + // Issue #1936 var items = new AvaloniaList(Enumerable.Range(0, 11).Select(x => $"Item {x}")); var target = new ListBox { - Template = ListBoxTemplate(), Items = items, ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), SelectedIndex = 0, @@ -209,18 +201,21 @@ public void Containers_Correct_After_Clear_Add_Remove() items.AddRange(Enumerable.Range(0, 11).Select(x => $"Item {x}")); items.Remove("Item 2"); + Layout(target); + Assert.Equal( items, - target.Presenter.Panel.Children.Cast().Select(x => (string)x.Content)); + target.Presenter.RealizedElements.Cast().Select(x => (string)x.Content)); } [Fact] public void Toggle_Selection_Should_Update_Containers() { + using var app = Start(); + var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray(); var target = new ListBox { - Template = ListBoxTemplate(), Items = items, SelectionMode = SelectionMode.Toggle, ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }) @@ -246,10 +241,11 @@ public void Toggle_Selection_Should_Update_Containers() [Fact] public void Can_Decrease_Number_Of_Materialized_Items_By_Removing_From_Source_Collection() { + using var app = Start(); + var items = new AvaloniaList(Enumerable.Range(0, 20).Select(x => $"Item {x}")); var target = new ListBox { - Template = ListBoxTemplate(), Items = items, ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }) }; @@ -265,130 +261,90 @@ private void RaisePressedEvent(ListBox listBox, ListBoxItem item, MouseButton mo _mouse.Click(listBox, item, mouseButton); } - [Fact] - public void ListBox_After_Scroll_IndexOutOfRangeException_Shouldnt_Be_Thrown() - { - var items = Enumerable.Range(0, 11).Select(x => $"{x}").ToArray(); - - var target = new ListBox - { - Template = ListBoxTemplate(), - Items = items, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 11 }) - }; - - Prepare(target); - - var panel = target.Presenter.Panel as IVirtualizingPanel; - - var listBoxItems = panel.Children.OfType(); - - //virtualization should have created exactly 10 items - Assert.Equal(10, listBoxItems.Count()); - Assert.Equal("0", listBoxItems.First().DataContext); - Assert.Equal("9", listBoxItems.Last().DataContext); - - //instead pixeloffset > 0 there could be pretty complex sequence for repro - //it involves add/remove/scroll to end multiple actions - //which i can't find so far :(, but this is the simplest way to add it to unit test - panel.PixelOffset = 1; - - //here scroll to end -> IndexOutOfRangeException is thrown - target.Scroll.Offset = new Vector(0, 2); - - Assert.True(true); - } - [Fact] public void LayoutManager_Should_Measure_Arrange_All() { - var virtualizationMode = ItemVirtualizationMode.Simple; - using (UnitTestApplication.Start(TestServices.StyledWindow)) - { - var items = new AvaloniaList(Enumerable.Range(1, 7).Select(v => v.ToString())); - - var wnd = new Window() { SizeToContent = SizeToContent.WidthAndHeight }; - - wnd.IsVisible = true; + using var app = Start(); - var target = new ListBox(); + var items = new AvaloniaList(Enumerable.Range(1, 7).Select(v => v.ToString())); + var wnd = new Window() { SizeToContent = SizeToContent.WidthAndHeight, IsVisible = true }; + var target = new ListBox(); - wnd.Content = target; + wnd.Content = target; - var lm = wnd.LayoutManager; + var lm = wnd.LayoutManager; - target.Height = 110; - target.Width = 50; - target.DataContext = items; - target.VirtualizationMode = virtualizationMode; + target.Height = 110; + target.Width = 50; + target.DataContext = items; - target.ItemTemplate = new FuncDataTemplate((c, _) => - { - var tb = new TextBlock() { Height = 10, Width = 30 }; - tb.Bind(TextBlock.TextProperty, new Data.Binding()); - return tb; - }, true); + target.ItemTemplate = new FuncDataTemplate((c, _) => + { + var tb = new TextBlock() { Height = 10, Width = 30 }; + tb.Bind(TextBlock.TextProperty, new Data.Binding()); + return tb; + }, true); - lm.ExecuteInitialLayoutPass(); + lm.ExecuteInitialLayoutPass(); - target.Items = items; + target.Items = items; - lm.ExecuteLayoutPass(); + lm.ExecuteLayoutPass(); - items.Insert(3, "3+"); - lm.ExecuteLayoutPass(); + items.Insert(3, "3+"); + lm.ExecuteLayoutPass(); - items.Insert(4, "4+"); - lm.ExecuteLayoutPass(); + items.Insert(4, "4+"); + lm.ExecuteLayoutPass(); - //RESET - items.Clear(); - foreach (var i in Enumerable.Range(1, 7)) - { - items.Add(i.ToString()); - } + //RESET + items.Clear(); + foreach (var i in Enumerable.Range(1, 7)) + { + items.Add(i.ToString()); + } - //working bit better with this line no outof memory or remaining to arrange/measure ??? - //lm.ExecuteLayoutPass(); + //working bit better with this line no outof memory or remaining to arrange/measure ??? + //lm.ExecuteLayoutPass(); - items.Insert(2, "2+"); + items.Insert(2, "2+"); - lm.ExecuteLayoutPass(); - //after few more layout cycles layoutmanager shouldn't hold any more visual for measure/arrange - lm.ExecuteLayoutPass(); - lm.ExecuteLayoutPass(); + lm.ExecuteLayoutPass(); + //after few more layout cycles layoutmanager shouldn't hold any more visual for measure/arrange + lm.ExecuteLayoutPass(); + lm.ExecuteLayoutPass(); - var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic; - var toMeasure = lm.GetType().GetField("_toMeasure", flags).GetValue(lm) as System.Collections.Generic.IEnumerable; - var toArrange = lm.GetType().GetField("_toArrange", flags).GetValue(lm) as System.Collections.Generic.IEnumerable; + var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic; + var toMeasure = lm.GetType().GetField("_toMeasure", flags).GetValue(lm) as System.Collections.Generic.IEnumerable; + var toArrange = lm.GetType().GetField("_toArrange", flags).GetValue(lm) as System.Collections.Generic.IEnumerable; - Assert.Equal(0, toMeasure.Count()); - Assert.Equal(0, toArrange.Count()); - } + Assert.Equal(0, toMeasure.Count()); + Assert.Equal(0, toArrange.Count()); } [Fact] public void Clicking_Item_Should_Raise_BringIntoView_For_Correct_Control() { + using var app = Start(); + // Issue #3934 var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray(); var target = new ListBox { - Template = ListBoxTemplate(), Items = items, ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), SelectionMode = SelectionMode.AlwaysSelected, - VirtualizationMode = ItemVirtualizationMode.None, + Layout = new NonVirtualizingStackLayout(), }; Prepare(target); // First an item that is not index 0 must be selected. - _mouse.Click(target.Presenter.Panel.Children[1]); + _mouse.Click(target.Presenter.RealizedElements.ElementAt(1)); Assert.Equal(1, target.Selection.AnchorIndex); // We're going to be clicking on item 9. - var item = (ListBoxItem)target.Presenter.Panel.Children[9]; + var item = (ListBoxItem)target.Presenter.RealizedElements.ElementAt(9); var raised = 0; // Make sure a RequestBringIntoView event is raised for item 9. It won't be handled @@ -407,7 +363,51 @@ public void Clicking_Item_Should_Raise_BringIntoView_For_Correct_Control() Assert.Equal(1, raised); } - private FuncControlTemplate ListBoxTemplate() + private static IDisposable Start() + { + var services = TestServices.MockPlatformRenderInterface.With( + focusManager: new FocusManager(), + styler: new Styler(), + windowingPlatform: new MockWindowingPlatform()); + return UnitTestApplication.Start(services); + } + + private static void Prepare(ListBox target) + { + var root = new TestRoot + { + Child = target, + Width = 100, + Height = 100, + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(ListBox.TemplateProperty, ListBoxTemplate()), + }, + }, + new Style(x => x.OfType()) + { + Setters = + { + new Setter(ListBoxItem.TemplateProperty, ListBoxItemTemplate()), + }, + }, + }, + }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + } + + private void Layout(ListBox target) + { + var root = (TestRoot)target.GetVisualRoot(); + root.LayoutManager.ExecuteLayoutPass(); + } + + private static FuncControlTemplate ListBoxTemplate() { return new FuncControlTemplate((parent, scope) => new ScrollViewer @@ -417,14 +417,15 @@ private FuncControlTemplate ListBoxTemplate() Content = new ItemsPresenter { Name = "PART_ItemsPresenter", + HorizontalCacheLength = 0, + VerticalCacheLength = 0, [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(), - [~ItemsPresenter.ItemsPanelProperty] = parent.GetObservable(ItemsControl.ItemsPanelProperty).ToBinding(), - [~ItemsPresenter.VirtualizationModeProperty] = parent.GetObservable(ListBox.VirtualizationModeProperty).ToBinding(), + [~ItemsPresenter.LayoutProperty] = parent.GetObservable(ItemsControl.LayoutProperty).ToBinding(), }.RegisterInNameScope(scope) }.RegisterInNameScope(scope)); } - private FuncControlTemplate ListBoxItemTemplate() + private static FuncControlTemplate ListBoxItemTemplate() { return new FuncControlTemplate((parent, scope) => new ContentPresenter @@ -435,7 +436,7 @@ private FuncControlTemplate ListBoxItemTemplate() }.RegisterInNameScope(scope)); } - private FuncControlTemplate ScrollViewerTemplate() + private static FuncControlTemplate ScrollViewerTemplate() { return new FuncControlTemplate((parent, scope) => new Panel @@ -445,6 +446,8 @@ private FuncControlTemplate ScrollViewerTemplate() new ScrollContentPresenter { Name = "PART_ContentPresenter", + [~ScrollContentPresenter.CanHorizontallyScrollProperty] = parent.GetObservable(ScrollViewer.CanHorizontallyScrollProperty).ToBinding(), + [~ScrollContentPresenter.CanVerticallyScrollProperty] = parent.GetObservable(ScrollViewer.CanVerticallyScrollProperty).ToBinding(), [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(), [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty], [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty], @@ -460,49 +463,6 @@ private FuncControlTemplate ScrollViewerTemplate() }); } - private void Prepare(ListBox target) - { - // The ListBox needs to be part of a rooted visual tree. - var root = new TestRoot(); - root.Child = target; - - // Apply the template to the ListBox itself. - target.ApplyTemplate(); - - // Then to its inner ScrollViewer. - var scrollViewer = (ScrollViewer)target.GetVisualChildren().Single(); - scrollViewer.ApplyTemplate(); - - // Then make the ScrollViewer create its child. - ((ContentPresenter)scrollViewer.Presenter).UpdateChild(); - - // Now the ItemsPresenter should be reigstered, so apply its template. - target.Presenter.ApplyTemplate(); - - // Because ListBox items are virtualized we need to do a layout to make them appear. - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - // Now set and apply the item templates. - foreach (ListBoxItem item in target.Presenter.Panel.Children) - { - item.Template = ListBoxItemTemplate(); - item.ApplyTemplate(); - item.Presenter.ApplyTemplate(); - ((ContentPresenter)item.Presenter).UpdateChild(); - } - - // The items were created before the template was applied, so now we need to go back - // and re-arrange everything. - foreach (IControl i in target.GetSelfAndVisualDescendants()) - { - i.InvalidateMeasure(); - } - - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - } - private class Item { public Item(string value) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs index 7c7cdd08db5..1c411225498 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs @@ -1,31 +1,27 @@ using System.Linq; -using Avalonia.Controls.Presenters; -using Avalonia.Controls.Templates; using Avalonia.Input; -using Avalonia.Styling; -using Avalonia.UnitTests; -using Avalonia.VisualTree; using Xunit; namespace Avalonia.Controls.UnitTests { - public class ListBoxTests_Multiple + public partial class ListBoxTests { [Fact] public void Focusing_Item_With_Shift_And_Arrow_Key_Should_Add_To_Selection() { + using var app = Start(); + var target = new ListBox { - Template = new FuncControlTemplate(CreateListBoxTemplate), Items = new[] { "Foo", "Bar", "Baz " }, SelectionMode = SelectionMode.Multiple }; - ApplyTemplate(target); + Prepare(target); target.SelectedItem = "Foo"; - target.Presenter.Panel.Children[1].RaiseEvent(new GotFocusEventArgs + target.Presenter.RealizedElements.ElementAt(1).RaiseEvent(new GotFocusEventArgs { RoutedEvent = InputElement.GotFocusEvent, NavigationMethod = NavigationMethod.Directional, @@ -38,18 +34,19 @@ public void Focusing_Item_With_Shift_And_Arrow_Key_Should_Add_To_Selection() [Fact] public void Focusing_Item_With_Ctrl_And_Arrow_Key_Should_Add_To_Selection() { + using var app = Start(); + var target = new ListBox { - Template = new FuncControlTemplate(CreateListBoxTemplate), Items = new[] { "Foo", "Bar", "Baz " }, SelectionMode = SelectionMode.Multiple }; - ApplyTemplate(target); + Prepare(target); target.SelectedItem = "Foo"; - target.Presenter.Panel.Children[1].RaiseEvent(new GotFocusEventArgs + target.Presenter.RealizedElements.ElementAt(1).RaiseEvent(new GotFocusEventArgs { RoutedEvent = InputElement.GotFocusEvent, NavigationMethod = NavigationMethod.Directional, @@ -62,19 +59,20 @@ public void Focusing_Item_With_Ctrl_And_Arrow_Key_Should_Add_To_Selection() [Fact] public void Focusing_Selected_Item_With_Ctrl_And_Arrow_Key_Should_Remove_From_Selection() { + using var app = Start(); + var target = new ListBox { - Template = new FuncControlTemplate(CreateListBoxTemplate), Items = new[] { "Foo", "Bar", "Baz " }, SelectionMode = SelectionMode.Multiple }; - ApplyTemplate(target); + Prepare(target); target.SelectedItems.Add("Foo"); target.SelectedItems.Add("Bar"); - target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs + target.Presenter.RealizedElements.ElementAt(0).RaiseEvent(new GotFocusEventArgs { RoutedEvent = InputElement.GotFocusEvent, NavigationMethod = NavigationMethod.Directional, @@ -83,44 +81,5 @@ public void Focusing_Selected_Item_With_Ctrl_And_Arrow_Key_Should_Remove_From_Se Assert.Equal(new[] { "Bar" }, target.SelectedItems); } - - private Control CreateListBoxTemplate(ITemplatedControl parent, INameScope scope) - { - return new ScrollViewer - { - Template = new FuncControlTemplate(CreateScrollViewerTemplate), - Content = new ItemsPresenter - { - Name = "PART_ItemsPresenter", - [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(), - }.RegisterInNameScope(scope) - }; - } - - private Control CreateScrollViewerTemplate(ITemplatedControl parent, INameScope scope) - { - return new ScrollContentPresenter - { - Name = "PART_ContentPresenter", - [~ContentPresenter.ContentProperty] = - parent.GetObservable(ContentControl.ContentProperty).ToBinding(), - }.RegisterInNameScope(scope); - } - - private void ApplyTemplate(ListBox target) - { - // Apply the template to the ListBox itself. - target.ApplyTemplate(); - - // Then to its inner ScrollViewer. - var scrollViewer = (ScrollViewer)target.GetVisualChildren().Single(); - scrollViewer.ApplyTemplate(); - - // Then make the ScrollViewer create its child. - ((ContentPresenter)scrollViewer.Presenter).UpdateChild(); - - // Now the ItemsPresenter should be reigstered, so apply its template. - target.Presenter.ApplyTemplate(); - } } } diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Selection.cs similarity index 62% rename from tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs rename to tests/Avalonia.Controls.UnitTests/ListBoxTests_Selection.cs index 8f795104bfd..b18c874ad01 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Selection.cs @@ -1,34 +1,28 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using Avalonia.Controls.Presenters; -using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.LogicalTree; -using Avalonia.Styling; -using Avalonia.UnitTests; -using Avalonia.VisualTree; using Xunit; namespace Avalonia.Controls.UnitTests { - public class ListBoxTests_Single + public partial class ListBoxTests { - MouseTestHelper _mouse = new MouseTestHelper(); - [Fact] public void Focusing_Item_With_Tab_Should_Not_Select_It() { + using var app = Start(); + var target = new ListBox { - Template = new FuncControlTemplate(CreateListBoxTemplate), Items = new[] { "Foo", "Bar", "Baz " }, }; - ApplyTemplate(target); + Prepare(target); - target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs + target.Presenter.RealizedElements.First().RaiseEvent(new GotFocusEventArgs { RoutedEvent = InputElement.GotFocusEvent, NavigationMethod = NavigationMethod.Tab, @@ -40,15 +34,16 @@ public void Focusing_Item_With_Tab_Should_Not_Select_It() [Fact] public void Focusing_Item_With_Arrow_Key_Should_Select_It() { + using var app = Start(); + var target = new ListBox { - Template = new FuncControlTemplate(CreateListBoxTemplate), Items = new[] { "Foo", "Bar", "Baz " }, }; - ApplyTemplate(target); + Prepare(target); - target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs + target.Presenter.RealizedElements.First().RaiseEvent(new GotFocusEventArgs { RoutedEvent = InputElement.GotFocusEvent, NavigationMethod = NavigationMethod.Directional, @@ -60,14 +55,15 @@ public void Focusing_Item_With_Arrow_Key_Should_Select_It() [Fact] public void Clicking_Item_Should_Select_It() { + using var app = Start(); + var target = new ListBox { - Template = new FuncControlTemplate(CreateListBoxTemplate), Items = new[] { "Foo", "Bar", "Baz " }, }; - ApplyTemplate(target); - _mouse.Click(target.Presenter.Panel.Children[0]); + Prepare(target); + _mouse.Click(target.Presenter.RealizedElements.First()); Assert.Equal(0, target.SelectedIndex); } @@ -75,16 +71,17 @@ public void Clicking_Item_Should_Select_It() [Fact] public void Clicking_Selected_Item_Should_Not_Deselect_It() { + using var app = Start(); + var target = new ListBox { - Template = new FuncControlTemplate(CreateListBoxTemplate), Items = new[] { "Foo", "Bar", "Baz " }, }; - ApplyTemplate(target); + Prepare(target); target.SelectedIndex = 0; - _mouse.Click(target.Presenter.Panel.Children[0]); + _mouse.Click(target.Presenter.RealizedElements.First()); Assert.Equal(0, target.SelectedIndex); } @@ -92,16 +89,17 @@ public void Clicking_Selected_Item_Should_Not_Deselect_It() [Fact] public void Clicking_Item_Should_Select_It_When_SelectionMode_Toggle() { + using var app = Start(); + var target = new ListBox { - Template = new FuncControlTemplate(CreateListBoxTemplate), Items = new[] { "Foo", "Bar", "Baz " }, SelectionMode = SelectionMode.Single | SelectionMode.Toggle, }; - ApplyTemplate(target); + Prepare(target); - _mouse.Click(target.Presenter.Panel.Children[0]); + _mouse.Click(target.Presenter.RealizedElements.First()); Assert.Equal(0, target.SelectedIndex); } @@ -109,17 +107,18 @@ public void Clicking_Item_Should_Select_It_When_SelectionMode_Toggle() [Fact] public void Clicking_Selected_Item_Should_Deselect_It_When_SelectionMode_Toggle() { + using var app = Start(); + var target = new ListBox { - Template = new FuncControlTemplate(CreateListBoxTemplate), Items = new[] { "Foo", "Bar", "Baz " }, SelectionMode = SelectionMode.Toggle, }; - ApplyTemplate(target); + Prepare(target); target.SelectedIndex = 0; - _mouse.Click(target.Presenter.Panel.Children[0]); + _mouse.Click(target.Presenter.RealizedElements.First()); Assert.Equal(-1, target.SelectedIndex); } @@ -127,17 +126,18 @@ public void Clicking_Selected_Item_Should_Deselect_It_When_SelectionMode_Toggle( [Fact] public void Clicking_Selected_Item_Should_Not_Deselect_It_When_SelectionMode_ToggleAlwaysSelected() { + using var app = Start(); + var target = new ListBox { - Template = new FuncControlTemplate(CreateListBoxTemplate), Items = new[] { "Foo", "Bar", "Baz " }, SelectionMode = SelectionMode.Toggle | SelectionMode.AlwaysSelected, }; - ApplyTemplate(target); + Prepare(target); target.SelectedIndex = 0; - _mouse.Click(target.Presenter.Panel.Children[0]); + _mouse.Click(target.Presenter.RealizedElements.First()); Assert.Equal(0, target.SelectedIndex); } @@ -145,17 +145,18 @@ public void Clicking_Selected_Item_Should_Not_Deselect_It_When_SelectionMode_Tog [Fact] public void Clicking_Another_Item_Should_Select_It_When_SelectionMode_Toggle() { + using var app = Start(); + var target = new ListBox { - Template = new FuncControlTemplate(CreateListBoxTemplate), Items = new[] { "Foo", "Bar", "Baz " }, SelectionMode = SelectionMode.Single | SelectionMode.Toggle, }; - ApplyTemplate(target); + Prepare(target); target.SelectedIndex = 1; - _mouse.Click(target.Presenter.Panel.Children[0]); + _mouse.Click(target.Presenter.RealizedElements.First()); Assert.Equal(0, target.SelectedIndex); } @@ -163,13 +164,14 @@ public void Clicking_Another_Item_Should_Select_It_When_SelectionMode_Toggle() [Fact] public void Setting_Item_IsSelected_Sets_ListBox_Selection() { + using var app = Start(); + var target = new ListBox { - Template = new FuncControlTemplate(CreateListBoxTemplate), Items = new[] { "Foo", "Bar", "Baz " }, }; - ApplyTemplate(target); + Prepare(target); ((ListBoxItem)target.GetLogicalChildren().ElementAt(1)).IsSelected = true; @@ -180,6 +182,8 @@ public void Setting_Item_IsSelected_Sets_ListBox_Selection() [Fact] public void SelectedItem_Should_Not_Cause_StackOverflow() { + using var app = Start(); + var viewModel = new TestStackOverflowViewModel() { Items = new List { "foo", "bar", "baz" } @@ -187,7 +191,6 @@ public void SelectedItem_Should_Not_Cause_StackOverflow() var target = new ListBox { - Template = new FuncControlTemplate(CreateListBoxTemplate), DataContext = viewModel, Items = viewModel.Items }; @@ -241,44 +244,5 @@ public string SelectedItem } } } - - private Control CreateListBoxTemplate(ITemplatedControl parent, INameScope scope) - { - return new ScrollViewer - { - Template = new FuncControlTemplate(CreateScrollViewerTemplate), - Content = new ItemsPresenter - { - Name = "PART_ItemsPresenter", - [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(), - }.RegisterInNameScope(scope) - }; - } - - private Control CreateScrollViewerTemplate(ITemplatedControl parent, INameScope scope) - { - return new ScrollContentPresenter - { - Name = "PART_ContentPresenter", - [~ContentPresenter.ContentProperty] = - parent.GetObservable(ContentControl.ContentProperty).ToBinding(), - }.RegisterInNameScope(scope); - } - - private void ApplyTemplate(ListBox target) - { - // Apply the template to the ListBox itself. - target.ApplyTemplate(); - - // Then to its inner ScrollViewer. - var scrollViewer = (ScrollViewer)target.GetVisualChildren().Single(); - scrollViewer.ApplyTemplate(); - - // Then make the ScrollViewer create its child. - ((ContentPresenter)scrollViewer.Presenter).UpdateChild(); - - // Now the ItemsPresenter should be reigstered, so apply its template. - target.Presenter.ApplyTemplate(); - } } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs index 11c97871143..cc11f074fac 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs @@ -9,722 +9,722 @@ namespace Avalonia.Controls.UnitTests.Presenters { - public class CarouselPresenterTests - { - [Fact] - public void Should_Register_With_Host_When_TemplatedParent_Set() - { - var host = new Mock(); - var target = new CarouselPresenter(); - - target.SetValue(Control.TemplatedParentProperty, host.Object); - - host.Verify(x => x.RegisterItemsPresenter(target)); - } - - [Fact] - public void ApplyTemplate_Should_Create_Panel() - { - var target = new CarouselPresenter - { - ItemsPanel = new FuncTemplate(() => new Panel()), - }; - - target.ApplyTemplate(); - - Assert.IsType(target.Panel); - } - - [Fact] - public void ItemContainerGenerator_Should_Be_Picked_Up_From_TemplatedControl() - { - var parent = new TestItemsControl(); - var target = new CarouselPresenter - { - [StyledElement.TemplatedParentProperty] = parent, - }; - - Assert.IsType>(target.ItemContainerGenerator); - } - - public class Virtualized - { - [Fact] - public void Should_Initially_Materialize_Selected_Container() - { - var target = new CarouselPresenter - { - Items = new[] { "foo", "bar" }, - SelectedIndex = 0, - IsVirtualized = true, - }; - - target.ApplyTemplate(); - - AssertSingle(target); - } - - [Fact] - public void Should_Initially_Materialize_Nothing_If_No_Selected_Container() - { - var target = new CarouselPresenter - { - Items = new[] { "foo", "bar" }, - IsVirtualized = true, - }; - - target.ApplyTemplate(); - - Assert.Empty(target.Panel.Children); - Assert.Empty(target.ItemContainerGenerator.Containers); - } - - [Fact] - public void Switching_To_Virtualized_Should_Reset_Containers() - { - var target = new CarouselPresenter - { - Items = new[] { "foo", "bar" }, - SelectedIndex = 0, - IsVirtualized = false, - }; - - target.ApplyTemplate(); - target.IsVirtualized = true; - - AssertSingle(target); - } - - [Fact] - public void Changing_SelectedIndex_Should_Show_Page() - { - var target = new CarouselPresenter - { - Items = new[] { "foo", "bar" }, - SelectedIndex = 0, - IsVirtualized = true, - }; - - target.ApplyTemplate(); - AssertSingle(target); - - target.SelectedIndex = 1; - AssertSingle(target); - } - - [Fact] - public void Should_Remove_NonCurrent_Page() - { - var target = new CarouselPresenter - { - Items = new[] { "foo", "bar" }, - IsVirtualized = true, - SelectedIndex = 0, - }; - - target.ApplyTemplate(); - AssertSingle(target); - - target.SelectedIndex = 1; - AssertSingle(target); - - } - - [Fact] - public void Should_Handle_Inserting_Item_At_SelectedItem() - { - var items = new ObservableCollection - { - "item0", - "item1", - "item2", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 1, - IsVirtualized = true, - }; - - target.ApplyTemplate(); - - items.Insert(1, "item1a"); - AssertSingle(target); - } - - [Fact] - public void Should_Handle_Inserting_Item_Before_SelectedItem() - { - var items = new ObservableCollection - { - "item0", - "item1", - "item2", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 2, - IsVirtualized = true, - }; - - target.ApplyTemplate(); - - items.Insert(1, "item1a"); - AssertSingle(target); - } - - [Fact] - public void Should_Do_Nothing_When_Inserting_Item_After_SelectedItem() - { - var items = new ObservableCollection - { - "item0", - "item1", - "item2", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 1, - IsVirtualized = true, - }; - - target.ApplyTemplate(); - var child = AssertSingle(target); - items.Insert(2, "after"); - Assert.Same(child, AssertSingle(target)); - } - - [Fact] - public void Should_Handle_Removing_Item_At_SelectedItem() - { - var items = new ObservableCollection - { - "item0", - "item1", - "item2", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 1, - IsVirtualized = true, - }; - - target.ApplyTemplate(); - - items.RemoveAt(1); - AssertSingle(target); - } - - [Fact] - public void Should_Handle_Removing_Item_Before_SelectedItem() - { - var items = new ObservableCollection - { - "item0", - "item1", - "item2", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 1, - IsVirtualized = true, - }; - - target.ApplyTemplate(); - - items.RemoveAt(0); - AssertSingle(target); - } - - [Fact] - public void Should_Do_Nothing_When_Removing_Item_After_SelectedItem() - { - var items = new ObservableCollection - { - "item0", - "item1", - "item2", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 1, - IsVirtualized = true, - }; - - target.ApplyTemplate(); - var child = AssertSingle(target); - items.RemoveAt(2); - Assert.Same(child, AssertSingle(target)); - } - - [Fact] - public void Should_Handle_Removing_SelectedItem_When_Its_Last() - { - var items = new ObservableCollection - { - "item0", - "item1", - "item2", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 2, - IsVirtualized = true, - }; - - target.ApplyTemplate(); - - items.RemoveAt(2); - Assert.Equal(1, target.SelectedIndex); - AssertSingle(target); - } - - [Fact] - public void Should_Handle_Removing_Last_Item() - { - var items = new ObservableCollection - { - "item0", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 0, - IsVirtualized = true, - }; - - target.ApplyTemplate(); - - items.RemoveAt(0); - Assert.Empty(target.Panel.Children); - Assert.Empty(target.ItemContainerGenerator.Containers); - } - - [Fact] - public void Should_Handle_Replacing_SelectedItem() - { - var items = new ObservableCollection - { - "item0", - "item1", - "item2", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 1, - IsVirtualized = true, - }; - - target.ApplyTemplate(); - - items[1] = "replaced"; - AssertSingle(target); - } - - [Fact] - public void Should_Do_Nothing_When_Replacing_Non_SelectedItem() - { - var items = new ObservableCollection - { - "item0", - "item1", - "item2", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 1, - IsVirtualized = true, - }; - - target.ApplyTemplate(); - var child = AssertSingle(target); - items[0] = "replaced"; - Assert.Same(child, AssertSingle(target)); - } - - [Fact] - public void Should_Handle_Moving_SelectedItem() - { - var items = new ObservableCollection - { - "item0", - "item1", - "item2", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 1, - IsVirtualized = true, - }; - - target.ApplyTemplate(); - - items.Move(1, 0); - AssertSingle(target); - } - - private static IControl AssertSingle(CarouselPresenter target) - { - var items = (IList)target.Items; - var index = target.SelectedIndex; - var content = items[index]; - var child = Assert.Single(target.Panel.Children); - var presenter = Assert.IsType(child); - var container = Assert.Single(target.ItemContainerGenerator.Containers); - var visible = Assert.Single(target.Panel.Children.Where(x => x.IsVisible)); - - Assert.Same(child, container.ContainerControl); - Assert.Same(child, visible); - Assert.Equal(content, presenter.Content); - Assert.Equal(content, container.Item); - Assert.Equal(index, container.Index); - - return child; - } - } - - public class NonVirtualized - { - [Fact] - public void Should_Initially_Materialize_All_Containers() - { - var target = new CarouselPresenter - { - Items = new[] { "foo", "bar" }, - IsVirtualized = false, - }; - - target.ApplyTemplate(); - AssertAll(target); - } - - [Fact] - public void Should_Initially_Show_Selected_Item() - { - var target = new CarouselPresenter - { - Items = new[] { "foo", "bar" }, - SelectedIndex = 1, - IsVirtualized = false, - }; - - target.ApplyTemplate(); - AssertAll(target); - } - - [Fact] - public void Switching_To_Non_Virtualized_Should_Reset_Containers() - { - var target = new CarouselPresenter - { - Items = new[] { "foo", "bar" }, - SelectedIndex = 0, - IsVirtualized = true, - }; - - target.ApplyTemplate(); - target.IsVirtualized = false; - - AssertAll(target); - } - - [Fact] - public void Changing_SelectedIndex_Should_Show_Page() - { - var target = new CarouselPresenter - { - Items = new[] { "foo", "bar" }, - SelectedIndex = 0, - IsVirtualized = false, - }; - - target.ApplyTemplate(); - AssertAll(target); - - target.SelectedIndex = 1; - AssertAll(target); - } - - [Fact] - public void Should_Handle_Inserting_Item_At_SelectedItem() - { - var items = new ObservableCollection - { - "item0", - "item1", - "item2", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 1, - IsVirtualized = false, - }; - - target.ApplyTemplate(); - - items.Insert(1, "item1a"); - AssertAll(target); - } - - [Fact] - public void Should_Handle_Inserting_Item_Before_SelectedItem() - { - var items = new ObservableCollection - { - "item0", - "item1", - "item2", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 2, - IsVirtualized = false, - }; - - target.ApplyTemplate(); - - items.Insert(1, "item1a"); - AssertAll(target); - } - - [Fact] - public void Should_Do_Handle_Inserting_Item_After_SelectedItem() - { - var items = new ObservableCollection - { - "item0", - "item1", - "item2", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 1, - IsVirtualized = false, - }; - - target.ApplyTemplate(); - items.Insert(2, "after"); - AssertAll(target); - } - - [Fact] - public void Should_Handle_Removing_Item_At_SelectedItem() - { - var items = new ObservableCollection - { - "item0", - "item1", - "item2", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 1, - IsVirtualized = false, - }; - - target.ApplyTemplate(); - - items.RemoveAt(1); - AssertAll(target); - } - - [Fact] - public void Should_Handle_Removing_Item_Before_SelectedItem() - { - var items = new ObservableCollection - { - "item0", - "item1", - "item2", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 1, - IsVirtualized = false, - }; - - target.ApplyTemplate(); - - items.RemoveAt(0); - AssertAll(target); - } - - [Fact] - public void Should_Handle_Removing_Item_After_SelectedItem() - { - var items = new ObservableCollection - { - "item0", - "item1", - "item2", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 1, - IsVirtualized = false, - }; - - target.ApplyTemplate(); - items.RemoveAt(2); - AssertAll(target); - } - - [Fact] - public void Should_Handle_Removing_SelectedItem_When_Its_Last() - { - var items = new ObservableCollection - { - "item0", - "item1", - "item2", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 2, - IsVirtualized = false, - }; - - target.ApplyTemplate(); - - items.RemoveAt(2); - Assert.Equal(1, target.SelectedIndex); - AssertAll(target); - } - - [Fact] - public void Should_Handle_Removing_Last_Item() - { - var items = new ObservableCollection - { - "item0", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 0, - IsVirtualized = false, - }; - - target.ApplyTemplate(); - - items.RemoveAt(0); - Assert.Empty(target.Panel.Children); - Assert.Empty(target.ItemContainerGenerator.Containers); - } - - [Fact] - public void Should_Handle_Replacing_SelectedItem() - { - var items = new ObservableCollection - { - "item0", - "item1", - "item2", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 1, - IsVirtualized = false, - }; - - target.ApplyTemplate(); - - items[1] = "replaced"; - AssertAll(target); - } - - [Fact] - public void Should_Handle_Moving_SelectedItem() - { - var items = new ObservableCollection - { - "item0", - "item1", - "item2", - }; - - var target = new CarouselPresenter - { - Items = items, - SelectedIndex = 1, - IsVirtualized = false, - }; - - target.ApplyTemplate(); - - items.Move(1, 0); - AssertAll(target); - } - - private static void AssertAll(CarouselPresenter target) - { - var items = (IList)target.Items; - - Assert.Equal(items?.Count ?? 0, target.Panel.Children.Count); - Assert.Equal(items?.Count ?? 0, target.ItemContainerGenerator.Containers.Count()); - - for (var i = 0; i < items?.Count; ++i) - { - var content = items[i]; - var child = target.Panel.Children[i]; - var presenter = Assert.IsType(child); - var container = target.ItemContainerGenerator.ContainerFromIndex(i); - - Assert.Same(child, container); - Assert.Equal(i == target.SelectedIndex, child.IsVisible); - Assert.Equal(content, presenter.Content); - Assert.Equal(i, target.ItemContainerGenerator.IndexFromContainer(container)); - } - } - } - - private class TestItem : ContentControl - { - } - - private class TestItemsControl : ItemsControl - { - protected override IItemContainerGenerator CreateItemContainerGenerator() - { - return new ItemContainerGenerator(this, TestItem.ContentProperty, null); - } - } - } + ////public class CarouselPresenterTests + ////{ + //// [Fact] + //// public void Should_Register_With_Host_When_TemplatedParent_Set() + //// { + //// var host = new Mock(); + //// var target = new CarouselPresenter(); + + //// target.SetValue(Control.TemplatedParentProperty, host.Object); + + //// host.Verify(x => x.RegisterItemsPresenter(target)); + //// } + + //// [Fact] + //// public void ApplyTemplate_Should_Create_Panel() + //// { + //// var target = new CarouselPresenter + //// { + //// ItemsPanel = new FuncTemplate(() => new Panel()), + //// }; + + //// target.ApplyTemplate(); + + //// Assert.IsType(target.Panel); + //// } + + //// [Fact] + //// public void ItemContainerGenerator_Should_Be_Picked_Up_From_TemplatedControl() + //// { + //// var parent = new TestItemsControl(); + //// var target = new CarouselPresenter + //// { + //// [StyledElement.TemplatedParentProperty] = parent, + //// }; + + //// Assert.IsType>(target.ItemContainerGenerator); + //// } + + //// public class Virtualized + //// { + //// [Fact] + //// public void Should_Initially_Materialize_Selected_Container() + //// { + //// var target = new CarouselPresenter + //// { + //// Items = new[] { "foo", "bar" }, + //// SelectedIndex = 0, + //// IsVirtualized = true, + //// }; + + //// target.ApplyTemplate(); + + //// AssertSingle(target); + //// } + + //// [Fact] + //// public void Should_Initially_Materialize_Nothing_If_No_Selected_Container() + //// { + //// var target = new CarouselPresenter + //// { + //// Items = new[] { "foo", "bar" }, + //// IsVirtualized = true, + //// }; + + //// target.ApplyTemplate(); + + //// Assert.Empty(target.Panel.Children); + //// Assert.Empty(target.ItemContainerGenerator.Containers); + //// } + + //// [Fact] + //// public void Switching_To_Virtualized_Should_Reset_Containers() + //// { + //// var target = new CarouselPresenter + //// { + //// Items = new[] { "foo", "bar" }, + //// SelectedIndex = 0, + //// IsVirtualized = false, + //// }; + + //// target.ApplyTemplate(); + //// target.IsVirtualized = true; + + //// AssertSingle(target); + //// } + + //// [Fact] + //// public void Changing_SelectedIndex_Should_Show_Page() + //// { + //// var target = new CarouselPresenter + //// { + //// Items = new[] { "foo", "bar" }, + //// SelectedIndex = 0, + //// IsVirtualized = true, + //// }; + + //// target.ApplyTemplate(); + //// AssertSingle(target); + + //// target.SelectedIndex = 1; + //// AssertSingle(target); + //// } + + //// [Fact] + //// public void Should_Remove_NonCurrent_Page() + //// { + //// var target = new CarouselPresenter + //// { + //// Items = new[] { "foo", "bar" }, + //// IsVirtualized = true, + //// SelectedIndex = 0, + //// }; + + //// target.ApplyTemplate(); + //// AssertSingle(target); + + //// target.SelectedIndex = 1; + //// AssertSingle(target); + + //// } + + //// [Fact] + //// public void Should_Handle_Inserting_Item_At_SelectedItem() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// "item1", + //// "item2", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 1, + //// IsVirtualized = true, + //// }; + + //// target.ApplyTemplate(); + + //// items.Insert(1, "item1a"); + //// AssertSingle(target); + //// } + + //// [Fact] + //// public void Should_Handle_Inserting_Item_Before_SelectedItem() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// "item1", + //// "item2", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 2, + //// IsVirtualized = true, + //// }; + + //// target.ApplyTemplate(); + + //// items.Insert(1, "item1a"); + //// AssertSingle(target); + //// } + + //// [Fact] + //// public void Should_Do_Nothing_When_Inserting_Item_After_SelectedItem() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// "item1", + //// "item2", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 1, + //// IsVirtualized = true, + //// }; + + //// target.ApplyTemplate(); + //// var child = AssertSingle(target); + //// items.Insert(2, "after"); + //// Assert.Same(child, AssertSingle(target)); + //// } + + //// [Fact] + //// public void Should_Handle_Removing_Item_At_SelectedItem() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// "item1", + //// "item2", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 1, + //// IsVirtualized = true, + //// }; + + //// target.ApplyTemplate(); + + //// items.RemoveAt(1); + //// AssertSingle(target); + //// } + + //// [Fact] + //// public void Should_Handle_Removing_Item_Before_SelectedItem() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// "item1", + //// "item2", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 1, + //// IsVirtualized = true, + //// }; + + //// target.ApplyTemplate(); + + //// items.RemoveAt(0); + //// AssertSingle(target); + //// } + + //// [Fact] + //// public void Should_Do_Nothing_When_Removing_Item_After_SelectedItem() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// "item1", + //// "item2", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 1, + //// IsVirtualized = true, + //// }; + + //// target.ApplyTemplate(); + //// var child = AssertSingle(target); + //// items.RemoveAt(2); + //// Assert.Same(child, AssertSingle(target)); + //// } + + //// [Fact] + //// public void Should_Handle_Removing_SelectedItem_When_Its_Last() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// "item1", + //// "item2", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 2, + //// IsVirtualized = true, + //// }; + + //// target.ApplyTemplate(); + + //// items.RemoveAt(2); + //// Assert.Equal(1, target.SelectedIndex); + //// AssertSingle(target); + //// } + + //// [Fact] + //// public void Should_Handle_Removing_Last_Item() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 0, + //// IsVirtualized = true, + //// }; + + //// target.ApplyTemplate(); + + //// items.RemoveAt(0); + //// Assert.Empty(target.Panel.Children); + //// Assert.Empty(target.ItemContainerGenerator.Containers); + //// } + + //// [Fact] + //// public void Should_Handle_Replacing_SelectedItem() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// "item1", + //// "item2", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 1, + //// IsVirtualized = true, + //// }; + + //// target.ApplyTemplate(); + + //// items[1] = "replaced"; + //// AssertSingle(target); + //// } + + //// [Fact] + //// public void Should_Do_Nothing_When_Replacing_Non_SelectedItem() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// "item1", + //// "item2", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 1, + //// IsVirtualized = true, + //// }; + + //// target.ApplyTemplate(); + //// var child = AssertSingle(target); + //// items[0] = "replaced"; + //// Assert.Same(child, AssertSingle(target)); + //// } + + //// [Fact] + //// public void Should_Handle_Moving_SelectedItem() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// "item1", + //// "item2", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 1, + //// IsVirtualized = true, + //// }; + + //// target.ApplyTemplate(); + + //// items.Move(1, 0); + //// AssertSingle(target); + //// } + + //// private static IControl AssertSingle(CarouselPresenter target) + //// { + //// var items = (IList)target.Items; + //// var index = target.SelectedIndex; + //// var content = items[index]; + //// var child = Assert.Single(target.Panel.Children); + //// var presenter = Assert.IsType(child); + //// var container = Assert.Single(target.ItemContainerGenerator.Containers); + //// var visible = Assert.Single(target.Panel.Children.Where(x => x.IsVisible)); + + //// Assert.Same(child, container.ContainerControl); + //// Assert.Same(child, visible); + //// Assert.Equal(content, presenter.Content); + //// Assert.Equal(content, container.Item); + //// Assert.Equal(index, container.Index); + + //// return child; + //// } + //// } + + //// public class NonVirtualized + //// { + //// [Fact] + //// public void Should_Initially_Materialize_All_Containers() + //// { + //// var target = new CarouselPresenter + //// { + //// Items = new[] { "foo", "bar" }, + //// IsVirtualized = false, + //// }; + + //// target.ApplyTemplate(); + //// AssertAll(target); + //// } + + //// [Fact] + //// public void Should_Initially_Show_Selected_Item() + //// { + //// var target = new CarouselPresenter + //// { + //// Items = new[] { "foo", "bar" }, + //// SelectedIndex = 1, + //// IsVirtualized = false, + //// }; + + //// target.ApplyTemplate(); + //// AssertAll(target); + //// } + + //// [Fact] + //// public void Switching_To_Non_Virtualized_Should_Reset_Containers() + //// { + //// var target = new CarouselPresenter + //// { + //// Items = new[] { "foo", "bar" }, + //// SelectedIndex = 0, + //// IsVirtualized = true, + //// }; + + //// target.ApplyTemplate(); + //// target.IsVirtualized = false; + + //// AssertAll(target); + //// } + + //// [Fact] + //// public void Changing_SelectedIndex_Should_Show_Page() + //// { + //// var target = new CarouselPresenter + //// { + //// Items = new[] { "foo", "bar" }, + //// SelectedIndex = 0, + //// IsVirtualized = false, + //// }; + + //// target.ApplyTemplate(); + //// AssertAll(target); + + //// target.SelectedIndex = 1; + //// AssertAll(target); + //// } + + //// [Fact] + //// public void Should_Handle_Inserting_Item_At_SelectedItem() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// "item1", + //// "item2", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 1, + //// IsVirtualized = false, + //// }; + + //// target.ApplyTemplate(); + + //// items.Insert(1, "item1a"); + //// AssertAll(target); + //// } + + //// [Fact] + //// public void Should_Handle_Inserting_Item_Before_SelectedItem() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// "item1", + //// "item2", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 2, + //// IsVirtualized = false, + //// }; + + //// target.ApplyTemplate(); + + //// items.Insert(1, "item1a"); + //// AssertAll(target); + //// } + + //// [Fact] + //// public void Should_Do_Handle_Inserting_Item_After_SelectedItem() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// "item1", + //// "item2", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 1, + //// IsVirtualized = false, + //// }; + + //// target.ApplyTemplate(); + //// items.Insert(2, "after"); + //// AssertAll(target); + //// } + + //// [Fact] + //// public void Should_Handle_Removing_Item_At_SelectedItem() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// "item1", + //// "item2", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 1, + //// IsVirtualized = false, + //// }; + + //// target.ApplyTemplate(); + + //// items.RemoveAt(1); + //// AssertAll(target); + //// } + + //// [Fact] + //// public void Should_Handle_Removing_Item_Before_SelectedItem() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// "item1", + //// "item2", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 1, + //// IsVirtualized = false, + //// }; + + //// target.ApplyTemplate(); + + //// items.RemoveAt(0); + //// AssertAll(target); + //// } + + //// [Fact] + //// public void Should_Handle_Removing_Item_After_SelectedItem() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// "item1", + //// "item2", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 1, + //// IsVirtualized = false, + //// }; + + //// target.ApplyTemplate(); + //// items.RemoveAt(2); + //// AssertAll(target); + //// } + + //// [Fact] + //// public void Should_Handle_Removing_SelectedItem_When_Its_Last() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// "item1", + //// "item2", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 2, + //// IsVirtualized = false, + //// }; + + //// target.ApplyTemplate(); + + //// items.RemoveAt(2); + //// Assert.Equal(1, target.SelectedIndex); + //// AssertAll(target); + //// } + + //// [Fact] + //// public void Should_Handle_Removing_Last_Item() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 0, + //// IsVirtualized = false, + //// }; + + //// target.ApplyTemplate(); + + //// items.RemoveAt(0); + //// Assert.Empty(target.Panel.Children); + //// Assert.Empty(target.ItemContainerGenerator.Containers); + //// } + + //// [Fact] + //// public void Should_Handle_Replacing_SelectedItem() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// "item1", + //// "item2", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 1, + //// IsVirtualized = false, + //// }; + + //// target.ApplyTemplate(); + + //// items[1] = "replaced"; + //// AssertAll(target); + //// } + + //// [Fact] + //// public void Should_Handle_Moving_SelectedItem() + //// { + //// var items = new ObservableCollection + //// { + //// "item0", + //// "item1", + //// "item2", + //// }; + + //// var target = new CarouselPresenter + //// { + //// Items = items, + //// SelectedIndex = 1, + //// IsVirtualized = false, + //// }; + + //// target.ApplyTemplate(); + + //// items.Move(1, 0); + //// AssertAll(target); + //// } + + //// private static void AssertAll(CarouselPresenter target) + //// { + //// var items = (IList)target.Items; + + //// Assert.Equal(items?.Count ?? 0, target.Panel.Children.Count); + //// Assert.Equal(items?.Count ?? 0, target.ItemContainerGenerator.Containers.Count()); + + //// for (var i = 0; i < items?.Count; ++i) + //// { + //// var content = items[i]; + //// var child = target.Panel.Children[i]; + //// var presenter = Assert.IsType(child); + //// var container = target.ItemContainerGenerator.ContainerFromIndex(i); + + //// Assert.Same(child, container); + //// Assert.Equal(i == target.SelectedIndex, child.IsVisible); + //// Assert.Equal(content, presenter.Content); + //// Assert.Equal(i, target.ItemContainerGenerator.IndexFromContainer(container)); + //// } + //// } + //// } + + //// private class TestItem : ContentControl + //// { + //// } + + //// private class TestItemsControl : ItemsControl + //// { + //// protected override IItemContainerGenerator CreateItemContainerGenerator() + //// { + //// return new ItemContainerGenerator(this, TestItem.ContentProperty, null); + //// } + //// } + ////} } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs deleted file mode 100644 index fab57cec494..00000000000 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ /dev/null @@ -1,351 +0,0 @@ -using System.Collections.ObjectModel; -using System.Linq; -using Moq; -using Avalonia.Collections; -using Avalonia.Controls.Generators; -using Avalonia.Controls.Presenters; -using Avalonia.Controls.Templates; -using Avalonia.Input; -using Avalonia.VisualTree; -using Xunit; - -namespace Avalonia.Controls.UnitTests.Presenters -{ - public class ItemsPresenterTests - { - [Fact] - public void Should_Register_With_Host_When_TemplatedParent_Set() - { - var host = new Mock(); - var target = new ItemsPresenter(); - - target.SetValue(Control.TemplatedParentProperty, host.Object); - - host.Verify(x => x.RegisterItemsPresenter(target)); - } - - [Fact] - public void Should_Add_Containers() - { - var target = new ItemsPresenter - { - Items = new[] { "foo", "bar" }, - }; - - target.ApplyTemplate(); - - Assert.Equal(2, target.Panel.Children.Count); - Assert.IsType(target.Panel.Children[0]); - Assert.IsType(target.Panel.Children[1]); - Assert.Equal("foo", ((ContentPresenter)target.Panel.Children[0]).Content); - Assert.Equal("bar", ((ContentPresenter)target.Panel.Children[1]).Content); - } - - [Fact] - public void Should_Add_Containers_Of_Correct_Type() - { - var target = new ItemsPresenter - { - Items = new[] { "foo", "bar" }, - }; - - target.ItemContainerGenerator = new ItemContainerGenerator( - target, - ListBoxItem.ContentProperty, - null); - target.ApplyTemplate(); - - Assert.Equal(2, target.Panel.Children.Count); - Assert.IsType(target.Panel.Children[0]); - Assert.IsType(target.Panel.Children[1]); - } - - [Fact] - public void Should_Create_Containers_Only_Once() - { - var parent = new TestItemsControl(); - var target = new ItemsPresenter - { - Items = new[] { "foo", "bar" }, - [StyledElement.TemplatedParentProperty] = parent, - }; - var raised = 0; - - parent.ItemContainerGenerator.Materialized += (s, e) => ++raised; - - target.ApplyTemplate(); - - Assert.Equal(2, target.Panel.Children.Count); - Assert.Equal(2, raised); - } - - [Fact] - public void ItemContainerGenerator_Should_Be_Picked_Up_From_TemplatedControl() - { - var parent = new TestItemsControl(); - var target = new ItemsPresenter - { - [StyledElement.TemplatedParentProperty] = parent, - }; - - Assert.IsType>(target.ItemContainerGenerator); - } - - [Fact] - public void Should_Remove_Containers() - { - var items = new AvaloniaList(new[] { "foo", "bar" }); - var target = new ItemsPresenter - { - Items = items, - }; - - target.ApplyTemplate(); - items.RemoveAt(0); - - Assert.Single(target.Panel.Children); - Assert.Equal("bar", ((ContentPresenter)target.Panel.Children[0]).Content); - Assert.Equal("bar", ((ContentPresenter)target.ItemContainerGenerator.ContainerFromIndex(0)).Content); - } - - [Fact] - public void Clearing_Items_Should_Remove_Containers() - { - var items = new ObservableCollection { "foo", "bar" }; - var target = new ItemsPresenter - { - Items = items, - }; - - target.ApplyTemplate(); - items.Clear(); - - Assert.Empty(target.Panel.Children); - Assert.Empty(target.ItemContainerGenerator.Containers); - } - - [Fact] - public void Replacing_Items_Should_Update_Containers() - { - var items = new ObservableCollection { "foo", "bar", "baz" }; - var target = new ItemsPresenter - { - Items = items, - }; - - target.ApplyTemplate(); - items[1] = "baz"; - - var text = target.Panel.Children - .OfType() - .Select(x => x.Content) - .ToList(); - - Assert.Equal(new[] { "foo", "baz", "baz" }, text); - } - - [Fact] - public void Moving_Items_Should_Update_Containers() - { - var items = new ObservableCollection { "foo", "bar", "baz" }; - var target = new ItemsPresenter - { - Items = items, - }; - - target.ApplyTemplate(); - items.Move(2, 1); - - var text = target.Panel.Children - .OfType() - .Select(x => x.Content) - .ToList(); - - Assert.Equal(new[] { "foo", "baz", "bar" }, text); - } - - [Fact] - public void Inserting_Items_Should_Update_Containers() - { - var items = new ObservableCollection { "foo", "bar", "baz" }; - var target = new ItemsPresenter - { - Items = items, - }; - - target.ApplyTemplate(); - items.Insert(2, "insert"); - - var text = target.Panel.Children - .OfType() - .Select(x => x.Content) - .ToList(); - - Assert.Equal(new[] { "foo", "bar", "insert", "baz" }, text); - } - - [Fact] - public void Setting_Items_To_Null_Should_Remove_Containers() - { - var target = new ItemsPresenter - { - Items = new[] { "foo", "bar" }, - }; - - target.ApplyTemplate(); - target.Items = null; - - Assert.Empty(target.Panel.Children); - Assert.Empty(target.ItemContainerGenerator.Containers); - } - - [Fact] - public void Should_Handle_Null_Items() - { - var items = new AvaloniaList(new[] { "foo", null, "bar" }); - - var target = new ItemsPresenter - { - Items = items, - }; - - target.ApplyTemplate(); - - var text = target.Panel.Children.Cast().Select(x => x.Content).ToList(); - - Assert.Equal(new[] { "foo", null, "bar" }, text); - Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(0)); - Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(1)); - Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(2)); - - items.RemoveAt(1); - - text = target.Panel.Children.Cast().Select(x => x.Content).ToList(); - - Assert.Equal(new[] { "foo", "bar" }, text); - Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(0)); - Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(1)); - } - - [Fact] - public void Inserting_Then_Removing_Should_Add_Remove_Containers() - { - var items = new AvaloniaList(Enumerable.Range(0, 5).Select(x => $"Item {x}")); - var toAdd = Enumerable.Range(0, 3).Select(x => $"Added Item {x}").ToArray(); - var target = new ItemsPresenter - { - VirtualizationMode = ItemVirtualizationMode.None, - Items = items, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), - }; - - target.ApplyTemplate(); - - Assert.Equal(items.Count, target.Panel.Children.Count); - - foreach (var item in toAdd) - { - items.Insert(1, item); - } - - Assert.Equal(items.Count, target.Panel.Children.Count); - - foreach (var item in toAdd) - { - items.Remove(item); - } - - Assert.Equal(items.Count, target.Panel.Children.Count); - } - - [Fact] - public void Should_Handle_Duplicate_Items() - { - var items = new AvaloniaList(new[] { 1, 2, 1 }); - - var target = new ItemsPresenter - { - Items = items, - }; - - target.ApplyTemplate(); - items.RemoveAt(2); - - var numbers = target.Panel.Children - .OfType() - .Select(x => x.Content) - .Cast(); - Assert.Equal(new[] { 1, 2 }, numbers); - } - - [Fact] - public void Panel_Should_Be_Created_From_ItemsPanel_Template() - { - var panel = new Panel(); - var target = new ItemsPresenter - { - ItemsPanel = new FuncTemplate(() => panel), - }; - - target.ApplyTemplate(); - - Assert.Same(panel, target.Panel); - Assert.Same(target, target.Panel.Parent); - } - - [Fact] - public void Panel_TabNavigation_Should_Be_Set_To_Once() - { - var target = new ItemsPresenter(); - - target.ApplyTemplate(); - - Assert.Equal(KeyboardNavigationMode.Once, KeyboardNavigation.GetTabNavigation((InputElement)target.Panel)); - } - - [Fact] - public void Panel_TabNavigation_Should_Be_Set_To_ItemsPresenter_Value() - { - var target = new ItemsPresenter(); - - KeyboardNavigation.SetTabNavigation(target, KeyboardNavigationMode.Cycle); - target.ApplyTemplate(); - - Assert.Equal(KeyboardNavigationMode.Cycle, KeyboardNavigation.GetTabNavigation((InputElement)target.Panel)); - } - - [Fact] - public void Panel_Should_Be_Visual_Child() - { - var target = new ItemsPresenter(); - - target.ApplyTemplate(); - - var child = target.GetVisualChildren().Single(); - - Assert.Equal(target.Panel, child); - } - - private class Item - { - public Item(string value) - { - Value = value; - } - - public string Value { get; } - } - - private class TestItem : ContentControl - { - } - - private class TestItemsControl : ItemsControl - { - protected override IItemContainerGenerator CreateItemContainerGenerator() - { - return new ItemContainerGenerator(this, TestItem.ContentProperty, null); - } - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs deleted file mode 100644 index d529cc4f75a..00000000000 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ /dev/null @@ -1,374 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls.Generators; -using Avalonia.Controls.Presenters; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; -using Avalonia.Layout; -using Avalonia.Platform; -using Avalonia.Rendering; -using Avalonia.UnitTests; -using Avalonia.VisualTree; -using Xunit; - -namespace Avalonia.Controls.UnitTests.Presenters -{ - public class ItemsPresenterTests_Virtualization - { - [Fact] - public void Should_Not_Create_Items_Before_Added_To_Visual_Tree() - { - var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList(); - var target = new TestItemsPresenter(true) - { - Items = items, - ItemsPanel = VirtualizingPanelTemplate(Orientation.Vertical), - ItemTemplate = ItemTemplate(), - VirtualizationMode = ItemVirtualizationMode.Simple, - }; - - var scroller = new ScrollContentPresenter - { - Content = target, - }; - - scroller.UpdateChild(); - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - Assert.Empty(target.Panel.Children); - - var root = new TestRoot - { - Child = scroller, - }; - - target.InvalidateMeasure(); - target.Panel.InvalidateMeasure(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - Assert.Equal(10, target.Panel.Children.Count); - } - - [Fact] - public void Should_Return_IsLogicalScrollEnabled_False_When_Has_No_Virtualizing_Panel() - { - var target = CreateTarget(); - target.ClearValue(ItemsPresenter.ItemsPanelProperty); - - target.ApplyTemplate(); - - Assert.False(((ILogicalScrollable)target).IsLogicalScrollEnabled); - } - - [Fact] - public void Should_Return_IsLogicalScrollEnabled_False_When_VirtualizationMode_None() - { - var target = CreateTarget(ItemVirtualizationMode.None); - - target.ApplyTemplate(); - - Assert.False(((ILogicalScrollable)target).IsLogicalScrollEnabled); - } - - [Fact] - public void Should_Return_IsLogicalScrollEnabled_False_When_Doesnt_Have_ScrollPresenter_Parent() - { - var target = new ItemsPresenter - { - ItemsPanel = VirtualizingPanelTemplate(), - ItemTemplate = ItemTemplate(), - VirtualizationMode = ItemVirtualizationMode.Simple, - }; - - target.ApplyTemplate(); - - Assert.False(((ILogicalScrollable)target).IsLogicalScrollEnabled); - } - - [Fact] - public void Should_Return_IsLogicalScrollEnabled_True() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - - Assert.True(((ILogicalScrollable)target).IsLogicalScrollEnabled); - } - - [Fact] - public void Parent_ScrollContentPresenter_Properties_Should_Be_Set() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - var scroll = (ScrollContentPresenter)target.Parent; - Assert.Equal(new Size(10, 20), scroll.Extent); - Assert.Equal(new Size(100, 10), scroll.Viewport); - } - - [Fact] - public void Should_Fill_Panel_With_Containers() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - - target.Measure(new Size(100, 100)); - Assert.Equal(10, target.Panel.Children.Count); - - target.Arrange(new Rect(0, 0, 100, 100)); - Assert.Equal(10, target.Panel.Children.Count); - } - - [Fact] - public void Should_Only_Create_Enough_Containers_To_Display_All_Items() - { - var target = CreateTarget(itemCount: 2); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - Assert.Equal(2, target.Panel.Children.Count); - } - - [Fact] - public void Should_Expand_To_Fit_Containers_When_Flexible_Size() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - target.Measure(Size.Infinity); - target.Arrange(new Rect(target.DesiredSize)); - - Assert.Equal(new Size(10, 200), target.DesiredSize); - Assert.Equal(new Size(10, 200), target.Bounds.Size); - Assert.Equal(20, target.Panel.Children.Count); - } - - [Fact] - public void Initial_Item_DataContexts_Should_Be_Correct() - { - var target = CreateTarget(); - var items = (IList)target.Items; - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - for (var i = 0; i < target.Panel.Children.Count; ++i) - { - Assert.Equal(items[i], target.Panel.Children[i].DataContext); - } - } - - [Fact] - public void Should_Add_New_Items_When_Control_Is_Enlarged() - { - var target = CreateTarget(); - var items = (IList)target.Items; - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - Assert.Equal(10, target.Panel.Children.Count); - - target.Measure(new Size(120, 120)); - target.Arrange(new Rect(0, 0, 100, 120)); - - Assert.Equal(12, target.Panel.Children.Count); - - for (var i = 0; i < target.Panel.Children.Count; ++i) - { - Assert.Equal(items[i], target.Panel.Children[i].DataContext); - } - } - - [Fact] - public void Should_Not_Create_Virtualizer_Before_Panel() - { - var target = CreateTarget(); - - Assert.Null(target.Panel); - Assert.Null(target.Virtualizer); - } - - [Fact] - public void Changing_VirtualizationMode_None_To_Simple_Should_Update_Control() - { - var target = CreateTarget(mode: ItemVirtualizationMode.None); - var scroll = (ScrollContentPresenter)target.Parent; - - scroll.Measure(new Size(100, 100)); - scroll.Arrange(new Rect(0, 0, 100, 100)); - - Assert.Equal(20, target.Panel.Children.Count); - Assert.Equal(new Size(100, 200), scroll.Extent); - Assert.Equal(new Size(100, 100), scroll.Viewport); - - target.VirtualizationMode = ItemVirtualizationMode.Simple; - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - Assert.Equal(10, target.Panel.Children.Count); - Assert.Equal(new Size(10, 20), scroll.Extent); - Assert.Equal(new Size(100, 10), scroll.Viewport); - } - - [Fact] - public void Changing_VirtualizationMode_None_To_Simple_Should_Add_Correct_Number_Of_Controls() - { - using (UnitTestApplication.Start(new TestServices())) - { - var target = CreateTarget(mode: ItemVirtualizationMode.None); - var scroll = (TestScroller)target.Parent; - - scroll.Width = scroll.Height = 100; - scroll.LayoutManager.ExecuteInitialLayoutPass(); - - // Ensure than an intermediate measure pass doesn't add more controls than it - // should. This can happen if target gets measured with Size.Infinity which - // is what the available size should be when VirtualizationMode == None but not - // what it should after VirtualizationMode is changed to Simple. - target.Panel.Children.CollectionChanged += (s, e) => - { - Assert.InRange(target.Panel.Children.Count, 0, 10); - }; - - target.VirtualizationMode = ItemVirtualizationMode.Simple; - ((ILayoutRoot)scroll.GetVisualRoot()).LayoutManager.ExecuteLayoutPass(); - - Assert.Equal(10, target.Panel.Children.Count); - } - } - - [Fact] - public void Changing_VirtualizationMode_Simple_To_None_Should_Update_Control() - { - var target = CreateTarget(); - var scroll = (ScrollContentPresenter)target.Parent; - - scroll.Measure(new Size(100, 100)); - scroll.Arrange(new Rect(0, 0, 100, 100)); - - Assert.Equal(10, target.Panel.Children.Count); - Assert.Equal(new Size(10, 20), scroll.Extent); - Assert.Equal(new Size(100, 10), scroll.Viewport); - - target.VirtualizationMode = ItemVirtualizationMode.None; - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - // Here - unlike changing the other way - we need to do a layout pass on the scroll - // content presenter as non-logical scroll values are only updated on arrange. - scroll.Measure(new Size(100, 100)); - scroll.Arrange(new Rect(0, 0, 100, 100)); - - Assert.Equal(20, target.Panel.Children.Count); - Assert.Equal(new Size(100, 200), scroll.Extent); - Assert.Equal(new Size(100, 100), scroll.Viewport); - } - - private static ItemsPresenter CreateTarget( - ItemVirtualizationMode mode = ItemVirtualizationMode.Simple, - Orientation orientation = Orientation.Vertical, - bool useContainers = true, - int itemCount = 20) - { - ItemsPresenter result; - var items = Enumerable.Range(0, itemCount).Select(x => $"Item {x}").ToList(); - - var scroller = new TestScroller - { - CanHorizontallyScroll = false, - CanVerticallyScroll = true, - Content = result = new TestItemsPresenter(useContainers) - { - Items = items, - ItemsPanel = VirtualizingPanelTemplate(orientation), - ItemTemplate = ItemTemplate(), - VirtualizationMode = mode, - } - }; - - scroller.UpdateChild(); - - return result; - } - - private static IDataTemplate ItemTemplate() - { - return new FuncDataTemplate((x, _) => new Canvas - { - Width = 10, - Height = 10, - }); - } - - private static ITemplate VirtualizingPanelTemplate( - Orientation orientation = Orientation.Vertical) - { - return new FuncTemplate(() => new VirtualizingStackPanel - { - Orientation = orientation, - }); - } - - private class TestScroller : ScrollContentPresenter, IRenderRoot, ILayoutRoot - { - public TestScroller() - { - LayoutManager = new LayoutManager(this); - } - - public IRenderer Renderer { get; } - public Size ClientSize { get; } - public double RenderScaling => 1; - - public Size MaxClientSize => Size.Infinity; - - public double LayoutScaling => 1; - - public ILayoutManager LayoutManager { get; } - - public IRenderTarget CreateRenderTarget() => throw new NotImplementedException(); - public void Invalidate(Rect rect) => throw new NotImplementedException(); - public Point PointToClient(PixelPoint p) => throw new NotImplementedException(); - public PixelPoint PointToScreen(Point p) => throw new NotImplementedException(); - } - - private class TestItemsPresenter : ItemsPresenter - { - private bool _useContainers; - - public TestItemsPresenter(bool useContainers) - { - _useContainers = useContainers; - } - - protected override IItemContainerGenerator CreateItemContainerGenerator() - { - return _useContainers ? - new ItemContainerGenerator(this, TestContainer.ContentProperty, null) : - new ItemContainerGenerator(this); - } - } - - private class TestContainer : ContentControl - { - public TestContainer() - { - Width = 10; - Height = 10; - } - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs deleted file mode 100644 index a467c6dd03a..00000000000 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ /dev/null @@ -1,1110 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using Avalonia.Collections; -using Avalonia.Controls.Generators; -using Avalonia.Controls.Presenters; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; -using Avalonia.Input; -using Avalonia.Layout; -using Avalonia.LogicalTree; -using Avalonia.Platform; -using Avalonia.Rendering; -using Avalonia.Styling; -using Avalonia.UnitTests; -using Xunit; - -namespace Avalonia.Controls.UnitTests.Presenters -{ - public class ItemsPresenterTests_Virtualization_Simple - { - [Fact] - public void Should_Return_Items_Count_For_Extent_Vertical() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - - Assert.Equal(new Size(0, 20), ((ILogicalScrollable)target).Extent); - } - - [Fact] - public void Should_Return_Items_Count_For_Extent_Horizontal() - { - var target = CreateTarget(orientation: Orientation.Horizontal); - - target.ApplyTemplate(); - - Assert.Equal(new Size(20, 0), ((ILogicalScrollable)target).Extent); - } - - [Fact] - public void Should_Have_Number_Of_Visible_Items_As_Viewport_Vertical() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - Assert.Equal(new Size(100, 10), ((ILogicalScrollable)target).Viewport); - } - - [Fact] - public void Should_Have_Number_Of_Visible_Items_As_Viewport_Horizontal() - { - var target = CreateTarget(orientation: Orientation.Horizontal); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - Assert.Equal(new Size(10, 100), ((ILogicalScrollable)target).Viewport); - } - - [Fact] - public void Should_Add_Containers_When_Panel_Is_Not_Full() - { - var target = CreateTarget(itemCount: 5); - var items = (IList)target.Items; - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - Assert.Equal(5, target.Panel.Children.Count); - - items.Add("New Item"); - - Assert.Equal(6, target.Panel.Children.Count); - } - - [Fact] - public void Should_Remove_Items_When_Control_Is_Shrank() - { - var target = CreateTarget(); - var items = (IList)target.Items; - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - Assert.Equal(10, target.Panel.Children.Count); - - target.Measure(new Size(100, 80)); - target.Arrange(new Rect(0, 0, 100, 80)); - - Assert.Equal(8, target.Panel.Children.Count); - } - - [Fact] - public void Should_Add_New_Containers_At_Top_When_Control_Is_Scrolled_To_Bottom_And_Enlarged() - { - var target = CreateTarget(); - var items = (IList)target.Items; - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - Assert.Equal(10, target.Panel.Children.Count); - - ((IScrollable)target).Offset = new Vector(0, 10); - target.Measure(new Size(120, 120)); - target.Arrange(new Rect(0, 0, 100, 120)); - - Assert.Equal(12, target.Panel.Children.Count); - - for (var i = 0; i < target.Panel.Children.Count; ++i) - { - Assert.Equal(items[i + 8], target.Panel.Children[i].DataContext); - } - } - - [Fact] - public void Should_Update_Containers_When_Items_Changes() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - target.Items = new[] { "foo", "bar", "baz" }; - - Assert.Equal(3, target.Panel.Children.Count); - } - - [Fact] - public void Should_Decrease_The_Viewport_Size_By_One_If_There_Is_A_Partial_Item() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - target.Measure(new Size(100, 95)); - target.Arrange(new Rect(0, 0, 100, 95)); - - Assert.Equal(new Size(100, 9), ((ILogicalScrollable)target).Viewport); - } - - [Fact] - public void Moving_To_And_From_The_End_With_Partial_Item_Should_Set_Panel_PixelOffset() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - target.Measure(new Size(100, 95)); - target.Arrange(new Rect(0, 0, 100, 95)); - - ((ILogicalScrollable)target).Offset = new Vector(0, 11); - - var minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); - Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset); - Assert.Equal(10, minIndex); - Assert.Equal(10, ((IVirtualizingPanel)target.Panel).PixelOffset); - - ((ILogicalScrollable)target).Offset = new Vector(0, 10); - - minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); - Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); - Assert.Equal(10, minIndex); - Assert.Equal(0, ((IVirtualizingPanel)target.Panel).PixelOffset); - - ((ILogicalScrollable)target).Offset = new Vector(0, 11); - - minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); - Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset); - Assert.Equal(10, minIndex); - Assert.Equal(10, ((IVirtualizingPanel)target.Panel).PixelOffset); - } - - [Fact] - public void Inserting_Items_Should_Update_Containers() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - ((ILogicalScrollable)target).Offset = new Vector(0, 5); - - var expected = Enumerable.Range(5, 10).Select(x => $"Item {x}").ToList(); - var items = (ObservableCollection)target.Items; - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - - Assert.Equal(expected, actual); - - items.Insert(6, "Inserted"); - expected.Insert(1, "Inserted"); - expected.RemoveAt(expected.Count - 1); - - actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(expected, actual); - } - - [Fact] - public void Inserting_Items_Before_Visibile_Containers_Should_Update_Containers() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - ((ILogicalScrollable)target).Offset = new Vector(0, 5); - - var expected = Enumerable.Range(5, 10).Select(x => $"Item {x}").ToList(); - var items = (ObservableCollection)target.Items; - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - - Assert.Equal(expected, actual); - - items.Insert(0, "Inserted"); - - expected = Enumerable.Range(4, 10).Select(x => $"Item {x}").ToList(); - actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(expected, actual); - } - - [Fact] - public void Removing_First_Materialized_Item_Should_Update_Containers() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList(); - var items = (ObservableCollection)target.Items; - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - - Assert.Equal(expected, actual); - - items.RemoveAt(0); - expected = Enumerable.Range(1, 10).Select(x => $"Item {x}").ToList(); - - actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(expected, actual); - } - - [Fact] - public void Removing_Items_From_Middle_Should_Update_Containers_When_All_Items_Visible() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - target.Measure(new Size(100, 200)); - target.Arrange(new Rect(0, 0, 100, 200)); - - var items = (ObservableCollection)target.Items; - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - - Assert.Equal(items, actual); - - items.RemoveAt(2); - - actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(items, actual); - - items.RemoveAt(items.Count - 2); - - actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(items, actual); - } - - [Fact] - public void Removing_Last_Item_Should_Update_Containers_When_All_Items_Visible() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - target.Measure(new Size(100, 200)); - target.Arrange(new Rect(0, 0, 100, 200)); - - ((ILogicalScrollable)target).Offset = new Vector(0, 5); - - var expected = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(); - var items = (ObservableCollection)target.Items; - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - - Assert.Equal(expected, actual); - - items.Remove(items.Last()); - expected.Remove(expected.Last()); - - actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(expected, actual); - } - - [Fact] - public void Removing_Items_When_Scrolled_To_End_Should_Recyle_Containers_At_Top() - { - var target = CreateTarget(useAvaloniaList: true); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - ((ILogicalScrollable)target).Offset = new Vector(0, 10); - - var expected = Enumerable.Range(10, 10).Select(x => $"Item {x}").ToList(); - var items = (AvaloniaList)target.Items; - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - - Assert.Equal(expected, actual); - - items.RemoveRange(18, 2); - expected = Enumerable.Range(8, 10).Select(x => $"Item {x}").ToList(); - - actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(expected, actual); - } - - [Fact] - public void Removing_Items_When_Scrolled_To_Near_End_Should_Recycle_Containers_At_Bottom_And_Top() - { - var target = CreateTarget(useAvaloniaList: true); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - ((ILogicalScrollable)target).Offset = new Vector(0, 9); - - var expected = Enumerable.Range(9, 10).Select(x => $"Item {x}").ToList(); - var items = (AvaloniaList)target.Items; - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - - Assert.Equal(expected, actual); - - items.RemoveRange(15, 3); - expected = Enumerable.Range(7, 8).Select(x => $"Item {x}") - .Concat(Enumerable.Range(18, 2).Select(x => $"Item {x}")) - .ToList(); - - actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(expected, actual); - } - - [Fact] - public void Measuring_To_Infinity_When_Scrolled_To_End_Should_Not_Throw() - { - var target = CreateTarget(useAvaloniaList: true); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - ((ILogicalScrollable)target).Offset = new Vector(0, 10); - - // Check for issue #589: this should not throw. - target.Measure(Size.Infinity); - - var expected = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(); - var items = (AvaloniaList)target.Items; - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - - Assert.Equal(expected, actual); - } - - [Fact] - public void Replacing_Items_Should_Update_Containers() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList(); - var items = (ObservableCollection)target.Items; - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - - Assert.Equal(expected, actual); - - items[4] = expected[4] = "Replaced"; - - actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(expected, actual); - } - - [Fact] - public void Moving_Items_Should_Update_Containers() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList(); - var items = (ObservableCollection)target.Items; - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - - Assert.Equal(expected, actual); - - items.Move(4, 8); - var i = expected[4]; - expected.RemoveAt(4); - expected.Insert(8, i); - - actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(expected, actual); - } - - [Fact] - public void Setting_Items_To_Null_Should_Remove_Containers() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList(); - var items = (ObservableCollection)target.Items; - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - - Assert.Equal(expected, actual); - - target.Items = null; - - Assert.Empty(target.Panel.Children); - } - - [Fact] - public void Reassigning_Items_Should_Create_Containers() - { - var target = CreateTarget(itemCount: 5); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - var expected = Enumerable.Range(0, 5).Select(x => $"Item {x}").ToList(); - var items = (ObservableCollection)target.Items; - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - - Assert.Equal(expected, actual); - - expected = Enumerable.Range(0, 6).Select(x => $"Item {x}").ToList(); - target.Items = expected; - - actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(expected, actual); - } - - [Fact] - public void Inserting_Then_Removing_Should_Add_Remove_Containers() - { - var items = new AvaloniaList(Enumerable.Range(0, 5).Select(x => $"Item {x}")); - var toAdd = Enumerable.Range(0, 3).Select(x => $"Added Item {x}").ToArray(); - var target = new ItemsPresenter - { - VirtualizationMode = ItemVirtualizationMode.None, - Items = items, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), - }; - - target.ApplyTemplate(); - - Assert.Equal(items.Count, target.Panel.Children.Count); - - int addIndex = 1; - foreach (var item in toAdd) - { - items.Insert(addIndex++, item); - } - - Assert.Equal(items.Count, target.Panel.Children.Count); - - foreach (var item in toAdd) - { - items.Remove(item); - } - - Assert.Equal(items.Count, target.Panel.Children.Count); - } - - [Fact] - public void Reassigning_Items_Should_Remove_Containers() - { - var target = CreateTarget(itemCount: 6); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - var expected = Enumerable.Range(0, 6).Select(x => $"Item {x}").ToList(); - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - - Assert.Equal(expected, actual); - - expected = Enumerable.Range(0, 5).Select(x => $"Item {x}").ToList(); - target.Items = expected; - - actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(expected, actual); - } - - [Fact] - public void Clearing_Items_And_ReAdding_Should_Remove_Containers() - { - var target = CreateTarget(itemCount: 6); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - var expected = Enumerable.Range(0, 6).Select(x => $"Item {x}").ToList(); - var items = (ObservableCollection)target.Items; - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - - Assert.Equal(expected, actual); - - expected = Enumerable.Range(0, 5).Select(x => $"Item {x}").ToList(); - target.Items = null; - target.Items = expected; - - actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(expected, actual); - } - - [Fact] - public void Scrolling_To_Partial_Last_Item_Then_Adding_Item_Updates_Containers() - { - var target = CreateTarget(itemCount: 10); - var items = (IList)target.Items; - - target.ApplyTemplate(); - target.Measure(new Size(100, 95)); - target.Arrange(new Rect(0, 0, 100, 95)); - - ((ILogicalScrollable)target).Offset = new Vector(0, 1); - Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); - - var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList(); - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(expected, actual); - Assert.Equal(10, ((IVirtualizingPanel)target.Panel).PixelOffset); - - items.Add("Item 10"); - - expected = Enumerable.Range(1, 10).Select(x => $"Item {x}").ToList(); - actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(expected, actual); - Assert.Equal(0, ((IVirtualizingPanel)target.Panel).PixelOffset); - } - - [Fact] - public void Scrolling_To_Item_In_Zero_Sized_Presenter_Doesnt_Throw() - { - using (UnitTestApplication.Start(new TestServices())) - { - var target = CreateTarget(itemCount: 10); - var items = (IList)target.Items; - target.ApplyTemplate(); - target.Measure(Size.Empty); - target.Arrange(Rect.Empty); - - // Check for issue #591: this should not throw. - target.ScrollIntoView(0); - } - } - - [Fact] - public void InsertRange_Items_Should_Update_Containers() - { - var target = CreateTarget(useAvaloniaList: true); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList(); - var items = (AvaloniaList)target.Items; - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - - Assert.Equal(expected, actual); - - var toAdd = Enumerable.Range(0, 3).Select(x => $"New Item {x}").ToList(); - - int index = 1; - - items.InsertRange(index, toAdd); - expected.InsertRange(index, toAdd); - expected.RemoveRange(10, toAdd.Count); - - actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(expected, actual); - } - - [Fact] - public void InsertRange_Items_Before_Last_Should_Update_Containers() - { - var target = CreateTarget(useAvaloniaList: true); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList(); - var items = (AvaloniaList)target.Items; - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - - Assert.Equal(expected, actual); - - var toAdd = Enumerable.Range(0, 3).Select(x => $"New Item {x}").ToList(); - - int index = 8; - - items.InsertRange(index, toAdd); - expected.InsertRange(index, toAdd); - expected.RemoveRange(10, toAdd.Count); - - actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(expected, actual); - } - - [Fact] - public void RemoveRange_Items_Should_Update_Containers() - { - var target = CreateTarget(useAvaloniaList: true); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - var expected = Enumerable.Range(0, 13).Select(x => $"Item {x}").ToList(); - var items = (AvaloniaList)target.Items; - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - - Assert.Equal(expected.Take(10), actual); - - int index = 5; - int count = 3; - - items.RemoveRange(index, count); - expected.RemoveRange(index, count); - - actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(expected, actual); - } - - [Fact] - public void RemoveRange_Items_Before_Last_Should_Update_Containers() - { - var target = CreateTarget(useAvaloniaList: true); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - var expected = Enumerable.Range(0, 13).Select(x => $"Item {x}").ToList(); - var items = (AvaloniaList)target.Items; - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - - Assert.Equal(expected.Take(10), actual); - - int index = 8; - int count = 3; - - items.RemoveRange(index, count); - expected.RemoveRange(index, count); - - actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(expected, actual); - } - - [Fact] - public void Should_Add_Containers_For_Items_After_Clear() - { - var target = CreateTarget(itemCount: 10); - var defaultItems = (IList)target.Items; - var items = new AvaloniaList(defaultItems); - target.Items = items; - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(target.DesiredSize)); - - Assert.Equal(10, target.Panel.Children.Count); - - items.Clear(); - - target.Panel.Measure(new Size(100, 100)); - target.Panel.Arrange(new Rect(target.Panel.DesiredSize)); - - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(target.DesiredSize)); - - Assert.Empty(target.Panel.Children); - - items.AddRange(defaultItems.Select(s => s + " new")); - - target.Panel.Measure(new Size(100, 100)); - target.Panel.Arrange(new Rect(target.Panel.DesiredSize)); - - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(target.DesiredSize)); - - Assert.Equal(10, target.Panel.Children.Count); - } - - [Fact] - public void Scroll_To_Last_Should_Work() - { - var target = CreateTarget(itemCount: 11); - var scroller = (TestScroller)target.Parent; - - scroller.Width = scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(); - - var last = (target.Items as IList)[10]; - - target.ScrollIntoView(10); - - Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[9].DataContext, last); - } - - [Fact] - public void Second_Scroll_To_Last_Should_Work() - { - var target = CreateTarget(itemCount: 11); - var scroller = (TestScroller)target.Parent; - - scroller.Width = scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(); - - var last = (target.Items as IList)[10]; - - target.ScrollIntoView(10); - - Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[9].DataContext, last); - - target.ScrollIntoView(10); - - Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[9].DataContext, last); - } - - [Fact] - public void Scrolling_Less_Than_A_Page_Should_Move_Recycled_Items() - { - var target = CreateTarget(); - var items = (IList)target.Items; - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - var containers = target.Panel.Children.ToList(); - var scroller = (ScrollContentPresenter)target.Parent; - - scroller.Offset = new Vector(0, 5); - - var scrolledContainers = containers - .Skip(5) - .Take(5) - .Concat(containers.Take(5)).ToList(); - - Assert.Equal(new Vector(0, 5), ((ILogicalScrollable)target).Offset); - Assert.Equal(scrolledContainers, target.Panel.Children); - - for (var i = 0; i < target.Panel.Children.Count; ++i) - { - Assert.Equal(items[i + 5], target.Panel.Children[i].DataContext); - } - - scroller.Offset = new Vector(0, 0); - Assert.Equal(new Vector(0, 0), ((ILogicalScrollable)target).Offset); - Assert.Equal(containers, target.Panel.Children); - - var dcs = target.Panel.Children.Select(x => x.DataContext).ToList(); - - for (var i = 0; i < target.Panel.Children.Count; ++i) - { - Assert.Equal(items[i], target.Panel.Children[i].DataContext); - } - } - - [Fact] - public void Scrolling_More_Than_A_Page_Should_Recycle_Items() - { - var target = CreateTarget(itemCount: 50); - var items = (IList)target.Items; - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - var containers = target.Panel.Children.ToList(); - var scroller = (ScrollContentPresenter)target.Parent; - - scroller.Offset = new Vector(0, 20); - - Assert.Equal(new Vector(0, 20), ((ILogicalScrollable)target).Offset); - Assert.Equal(containers, target.Panel.Children); - - for (var i = 0; i < target.Panel.Children.Count; ++i) - { - Assert.Equal(items[i + 20], target.Panel.Children[i].DataContext); - } - - scroller.Offset = new Vector(0, 0); - - Assert.Equal(new Vector(0, 0), ((ILogicalScrollable)target).Offset); - Assert.Equal(containers, target.Panel.Children); - - for (var i = 0; i < target.Panel.Children.Count; ++i) - { - Assert.Equal(items[i], target.Panel.Children[i].DataContext); - } - } - - public class Vertical - { - [Fact] - public void GetControlInDirection_Down_Should_Return_Existing_Container_If_Materialized() - { - var target = CreateTarget(); - var scroller = (TestScroller)target.Parent; - - scroller.Width = scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(); - - var from = target.Panel.Children[5]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Down, - from); - - Assert.Same(target.Panel.Children[6], result); - } - - [Fact] - public void GetControlInDirection_Down_Should_Scroll_If_Necessary() - { - var target = CreateTarget(); - var scroller = (TestScroller)target.Parent; - - scroller.Width = scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(); - - var from = target.Panel.Children[9]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Down, - from); - - Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[9], result); - } - - [Fact] - public void GetControlInDirection_Down_Should_Scroll_If_Partially_Visible() - { - var target = CreateTarget(); - var scroller = (TestScroller)target.Parent; - - scroller.Width = 100; - scroller.Height = 95; - scroller.LayoutManager.ExecuteInitialLayoutPass(); - - var from = target.Panel.Children[8]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Down, - from); - - Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[8], result); - } - - [Fact] - public void GetControlInDirection_Up_Should_Scroll_If_Partially_Visible_Item_Is_Currently_Shown() - { - var target = CreateTarget(); - var scroller = (TestScroller)target.Parent; - - scroller.Width = 100; - scroller.Height = 95; - scroller.LayoutManager.ExecuteInitialLayoutPass(); - ((ILogicalScrollable)target).Offset = new Vector(0, 11); - - var from = target.Panel.Children[1]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Up, - from); - - Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[0], result); - } - - [Fact] - public void Should_Return_Horizontal_Extent_And_Viewport() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - target.Measure(new Size(5, 100)); - target.Arrange(new Rect(0, 0, 5, 100)); - - Assert.Equal(new Size(10, 20), ((ILogicalScrollable)target).Extent); - Assert.Equal(new Size(5, 10), ((ILogicalScrollable)target).Viewport); - } - - [Fact] - public void Horizontal_Scroll_Should_Update_Item_Position() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - - target.Measure(new Size(5, 100)); - target.Arrange(new Rect(0, 0, 5, 100)); - - ((ILogicalScrollable)target).Offset = new Vector(5, 0); - - target.Measure(new Size(5, 100)); - target.Arrange(new Rect(0, 0, 5, 100)); - - Assert.Equal(new Rect(-5, 0, 10, 10), target.Panel.Children[0].Bounds); - } - } - - public class Horizontal - { - [Fact] - public void GetControlInDirection_Right_Should_Return_Existing_Container_If_Materialized() - { - var target = CreateTarget(orientation: Orientation.Horizontal); - var scroller = (TestScroller)target.Parent; - - scroller.Width = scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(); - - var from = target.Panel.Children[5]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Right, - from); - - Assert.Same(target.Panel.Children[6], result); - } - - [Fact] - public void GetControlInDirection_Right_Should_Scroll_If_Necessary() - { - var target = CreateTarget(orientation: Orientation.Horizontal); - var scroller = (TestScroller)target.Parent; - - scroller.Width = scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(); - - var from = target.Panel.Children[9]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Right, - from); - - Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[9], result); - } - - [Fact] - public void GetControlInDirection_Right_Should_Scroll_If_Partially_Visible() - { - var target = CreateTarget(orientation: Orientation.Horizontal); - var scroller = (TestScroller)target.Parent; - - scroller.Width = 95; - scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(); - - var from = target.Panel.Children[8]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Right, - from); - - Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[8], result); - } - - [Fact] - public void GetControlInDirection_Left_Should_Scroll_If_Partially_Visible_Item_Is_Currently_Shown() - { - var target = CreateTarget(orientation: Orientation.Horizontal); - var scroller = (TestScroller)target.Parent; - - scroller.Width = 95; - scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(); - ((ILogicalScrollable)target).Offset = new Vector(11, 0); - - var from = target.Panel.Children[1]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Left, - from); - - Assert.Equal(new Vector(10, 0), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[0], result); - } - } - - private static ItemsPresenter CreateTarget( - Orientation orientation = Orientation.Vertical, - int itemCount = 20, - bool useAvaloniaList = false) - { - ItemsPresenter result; - var itemsSource = Enumerable.Range(0, itemCount).Select(x => $"Item {x}"); - var items = useAvaloniaList ? - (IEnumerable)new AvaloniaList(itemsSource) : - (IEnumerable)new ObservableCollection(itemsSource); - - var scroller = new TestScroller - { - CanHorizontallyScroll = true, - CanVerticallyScroll = true, - Content = result = new TestItemsPresenter - { - Items = items, - ItemsPanel = VirtualizingPanelTemplate(orientation), - DataTemplates = { StringDataTemplate() }, - VirtualizationMode = ItemVirtualizationMode.Simple, - } - }; - - scroller.UpdateChild(); - return result; - } - - private static IDataTemplate StringDataTemplate() - { - return new FuncDataTemplate((x, _) => new Canvas - { - Width = 10, - Height = 10, - }); - } - - private static ITemplate VirtualizingPanelTemplate( - Orientation orientation = Orientation.Vertical) - { - return new FuncTemplate(() => new VirtualizingStackPanel - { - Orientation = orientation, - }); - } - - private class TestScroller : ScrollContentPresenter, IRenderRoot, ILayoutRoot, ILogicalRoot - { - public TestScroller() - { - LayoutManager = new LayoutManager(this); - } - - public IRenderer Renderer { get; } - public Size ClientSize { get; } - public double RenderScaling => 1; - - public Size MaxClientSize => Size.Infinity; - - public double LayoutScaling => 1; - - public ILayoutManager LayoutManager { get; } - - public IRenderTarget CreateRenderTarget() => throw new NotImplementedException(); - public void Invalidate(Rect rect) => throw new NotImplementedException(); - public Point PointToClient(PixelPoint p) => throw new NotImplementedException(); - public PixelPoint PointToScreen(Point p) => throw new NotImplementedException(); - } - - private class TestItemsPresenter : ItemsPresenter - { - protected override IItemContainerGenerator CreateItemContainerGenerator() - { - return new ItemContainerGenerator( - this, - TestContainer.ContentProperty, - null); - } - } - - private class TestContainer : ContentControl - { - public TestContainer() - { - Template = new FuncControlTemplate((parent, scope) => new ContentPresenter - { - Name = "PART_ContentPresenter", - [~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty], - [~ContentPresenter.ContentTemplateProperty] = parent[~ContentControl.ContentTemplateProperty], - }.RegisterInNameScope(scope)); - } - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 00d148093ac..cc615e01f46 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -12,9 +13,11 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.Markup.Data; using Avalonia.Styling; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Moq; using Xunit; @@ -27,6 +30,8 @@ public partial class SelectingItemsControlTests [Fact] public void SelectedIndex_Should_Initially_Be_Minus_1() { + using var app = Start(); + var items = new[] { new Item(), @@ -36,15 +41,18 @@ public void SelectedIndex_Should_Initially_Be_Minus_1() var target = new SelectingItemsControl { Items = items, - Template = Template(), }; + Prepare(target); + Assert.Equal(-1, target.SelectedIndex); } [Fact] public void Item_IsSelected_Should_Initially_Be_False() { + using var app = Start(); + var items = new[] { new Item(), @@ -54,7 +62,6 @@ public void Item_IsSelected_Should_Initially_Be_False() var target = new SelectingItemsControl { Items = items, - Template = Template(), }; Prepare(target); @@ -66,6 +73,8 @@ public void Item_IsSelected_Should_Initially_Be_False() [Fact] public void Setting_SelectedItem_Should_Set_Item_IsSelected_True() { + using var app = Start(); + var items = new[] { new Item(), @@ -75,7 +84,6 @@ public void Setting_SelectedItem_Should_Set_Item_IsSelected_True() var target = new SelectingItemsControl { Items = items, - Template = Template(), }; Prepare(target); @@ -89,6 +97,8 @@ public void Setting_SelectedItem_Should_Set_Item_IsSelected_True() [Fact] public void Setting_SelectedItem_Before_ApplyTemplate_Should_Set_Item_IsSelected_True() { + using var app = Start(); + var items = new[] { new Item(), @@ -98,7 +108,6 @@ public void Setting_SelectedItem_Before_ApplyTemplate_Should_Set_Item_IsSelected var target = new SelectingItemsControl { Items = items, - Template = Template(), }; target.SelectedItem = items[1]; @@ -246,6 +255,7 @@ public void Setting_SelectedItem_Before_Initialize_Should_Retain_Selection() [Fact] + public void Setting_SelectedItems_Before_Initialize_Should_Retain_Selection() { var listBox = new ListBox @@ -299,7 +309,6 @@ public void Setting_SelectedIndex_Before_Initialize_With_AlwaysSelected_Should_R var listBox = new ListBox { SelectionMode = SelectionMode.Single | SelectionMode.AlwaysSelected, - Items = new[] { "foo", "bar", "baz" }, SelectedIndex = 1 }; @@ -315,6 +324,8 @@ public void Setting_SelectedIndex_Before_Initialize_With_AlwaysSelected_Should_R [Fact] public void Setting_SelectedIndex_Before_ApplyTemplate_Should_Set_Item_IsSelected_True() { + using var app = Start(); + var items = new[] { new Item(), @@ -324,7 +335,6 @@ public void Setting_SelectedIndex_Before_ApplyTemplate_Should_Set_Item_IsSelecte var target = new SelectingItemsControl { Items = items, - Template = Template(), }; target.SelectedIndex = 1; @@ -337,6 +347,8 @@ public void Setting_SelectedIndex_Before_ApplyTemplate_Should_Set_Item_IsSelecte [Fact] public void Setting_SelectedItem_Should_Set_SelectedIndex() { + using var app = Start(); + var items = new[] { new Item(), @@ -346,10 +358,9 @@ public void Setting_SelectedItem_Should_Set_SelectedIndex() var target = new SelectingItemsControl { Items = items, - Template = Template(), }; - target.ApplyTemplate(); + Prepare(target); target.SelectedItem = items[1]; Assert.Equal(items[1], target.SelectedItem); @@ -359,6 +370,8 @@ public void Setting_SelectedItem_Should_Set_SelectedIndex() [Fact] public void SelectedIndex_Item_Is_Updated_As_Items_Removed_When_Last_Item_Is_Selected() { + using var app = Start(); + var items = new ObservableCollection { "Foo", @@ -369,10 +382,9 @@ public void SelectedIndex_Item_Is_Updated_As_Items_Removed_When_Last_Item_Is_Sel var target = new SelectingItemsControl { Items = items, - Template = Template(), }; - target.ApplyTemplate(); + Prepare(target); target.SelectedItem = items[2]; Assert.Equal(items[2], target.SelectedItem); @@ -387,6 +399,8 @@ public void SelectedIndex_Item_Is_Updated_As_Items_Removed_When_Last_Item_Is_Sel [Fact] public void Setting_SelectedItem_To_Not_Present_Item_Should_Clear_Selection() { + using var app = Start(); + var items = new[] { new Item(), @@ -396,10 +410,9 @@ public void Setting_SelectedItem_To_Not_Present_Item_Should_Clear_Selection() var target = new SelectingItemsControl { Items = items, - Template = Template(), }; - target.ApplyTemplate(); + Prepare(target); target.SelectedItem = items[1]; Assert.Equal(items[1], target.SelectedItem); @@ -414,6 +427,8 @@ public void Setting_SelectedItem_To_Not_Present_Item_Should_Clear_Selection() [Fact] public void Setting_SelectedIndex_Should_Set_SelectedItem() { + using var app = Start(); + var items = new[] { new Item(), @@ -423,10 +438,9 @@ public void Setting_SelectedIndex_Should_Set_SelectedItem() var target = new SelectingItemsControl { Items = items, - Template = Template(), }; - target.ApplyTemplate(); + Prepare(target); target.SelectedIndex = 1; Assert.Equal(items[1], target.SelectedItem); @@ -435,6 +449,8 @@ public void Setting_SelectedIndex_Should_Set_SelectedItem() [Fact] public void Setting_SelectedIndex_Out_Of_Bounds_Should_Clear_Selection() { + using var app = Start(); + var items = new[] { new Item(), @@ -444,10 +460,9 @@ public void Setting_SelectedIndex_Out_Of_Bounds_Should_Clear_Selection() var target = new SelectingItemsControl { Items = items, - Template = Template(), }; - target.ApplyTemplate(); + Prepare(target); target.SelectedIndex = 2; Assert.Equal(-1, target.SelectedIndex); @@ -456,12 +471,11 @@ public void Setting_SelectedIndex_Out_Of_Bounds_Should_Clear_Selection() [Fact] public void Setting_SelectedItem_To_Non_Existent_Item_Should_Clear_Selection() { - var target = new SelectingItemsControl - { - Template = Template(), - }; + using var app = Start(); - target.ApplyTemplate(); + var target = new SelectingItemsControl(); + + Prepare(target); target.SelectedItem = new Item(); Assert.Equal(-1, target.SelectedIndex); @@ -471,6 +485,8 @@ public void Setting_SelectedItem_To_Non_Existent_Item_Should_Clear_Selection() [Fact] public void Adding_Selected_Item_Should_Update_Selection() { + using var app = Start(); + var items = new AvaloniaList(new[] { new Item(), @@ -502,7 +518,6 @@ public void Setting_Items_To_Null_Should_Clear_Selection() var target = new SelectingItemsControl { Items = items, - Template = Template(), }; target.ApplyTemplate(); @@ -529,7 +544,6 @@ public void Removing_Selected_Item_Should_Clear_Selection() var target = new SelectingItemsControl { Items = items, - Template = Template(), }; Prepare(target); @@ -605,7 +619,6 @@ public void Resetting_Items_Collection_Should_Clear_Selection() var target = new SelectingItemsControl { Items = items, - Template = Template(), }; target.ApplyTemplate(); @@ -623,6 +636,8 @@ public void Resetting_Items_Collection_Should_Clear_Selection() [Fact] public void Raising_IsSelectedChanged_On_Item_Should_Update_Selection() { + using var app = Start(); + var items = new[] { new Item(), @@ -632,7 +647,6 @@ public void Raising_IsSelectedChanged_On_Item_Should_Update_Selection() var target = new SelectingItemsControl { Items = items, - Template = Template(), }; Prepare(target); @@ -653,6 +667,8 @@ public void Raising_IsSelectedChanged_On_Item_Should_Update_Selection() [Fact] public void Clearing_IsSelected_And_Raising_IsSelectedChanged_On_Item_Should_Update_Selection() { + using var app = Start(); + var items = new[] { new Item(), @@ -662,7 +678,6 @@ public void Clearing_IsSelected_And_Raising_IsSelectedChanged_On_Item_Should_Upd var target = new SelectingItemsControl { Items = items, - Template = Template(), }; Prepare(target); @@ -681,6 +696,8 @@ public void Clearing_IsSelected_And_Raising_IsSelectedChanged_On_Item_Should_Upd [Fact] public void Raising_IsSelectedChanged_On_Someone_Elses_Item_Should_Not_Update_Selection() { + using var app = Start(); + var items = new[] { new Item(), @@ -690,10 +707,9 @@ public void Raising_IsSelectedChanged_On_Someone_Elses_Item_Should_Not_Update_Se var target = new SelectingItemsControl { Items = items, - Template = Template(), }; - target.ApplyTemplate(); + Prepare(target); target.SelectedItem = items[1]; var notChild = new Item @@ -722,7 +738,6 @@ public void Setting_SelectedIndex_Should_Raise_SelectionChanged_Event() var target = new SelectingItemsControl { Items = items, - Template = Template(), }; var called = false; @@ -742,6 +757,8 @@ public void Setting_SelectedIndex_Should_Raise_SelectionChanged_Event() [Fact] public void Clearing_SelectedIndex_Should_Raise_SelectionChanged_Event() { + using var app = Start(); + var items = new[] { new Item(), @@ -751,7 +768,6 @@ public void Clearing_SelectedIndex_Should_Raise_SelectionChanged_Event() var target = new SelectingItemsControl { Items = items, - Template = Template(), SelectedIndex = 1, }; @@ -858,11 +874,12 @@ public void Changing_DataContext_Should_Not_Clear_Nested_ViewModel_SelectedItem( [Fact] public void Nested_ListBox_Does_Not_Change_Parent_SelectedIndex() { + using var app = Start(); + SelectingItemsControl nested; var root = new SelectingItemsControl { - Template = Template(), Items = new IControl[] { new Border(), @@ -876,10 +893,7 @@ public void Nested_ListBox_Does_Not_Change_Parent_SelectedIndex() SelectedIndex = 0, }; - root.ApplyTemplate(); - root.Presenter.ApplyTemplate(); - nested.ApplyTemplate(); - nested.Presenter.ApplyTemplate(); + Prepare(root); Assert.Equal(0, root.SelectedIndex); Assert.Equal(1, nested.SelectedIndex); @@ -892,20 +906,23 @@ public void Nested_ListBox_Does_Not_Change_Parent_SelectedIndex() [Fact] public void Setting_SelectedItem_With_Pointer_Should_Set_TabOnceActiveElement() { + using var app = Start(); + var target = new ListBox { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz " }, }; Prepare(target); - _helper.Down((Interactive)target.Presenter.Panel.Children[1]); - var panel = target.Presenter.Panel; + var selected = target.Presenter.RealizedElements.ElementAt(1); + _helper.Down(selected); + + var presenter = (InputElement)target.Presenter; Assert.Equal( - KeyboardNavigation.GetTabOnceActiveElement((InputElement)panel), - panel.Children[1]); + KeyboardNavigation.GetTabOnceActiveElement(presenter), + selected); } [Fact] @@ -921,13 +938,14 @@ public void Removing_SelectedItem_Should_Clear_TabOnceActiveElement() Prepare(target); - _helper.Down(target.Presenter.Panel.Children[1]); + var selected = target.Presenter.RealizedElements.ElementAt(1); + _helper.Down(selected); items.RemoveAt(1); - var panel = target.Presenter.Panel; + var presenter = (InputElement)target.Presenter; - Assert.Null(KeyboardNavigation.GetTabOnceActiveElement((InputElement)panel)); + Assert.Null(KeyboardNavigation.GetTabOnceActiveElement(presenter)); } [Fact] @@ -1007,14 +1025,15 @@ public void Mode_For_SelectedIndex_Is_TwoWay_By_Default() [Fact] public void Should_Select_Correct_Item_When_Duplicate_Items_Are_Present() { + using var app = Start(); + var target = new ListBox { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, }; Prepare(target); - _helper.Down((Interactive)target.Presenter.Panel.Children[3]); + _helper.Down((Interactive)target.Presenter.RealizedElements.ElementAt(3)); Assert.Equal(3, target.SelectedIndex); } @@ -1022,21 +1041,26 @@ public void Should_Select_Correct_Item_When_Duplicate_Items_Are_Present() [Fact] public void Should_Apply_Selected_Pseudoclass_To_Correct_Item_When_Duplicate_Items_Are_Present() { + using var app = Start(); + var target = new ListBox { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, }; Prepare(target); - _helper.Down((Interactive)target.Presenter.Panel.Children[3]); - Assert.Equal(new[] { ":pressed", ":selected" }, target.Presenter.Panel.Children[3].Classes); + var selected = target.Presenter.RealizedElements.ElementAt(3); + _helper.Down(selected); + + Assert.Equal(new[] { ":pressed", ":selected" }, selected.Classes); } [Fact] public void Adding_Item_Before_SelectedItem_Should_Update_SelectedIndex() { + using var app = Start(); + var items = new ObservableCollection { "Foo", @@ -1046,7 +1070,6 @@ public void Adding_Item_Before_SelectedItem_Should_Update_SelectedIndex() var target = new ListBox { - Template = Template(), Items = items, SelectedIndex = 1, }; @@ -1062,6 +1085,8 @@ public void Adding_Item_Before_SelectedItem_Should_Update_SelectedIndex() [Fact] public void Removing_Item_Before_SelectedItem_Should_Update_SelectedIndex() { + using var app = Start(); + var items = new ObservableCollection { "Foo", @@ -1071,7 +1096,6 @@ public void Removing_Item_Before_SelectedItem_Should_Update_SelectedIndex() var target = new ListBox { - Template = Template(), Items = items, SelectedIndex = 1, }; @@ -1146,6 +1170,8 @@ public void Binding_SelectedItem_Selects_Correct_Item() [Fact] public void Replacing_Selected_Item_Should_Update_SelectedItem() { + using var app = Start(); + var items = new ObservableCollection { "Foo", @@ -1155,7 +1181,6 @@ public void Replacing_Selected_Item_Should_Update_SelectedItem() var target = new ListBox { - Template = Template(), Items = items, SelectedIndex = 1, }; @@ -1171,6 +1196,8 @@ public void Replacing_Selected_Item_Should_Update_SelectedItem() [Fact] public void AutoScrollToSelectedItem_Causes_Scroll_To_SelectedItem() { + using var app = Start(); + var items = new ObservableCollection { "Foo", @@ -1180,7 +1207,6 @@ public void AutoScrollToSelectedItem_Causes_Scroll_To_SelectedItem() var target = new ListBox { - Template = Template(), Items = items, }; @@ -1197,40 +1223,37 @@ public void AutoScrollToSelectedItem_Causes_Scroll_To_SelectedItem() [Fact] public void AutoScrollToSelectedItem_On_Reset_Works() { + using var app = Start(); + // Issue #3148 - using (UnitTestApplication.Start(TestServices.StyledWindow)) - { - var items = new ResettingCollection(100); + var items = new ResettingCollection(100); - var target = new ListBox - { - Items = items, - ItemTemplate = new FuncDataTemplate((x, _) => - new TextBlock - { - Text = x, - Width = 100, - Height = 10 - }), - AutoScrollToSelectedItem = true, - VirtualizationMode = ItemVirtualizationMode.Simple, - }; + var target = new ListBox + { + Items = items, + ItemTemplate = new FuncDataTemplate((x, _) => + new TextBlock + { + Text = x, + Width = 100, + Height = 10 + }), + AutoScrollToSelectedItem = true, + Layout = new NonVirtualizingStackLayout(), + }; - var root = new TestRoot(true, target); - root.Measure(new Size(100, 100)); - root.Arrange(new Rect(0, 0, 100, 100)); + Prepare(target); - Assert.True(target.Presenter.Panel.Children.Count > 0); - Assert.True(target.Presenter.Panel.Children.Count < 100); + Assert.Equal(100, target.Presenter.RealizedElements.Count()); - target.SelectedItem = "Item99"; + target.SelectedItem = "Item99"; - // #3148 triggered here. - items.Reset(new[] { "Item99" }); + // #3148 triggered here. + items.Reset(new[] { "Item99" }); + Layout(target); - Assert.Equal(0, target.SelectedIndex); - Assert.Equal(1, target.Presenter.Panel.Children.Count); - } + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(1, target.Presenter.RealizedElements.Count()); } [Fact] @@ -1376,6 +1399,14 @@ public void Does_The_Best_It_Can_With_AutoSelecting_ViewModel() Assert.Equal(new[] { "foo" }, target.SelectedItems); } + private static IDisposable Start() + { + var services = TestServices.MockPlatformRenderInterface.With( + styler: new Styler(), + windowingPlatform: new MockWindowingPlatform()); + return UnitTestApplication.Start(services); + } + private static void Prepare(SelectingItemsControl target) { var root = new TestRoot @@ -1398,18 +1429,57 @@ private static void Prepare(SelectingItemsControl target) root.LayoutManager.ExecuteInitialLayoutPass(); } + private void Layout(SelectingItemsControl target) + { + var root = (TestRoot)target.GetVisualRoot(); + root.LayoutManager.ExecuteLayoutPass(); + } + private static FuncControlTemplate Template() { - return new FuncControlTemplate((control, scope) => - new ItemsPresenter + return new FuncControlTemplate((parent, scope) => + new ScrollViewer { - Name = "itemsPresenter", - [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], - [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], - [~ItemsPresenter.VirtualizationModeProperty] = control[~ListBox.VirtualizationModeProperty], + Name = "PART_ScrollViewer", + Template = ScrollViewerTemplate(), + Content = new ItemsPresenter + { + Name = "PART_ItemsPresenter", + HorizontalCacheLength = 0, + VerticalCacheLength = 0, + [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(), + [~ItemsPresenter.LayoutProperty] = parent.GetObservable(ItemsControl.LayoutProperty).ToBinding(), + }.RegisterInNameScope(scope) }.RegisterInNameScope(scope)); } + private static FuncControlTemplate ScrollViewerTemplate() + { + return new FuncControlTemplate((parent, scope) => + new Panel + { + Children = + { + new ScrollContentPresenter + { + Name = "PART_ContentPresenter", + [~ScrollContentPresenter.CanHorizontallyScrollProperty] = parent.GetObservable(ScrollViewer.CanHorizontallyScrollProperty).ToBinding(), + [~ScrollContentPresenter.CanVerticallyScrollProperty] = parent.GetObservable(ScrollViewer.CanVerticallyScrollProperty).ToBinding(), + [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(), + [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty], + [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty], + [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty], + }.RegisterInNameScope(scope), + new ScrollBar + { + Name = "verticalScrollBar", + [~ScrollBar.MaximumProperty] = parent[~ScrollViewer.VerticalScrollBarMaximumProperty], + [~~ScrollBar.ValueProperty] = parent[~~ScrollViewer.VerticalScrollBarValueProperty], + } + } + }); + } + private class Item : Control, ISelectable { public string Value { get; set; } @@ -1468,9 +1538,13 @@ private class RootWithItems : TestRoot private class TestSelector : SelectingItemsControl { + public static readonly new AvaloniaProperty SelectedItemsProperty = + SelectingItemsControl.SelectedItemsProperty; + public static readonly new DirectProperty SelectionProperty = + SelectingItemsControl.SelectionProperty; + public TestSelector() { - } public TestSelector(SelectionMode selectionMode) @@ -1484,6 +1558,12 @@ public TestSelector(SelectionMode selectionMode) set => base.Selection = value; } + public new IList SelectedItems + { + get => base.SelectedItems; + set => base.SelectedItems = value; + } + public new SelectionMode SelectionMode { get => base.SelectionMode; @@ -1494,6 +1574,9 @@ public TestSelector(SelectionMode selectionMode) { return base.MoveSelection(direction, wrap); } + + public void SelectRange(int index) => UpdateSelection(index, true, true); + public void Toggle(int index) => UpdateSelection(index, true, false, true); } private class ResettingCollection : List, INotifyCollectionChanged diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs index 7b7e651cc9e..6143ec573bb 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs @@ -1,26 +1,21 @@ 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 Xunit; namespace Avalonia.Controls.UnitTests.Primitives { - public class SelectingItemsControlTests_AutoSelect + public partial class SelectingItemsControlTests { [Fact] public void First_Item_Should_Be_Selected() { - var target = new TestSelector + var target = new TestSelector(SelectionMode.AlwaysSelected) { Items = new[] { "foo", "bar" }, - Template = Template(), }; - target.ApplyTemplate(); - Assert.Equal(0, target.SelectedIndex); Assert.Equal("foo", target.SelectedItem); } @@ -29,13 +24,11 @@ public void First_Item_Should_Be_Selected() public void First_Item_Should_Be_Selected_When_Added() { var items = new AvaloniaList(); - var target = new TestSelector + var target = new TestSelector(SelectionMode.AlwaysSelected) { Items = items, - Template = Template(), }; - target.ApplyTemplate(); items.Add("foo"); Assert.Equal(0, target.SelectedIndex); @@ -47,13 +40,11 @@ public void First_Item_Should_Be_Selected_When_Added() public void First_Item_Should_Be_Selected_When_Reset() { var items = new ResetOnAdd(); - var target = new TestSelector + var target = new TestSelector(SelectionMode.AlwaysSelected) { Items = items, - Template = Template(), }; - target.ApplyTemplate(); items.Add("foo"); Assert.Equal(0, target.SelectedIndex); @@ -64,14 +55,11 @@ public void First_Item_Should_Be_Selected_When_Reset() public void Item_Should_Be_Selected_When_Selection_Removed() { var items = new AvaloniaList(new[] { "foo", "bar", "baz", "qux" }); - - var target = new TestSelector + var target = new TestSelector(SelectionMode.AlwaysSelected) { Items = items, - Template = Template(), }; - target.ApplyTemplate(); target.SelectedIndex = 2; items.RemoveAt(2); @@ -83,14 +71,11 @@ public void Item_Should_Be_Selected_When_Selection_Removed() public void Selection_Should_Be_Cleared_When_No_Items_Left() { var items = new AvaloniaList(new[] { "foo", "bar" }); - - var target = new TestSelector + var target = new TestSelector(SelectionMode.AlwaysSelected) { Items = items, - Template = Template(), }; - target.ApplyTemplate(); target.SelectedIndex = 1; items.RemoveAt(1); items.RemoveAt(0); @@ -102,39 +87,21 @@ public void Selection_Should_Be_Cleared_When_No_Items_Left() [Fact] public void Removing_Selected_First_Item_Should_Select_Next_Item() { + using var app = Start(); + var items = new AvaloniaList(new[] { "foo", "bar" }); - var target = new TestSelector + var target = new TestSelector(SelectionMode.AlwaysSelected) { Items = items, - Template = Template(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); items.RemoveAt(0); + Layout(target); Assert.Equal(0, target.SelectedIndex); Assert.Equal("bar", target.SelectedItem); - Assert.Equal(new[] { ":selected" }, target.Presenter.Panel.Children[0].Classes); - } - - private FuncControlTemplate Template() - { - return new FuncControlTemplate((control, scope) => - new ItemsPresenter - { - Name = "itemsPresenter", - [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], - [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], - }.RegisterInNameScope(scope)); - } - - private class TestSelector : SelectingItemsControl - { - static TestSelector() - { - SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); - } + Assert.Equal(new[] { ":selected" }, target.Presenter.RealizedElements.First().Classes); } private class ResetOnAdd : List, INotifyCollectionChanged diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 6b26d763711..2a37c553172 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1,35 +1,28 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Avalonia.Collections; -using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Selection; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests.Primitives { - public class SelectingItemsControlTests_Multiple + public partial class SelectingItemsControlTests { - private MouseTestHelper _helper = new MouseTestHelper(); - [Fact] public void Setting_SelectedIndex_Should_Add_To_SelectedItems() { - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { "foo", "bar" }, - Template = Template(), }; - target.ApplyTemplate(); target.SelectedIndex = 1; Assert.Equal(new[] { "bar" }, target.SelectedItems.Cast().ToList()); @@ -38,13 +31,11 @@ public void Setting_SelectedIndex_Should_Add_To_SelectedItems() [Fact] public void Adding_SelectedItems_Should_Set_SelectedIndex() { - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { "foo", "bar" }, - Template = Template(), }; - target.ApplyTemplate(); target.SelectedItems.Add("bar"); Assert.Equal(1, target.SelectedIndex); @@ -53,14 +44,14 @@ public void Adding_SelectedItems_Should_Set_SelectedIndex() [Fact] public void Assigning_Single_SelectedItems_Should_Set_SelectedIndex() { - var target = new TestSelector + using var app = Start(); + + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { "foo", "bar" }, - Template = Template(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); target.SelectedItems = new AvaloniaList("bar"); Assert.Equal(1, target.SelectedIndex); @@ -71,34 +62,15 @@ public void Assigning_Single_SelectedItems_Should_Set_SelectedIndex() [Fact] public void Assigning_Multiple_SelectedItems_Should_Set_SelectedIndex() { - var target = new TestSelector - { - Items = new[] { "foo", "bar", "baz" }, - Template = Template(), - }; + using var app = Start(); - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - target.SelectedItems = new AvaloniaList("foo", "bar", "baz"); - - Assert.Equal(0, target.SelectedIndex); - Assert.Equal(new[] { "foo", "bar", "baz" }, target.SelectedItems); - Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); - } - - [Fact] - public void Selected_Items_Should_Be_Marked_When_Panel_Created_After_SelectedItems_Is_Set() - { - // Issue #2565. - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { "foo", "bar", "baz" }, - Template = Template(), }; - target.ApplyTemplate(); + Prepare(target); target.SelectedItems = new AvaloniaList("foo", "bar", "baz"); - target.Presenter.ApplyTemplate(); Assert.Equal(0, target.SelectedIndex); Assert.Equal(new[] { "foo", "bar", "baz" }, target.SelectedItems); @@ -108,13 +80,11 @@ public void Selected_Items_Should_Be_Marked_When_Panel_Created_After_SelectedIte [Fact] public void Reassigning_SelectedItems_Should_Clear_Selection() { - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { "foo", "bar" }, - Template = Template(), }; - target.ApplyTemplate(); target.SelectedItems.Add("bar"); target.SelectedItems = new AvaloniaList(); @@ -125,10 +95,9 @@ public void Reassigning_SelectedItems_Should_Clear_Selection() [Fact] public void Adding_First_SelectedItem_Should_Raise_SelectedIndex_SelectedItem_Changed() { - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { "foo", "bar" }, - Template = Template(), }; bool indexRaised = false; @@ -143,7 +112,6 @@ public void Adding_First_SelectedItem_Should_Raise_SelectedIndex_SelectedItem_Ch (string)e.NewValue == "bar"; }; - target.ApplyTemplate(); target.SelectedItems.Add("bar"); Assert.True(indexRaised); @@ -153,13 +121,11 @@ public void Adding_First_SelectedItem_Should_Raise_SelectedIndex_SelectedItem_Ch [Fact] public void Adding_Subsequent_SelectedItems_Should_Not_Raise_SelectedIndex_SelectedItem_Changed() { - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { "foo", "bar" }, - Template = Template(), }; - target.ApplyTemplate(); target.SelectedItems.Add("foo"); bool raised = false; @@ -178,10 +144,8 @@ public void Removing_Last_SelectedItem_Should_Raise_SelectedIndex_Changed() var target = new TestSelector { Items = new[] { "foo", "bar" }, - Template = Template(), }; - target.ApplyTemplate(); target.SelectedItems.Add("foo"); bool raised = false; @@ -198,6 +162,8 @@ public void Removing_Last_SelectedItem_Should_Raise_SelectedIndex_Changed() [Fact] public void Adding_SelectedItems_Should_Set_Item_IsSelected() { + using var app = Start(); + var items = new[] { new ListBoxItem(), @@ -205,18 +171,16 @@ public void Adding_SelectedItems_Should_Set_Item_IsSelected() new ListBoxItem(), }; - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = items, - Template = Template(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); target.SelectedItems.Add(items[0]); target.SelectedItems.Add(items[1]); - var foo = target.Presenter.Panel.Children[0]; + var foo = target.Presenter.RealizedElements.First(); Assert.True(items[0].IsSelected); Assert.True(items[1].IsSelected); @@ -226,6 +190,8 @@ public void Adding_SelectedItems_Should_Set_Item_IsSelected() [Fact] public void Assigning_SelectedItems_Should_Set_Item_IsSelected() { + using var app = Start(); + var items = new[] { new ListBoxItem(), @@ -233,14 +199,12 @@ public void Assigning_SelectedItems_Should_Set_Item_IsSelected() new ListBoxItem(), }; - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = items, - Template = Template(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); target.SelectedItems = new AvaloniaList { items[0], items[1] }; Assert.True(items[0].IsSelected); @@ -251,6 +215,8 @@ public void Assigning_SelectedItems_Should_Set_Item_IsSelected() [Fact] public void Removing_SelectedItems_Should_Clear_Item_IsSelected() { + using var app = Start(); + var items = new[] { new ListBoxItem(), @@ -258,14 +224,12 @@ public void Removing_SelectedItems_Should_Clear_Item_IsSelected() new ListBoxItem(), }; - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = items, - Template = Template(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); target.SelectedItems.Add(items[0]); target.SelectedItems.Add(items[1]); target.SelectedItems.Remove(items[1]); @@ -284,13 +248,11 @@ public void Reassigning_SelectedItems_Should_Clear_Item_IsSelected() new ListBoxItem(), }; - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = items, - Template = Template(), }; - target.ApplyTemplate(); target.SelectedItems.Add(items[0]); target.SelectedItems.Add(items[1]); @@ -303,14 +265,14 @@ public void Reassigning_SelectedItems_Should_Clear_Item_IsSelected() [Fact] public void Setting_SelectedIndex_Should_Unmark_Previously_Selected_Containers() { - var target = new TestSelector + using var app = Start(); + + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { "foo", "bar", "baz" }, - Template = Template(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); target.SelectedItems.Add("foo"); target.SelectedItems.Add("bar"); @@ -325,7 +287,7 @@ public void Setting_SelectedIndex_Should_Unmark_Previously_Selected_Containers() [Fact] public void Range_Select_Should_Select_Range() { - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { @@ -336,10 +298,8 @@ public void Range_Select_Should_Select_Range() "qiz", "lol", }, - Template = Template(), }; - target.ApplyTemplate(); target.SelectedIndex = 1; target.SelectRange(3); @@ -349,7 +309,7 @@ public void Range_Select_Should_Select_Range() [Fact] public void Range_Select_Backwards_Should_Select_Range() { - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { @@ -361,10 +321,8 @@ public void Range_Select_Backwards_Should_Select_Range() "lol", }, SelectionMode = SelectionMode.Multiple, - Template = Template(), }; - target.ApplyTemplate(); target.SelectedIndex = 3; target.SelectRange(1); @@ -374,7 +332,7 @@ public void Range_Select_Backwards_Should_Select_Range() [Fact] public void Second_Range_Select_Backwards_Should_Select_From_Original_Selection() { - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { @@ -386,10 +344,8 @@ public void Second_Range_Select_Backwards_Should_Select_From_Original_Selection( "lol", }, SelectionMode = SelectionMode.Multiple, - Template = Template(), }; - target.ApplyTemplate(); target.SelectedIndex = 2; target.SelectRange(5); target.SelectRange(4); @@ -400,16 +356,16 @@ public void Second_Range_Select_Backwards_Should_Select_From_Original_Selection( [Fact] public void Setting_SelectedIndex_After_Range_Should_Unmark_Previously_Selected_Containers() { - var target = new TestSelector + using var app = Start(); + + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { "foo", "bar", "baz", "qux" }, - Template = Template(), SelectedIndex = 0, SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); target.SelectRange(2); @@ -423,16 +379,16 @@ public void Setting_SelectedIndex_After_Range_Should_Unmark_Previously_Selected_ [Fact] public void Toggling_Selection_After_Range_Should_Work() { - var target = new TestSelector + using var app = Start(); + + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { "foo", "bar", "baz", "foo", "bar", "baz" }, - Template = Template(), SelectedIndex = 0, SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); target.SelectRange(3); @@ -446,14 +402,11 @@ public void Toggling_Selection_After_Range_Should_Work() [Fact] public void Suprious_SelectedIndex_Changes_Should_Not_Be_Triggered() { - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { "foo", "bar", "baz" }, - Template = Template(), }; - target.ApplyTemplate(); - var selectedIndexes = new List(); target.GetObservable(TestSelector.SelectedIndexProperty).Subscribe(x => selectedIndexes.Add(x)); @@ -467,14 +420,14 @@ public void Suprious_SelectedIndex_Changes_Should_Not_Be_Triggered() [Fact] public void Can_Set_SelectedIndex_To_Another_Selected_Item() { - var target = new TestSelector + using var app = Start(); + + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { "foo", "bar", "baz" }, - Template = Template(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); target.SelectedItems.Add("foo"); target.SelectedItems.Add("bar"); @@ -520,7 +473,7 @@ public void Can_Set_SelectedIndex_To_Another_Selected_Item() public void Should_Not_Write_SelectedItems_To_Old_DataContext() { var vm = new OldDataContextViewModel(); - var target = new TestSelector(); + var target = new TestSelector(SelectionMode.Multiple); var itemsBinding = new Binding { @@ -601,10 +554,9 @@ public void Unbound_SelectedItems_Should_Be_Cleared_When_DataContext_Cleared() Items = new[] { "foo", "bar", "baz" }, }; - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { DataContext = data, - Template = Template(), }; var itemsBinding = new Binding { Path = "Items" }; @@ -623,10 +575,9 @@ public void Adding_To_SelectedItems_Should_Raise_SelectionChanged() { var items = new[] { "foo", "bar", "baz" }; - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { DataContext = items, - Template = Template(), Items = items, }; @@ -649,10 +600,9 @@ public void Removing_From_SelectedItems_Should_Raise_SelectionChanged() { var items = new[] { "foo", "bar", "baz" }; - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = items, - Template = Template(), SelectedItem = "bar", }; @@ -675,15 +625,16 @@ public void Assigning_SelectedItems_Should_Raise_SelectionChanged() { var items = new[] { "foo", "bar", "baz" }; - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = items, - Template = Template(), SelectedItem = "bar", }; var called = false; + Prepare(target); + target.SelectionChanged += (s, e) => { Assert.Equal(new[] { "foo", "baz" }, e.AddedItems.Cast()); @@ -691,28 +642,26 @@ public void Assigning_SelectedItems_Should_Raise_SelectionChanged() called = true; }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); target.SelectedItems = new AvaloniaList("foo", "baz"); Assert.True(called); } - + [Fact] public void Shift_Selecting_From_No_Selection_Selects_From_Start() { + using var app = Start(); + var target = new ListBox { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz" }, SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Shift); + Prepare(target); - var panel = target.Presenter.Panel; + var selected = (Interactive)target.Presenter.RealizedElements.ElementAt(2); + _helper.Click(selected, modifiers: KeyModifiers.Shift); Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); @@ -721,15 +670,15 @@ public void Shift_Selecting_From_No_Selection_Selects_From_Start() [Fact] public void Ctrl_Selecting_Raises_SelectionChanged_Events() { + using var app = Start(); + var target = new ListBox { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz", "Qux" }, SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); SelectionChangedEventArgs receivedArgs = null; @@ -749,22 +698,22 @@ void VerifyRemoved(string selection) Assert.Empty(receivedArgs.AddedItems); } - _helper.Click((Interactive)target.Presenter.Panel.Children[1]); + ClickContainer(target, 1); VerifyAdded("Bar"); receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); + ClickContainer(target, 2, KeyModifiers.Control); VerifyAdded("Baz"); receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); + ClickContainer(target, 3, KeyModifiers.Control); VerifyAdded("Qux"); receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); + ClickContainer(target, 1, KeyModifiers.Control); VerifyRemoved("Bar"); } @@ -772,24 +721,24 @@ void VerifyRemoved(string selection) [Fact] public void Ctrl_Selecting_SelectedItem_With_Multiple_Selection_Active_Sets_SelectedItem_To_Next_Selection() { + using var app = Start(); + var target = new ListBox { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz", "Qux" }, SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[1]); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); - _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); + Prepare(target); + ClickContainer(target, 1); + ClickContainer(target, 2, KeyModifiers.Control); + ClickContainer(target, 3, KeyModifiers.Control); Assert.Equal(1, target.SelectedIndex); Assert.Equal("Bar", target.SelectedItem); Assert.Equal(new[] { "Bar", "Baz", "Qux" }, target.SelectedItems); - _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); + _helper.Click((Interactive)target.Presenter.RealizedElements.ElementAt(1), modifiers: KeyModifiers.Control); Assert.Equal(2, target.SelectedIndex); Assert.Equal("Baz", target.SelectedItem); @@ -799,22 +748,23 @@ public void Ctrl_Selecting_SelectedItem_With_Multiple_Selection_Active_Sets_Sele [Fact] public void Ctrl_Selecting_Non_SelectedItem_With_Multiple_Selection_Active_Leaves_SelectedItem_The_Same() { + using var app = Start(); + var target = new ListBox { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz" }, SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[1]); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); + Prepare(target); + + ClickContainer(target, 1); + ClickContainer(target, 2, KeyModifiers.Control); Assert.Equal(1, target.SelectedIndex); Assert.Equal("Bar", target.SelectedItem); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); + ClickContainer(target, 2, KeyModifiers.Control); Assert.Equal(1, target.SelectedIndex); Assert.Equal("Bar", target.SelectedItem); @@ -823,19 +773,18 @@ public void Ctrl_Selecting_Non_SelectedItem_With_Multiple_Selection_Active_Leave [Fact] public void Should_Ctrl_Select_Correct_Item_When_Duplicate_Items_Are_Present() { + using var app = Start(); + var target = new ListBox { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[3]); - _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: KeyModifiers.Control); + Prepare(target); - var panel = target.Presenter.Panel; + ClickContainer(target, 3); + ClickContainer(target, 4, KeyModifiers.Control); Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); Assert.Equal(new[] { 3, 4 }, SelectedContainers(target)); @@ -844,19 +793,18 @@ public void Should_Ctrl_Select_Correct_Item_When_Duplicate_Items_Are_Present() [Fact] public void Should_Shift_Select_Correct_Item_When_Duplicates_Are_Present() { + using var app = Start(); + var target = new ListBox { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[3]); - _helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: KeyModifiers.Shift); + Prepare(target); - var panel = target.Presenter.Panel; + ClickContainer(target, 3); + ClickContainer(target, 5, KeyModifiers.Shift); Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); Assert.Equal(new[] { 3, 4, 5 }, SelectedContainers(target)); @@ -865,19 +813,17 @@ public void Should_Shift_Select_Correct_Item_When_Duplicates_Are_Present() [Fact] public void Can_Shift_Select_All_Items_When_Duplicates_Are_Present() { + using var app = Start(); + var target = new ListBox { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - _helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: KeyModifiers.Shift); - - var panel = target.Presenter.Panel; + Prepare(target); + ClickContainer(target, 0); + ClickContainer(target, 5, KeyModifiers.Shift); Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.SelectedItems); Assert.Equal(new[] { 0, 1, 2, 3, 4, 5 }, SelectedContainers(target)); @@ -886,15 +832,15 @@ public void Can_Shift_Select_All_Items_When_Duplicates_Are_Present() [Fact] public void Shift_Selecting_Raises_SelectionChanged_Events() { + using var app = Start(); + var target = new ListBox { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz", "Qux" }, SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); SelectionChangedEventArgs receivedArgs = null; @@ -914,17 +860,17 @@ void VerifyRemoved(string selection) Assert.Empty(receivedArgs.AddedItems); } - _helper.Click((Interactive)target.Presenter.Panel.Children[1]); + ClickContainer(target, 1); VerifyAdded("Bar"); receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Shift); + ClickContainer(target, 3, KeyModifiers.Shift); - VerifyAdded("Baz" ,"Qux"); + VerifyAdded("Baz", "Qux"); receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Shift); + ClickContainer(target, 2, KeyModifiers.Shift); VerifyRemoved("Qux"); } @@ -932,28 +878,28 @@ void VerifyRemoved(string selection) [Fact] public void Duplicate_Items_Are_Added_To_SelectedItems_In_Order() { + using var app = Start(); + var target = new ListBox { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + Prepare(target); + ClickContainer(target, 0); Assert.Equal(new[] { "Foo" }, target.SelectedItems); - _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: KeyModifiers.Control); + ClickContainer(target, 4, KeyModifiers.Control); Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); - _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); + ClickContainer(target, 3, KeyModifiers.Control); Assert.Equal(new[] { "Foo", "Bar", "Foo" }, target.SelectedItems); - _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); + ClickContainer(target, 1, KeyModifiers.Control); Assert.Equal(new[] { "Foo", "Bar", "Foo", "Bar" }, target.SelectedItems); } @@ -961,17 +907,13 @@ public void Duplicate_Items_Are_Added_To_SelectedItems_In_Order() [Fact] public void SelectAll_Sets_SelectedIndex_And_SelectedItem() { - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz" }, SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - target.SelectAll(); + target.Selection.SelectAll(); Assert.Equal(0, target.SelectedIndex); Assert.Equal("Foo", target.SelectedItem); @@ -980,21 +922,17 @@ public void SelectAll_Sets_SelectedIndex_And_SelectedItem() [Fact] public void SelectAll_Raises_SelectionChanged_Event() { - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz" }, SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - SelectionChangedEventArgs receivedArgs = null; target.SelectionChanged += (_, args) => receivedArgs = args; - target.SelectAll(); + target.Selection.SelectAll(); Assert.NotNull(receivedArgs); Assert.Equal(target.Items, receivedArgs.AddedItems); @@ -1004,18 +942,14 @@ public void SelectAll_Raises_SelectionChanged_Event() [Fact] public void UnselectAll_Clears_SelectedIndex_And_SelectedItem() { - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz" }, SelectionMode = SelectionMode.Multiple, SelectedIndex = 0, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - target.UnselectAll(); + target.Selection.Clear(); Assert.Equal(-1, target.SelectedIndex); Assert.Equal(null, target.SelectedItem); @@ -1024,16 +958,13 @@ public void UnselectAll_Clears_SelectedIndex_And_SelectedItem() [Fact] public void SelectAll_Handles_Duplicate_Items() { - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - target.SelectAll(); + target.Selection.SelectAll(); Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.SelectedItems); } @@ -1041,6 +972,8 @@ public void SelectAll_Handles_Duplicate_Items() [Fact] public void Adding_Item_Before_SelectedItems_Should_Update_Selection() { + using var app = Start(); + var items = new ObservableCollection { "Foo", @@ -1050,26 +983,27 @@ public void Adding_Item_Before_SelectedItems_Should_Update_Selection() var target = new ListBox { - Template = Template(), Items = items, SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - + Prepare(target); target.SelectAll(); items.Insert(0, "Qux"); + target.ScrollIntoView(0); Assert.Equal(1, target.SelectedIndex); Assert.Equal("Foo", target.SelectedItem); Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(4, target.Presenter.RealizedElements.Count()); Assert.Equal(new[] { 1, 2, 3 }, SelectedContainers(target)); } [Fact] public void Removing_Item_Before_SelectedItem_Should_Update_Selection() { + using var app = Start(); + var items = new ObservableCollection { "Foo", @@ -1077,15 +1011,13 @@ public void Removing_Item_Before_SelectedItem_Should_Update_Selection() "Baz" }; - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { - Template = Template(), Items = items, SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); target.SelectedIndex = 1; target.SelectRange(2); @@ -1097,12 +1029,15 @@ public void Removing_Item_Before_SelectedItem_Should_Update_Selection() Assert.Equal(0, target.SelectedIndex); Assert.Equal("Bar", target.SelectedItem); Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(2, target.Presenter.RealizedElements.Count()); Assert.Equal(new[] { 0, 1 }, SelectedContainers(target)); } [Fact] public void Removing_SelectedItem_With_Multiple_Selection_Active_Should_Update_Selection() { + using var app = Start(); + var items = new ObservableCollection { "Foo", @@ -1112,13 +1047,11 @@ public void Removing_SelectedItem_With_Multiple_Selection_Active_Should_Update_S var target = new ListBox { - Template = Template(), Items = items, SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); target.SelectAll(); items.RemoveAt(0); @@ -1132,6 +1065,8 @@ public void Removing_SelectedItem_With_Multiple_Selection_Active_Should_Update_S [Fact] public void Replacing_Selected_Item_Should_Update_SelectedItems() { + using var app = Start(); + var items = new ObservableCollection { "Foo", @@ -1141,13 +1076,11 @@ public void Replacing_Selected_Item_Should_Update_SelectedItems() var target = new ListBox { - Template = Template(), Items = items, SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); target.SelectAll(); items[1] = "Qux"; @@ -1158,21 +1091,21 @@ public void Replacing_Selected_Item_Should_Update_SelectedItems() [Fact] public void Left_Click_On_SelectedItem_Should_Clear_Existing_Selection() { + using var app = Start(); + var target = new ListBox { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz" }, ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); target.SelectAll(); Assert.Equal(3, target.SelectedItems.Count); - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.RealizedElements.ElementAt(0)); Assert.Equal(1, target.SelectedItems.Count); Assert.Equal(new[] { "Foo", }, target.SelectedItems); @@ -1182,21 +1115,21 @@ public void Left_Click_On_SelectedItem_Should_Clear_Existing_Selection() [Fact] public void Right_Click_On_SelectedItem_Should_Not_Clear_Existing_Selection() { + using var app = Start(); + var target = new ListBox { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz" }, ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); target.SelectAll(); Assert.Equal(3, target.SelectedItems.Count); - _helper.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right); + _helper.Click((Interactive)target.Presenter.RealizedElements.ElementAt(0), MouseButton.Right); Assert.Equal(3, target.SelectedItems.Count); } @@ -1204,22 +1137,22 @@ public void Right_Click_On_SelectedItem_Should_Not_Clear_Existing_Selection() [Fact] public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection() { + using var app = Start(); + var target = new ListBox { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz" }, ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Shift); + Prepare(target); + _helper.Click((Interactive)target.Presenter.RealizedElements.ElementAt(0)); + _helper.Click((Interactive)target.Presenter.RealizedElements.ElementAt(1), modifiers: KeyModifiers.Shift); Assert.Equal(2, target.SelectedItems.Count); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right); + _helper.Click((Interactive)target.Presenter.RealizedElements.ElementAt(2), MouseButton.Right); Assert.Equal(1, target.SelectedItems.Count); } @@ -1227,21 +1160,21 @@ public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection() [Fact] public void Adding_Selected_ItemContainers_Should_Update_Selection() { + using var app = Start(); + var items = new AvaloniaList(new[] { new ItemContainer(), new ItemContainer(), }); - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = items, SelectionMode = SelectionMode.Multiple, - Template = Template(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); items.Add(new ItemContainer { IsSelected = true }); items.Add(new ItemContainer { IsSelected = true }); @@ -1253,19 +1186,19 @@ public void Adding_Selected_ItemContainers_Should_Update_Selection() [Fact] public void Shift_Right_Click_Should_Not_Select_Multiple() { + using var app = Start(); + var target = new ListBox { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz" }, ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: KeyModifiers.Shift); + _helper.Click((Interactive)target.Presenter.RealizedElements.ElementAt(0)); + _helper.Click((Interactive)target.Presenter.RealizedElements.ElementAt(2), MouseButton.Right, modifiers: KeyModifiers.Shift); Assert.Equal(1, target.SelectedItems.Count); } @@ -1273,19 +1206,19 @@ public void Shift_Right_Click_Should_Not_Select_Multiple() [Fact] public void Ctrl_Right_Click_Should_Not_Select_Multiple() { + using var app = Start(); + var target = new ListBox { - Template = Template(), Items = new[] { "Foo", "Bar", "Baz" }, ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), SelectionMode = SelectionMode.Multiple, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: KeyModifiers.Control); + _helper.Click((Interactive)target.Presenter.RealizedElements.ElementAt(0)); + _helper.Click((Interactive)target.Presenter.RealizedElements.ElementAt(2), MouseButton.Right, modifiers: KeyModifiers.Control); Assert.Equal(1, target.SelectedItems.Count); } @@ -1293,10 +1226,9 @@ public void Ctrl_Right_Click_Should_Not_Select_Multiple() [Fact] public void Adding_To_Selection_Should_Set_SelectedIndex() { - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { "foo", "bar" }, - Template = Template(), }; target.ApplyTemplate(); @@ -1308,10 +1240,9 @@ public void Adding_To_Selection_Should_Set_SelectedIndex() [Fact] public void Assigning_Null_To_Selection_Should_Create_New_SelectionModel() { - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { "foo", "bar" }, - Template = Template(), }; var oldSelection = target.Selection; @@ -1325,7 +1256,7 @@ public void Assigning_Null_To_Selection_Should_Create_New_SelectionModel() [Fact] public void Assigning_SelectionModel_With_Different_Source_To_Selection_Should_Fail() { - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { "foo", "bar" }, Template = Template(), @@ -1338,10 +1269,9 @@ public void Assigning_SelectionModel_With_Different_Source_To_Selection_Should_F [Fact] public void Assigning_SelectionModel_With_Null_Source_To_Selection_Should_Set_Source() { - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { "foo", "bar" }, - Template = Template(), }; var selection = new SelectionModel(); @@ -1353,14 +1283,14 @@ public void Assigning_SelectionModel_With_Null_Source_To_Selection_Should_Set_So [Fact] public void Assigning_Single_Selected_Item_To_Selection_Should_Set_SelectedIndex() { - var target = new TestSelector + using var app = Start(); + + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { "foo", "bar" }, - Template = Template(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); var selection = new SelectionModel { SingleSelect = false }; selection.Select(1); @@ -1374,14 +1304,14 @@ public void Assigning_Single_Selected_Item_To_Selection_Should_Set_SelectedIndex [Fact] public void Assigning_Multiple_Selected_Items_To_Selection_Should_Set_SelectedIndex() { - var target = new TestSelector + using var app = Start(); + + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { "foo", "bar", "baz" }, - Template = Template(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); var selection = new SelectionModel { SingleSelect = false }; selection.SelectRange(0, 2); @@ -1395,13 +1325,13 @@ public void Assigning_Multiple_Selected_Items_To_Selection_Should_Set_SelectedIn [Fact] public void Reassigning_Selection_Should_Clear_Selection() { - var target = new TestSelector + using var app = Start(); + + var target = new TestSelector(SelectionMode.Multiple) { Items = new[] { "foo", "bar" }, - Template = Template(), }; - target.ApplyTemplate(); target.Selection.Select(1); target.Selection = new SelectionModel(); @@ -1412,6 +1342,8 @@ public void Reassigning_Selection_Should_Clear_Selection() [Fact] public void Assigning_Selection_Should_Set_Item_IsSelected() { + using var app = Start(); + var items = new[] { new ListBoxItem(), @@ -1419,14 +1351,12 @@ public void Assigning_Selection_Should_Set_Item_IsSelected() new ListBoxItem(), }; - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = items, - Template = Template(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); var selection = new SelectionModel { SingleSelect = false }; selection.SelectRange(0, 1); @@ -1442,10 +1372,9 @@ public void Assigning_Selection_Should_Raise_SelectionChanged() { var items = new[] { "foo", "bar", "baz" }; - var target = new TestSelector + var target = new TestSelector(SelectionMode.Multiple) { Items = items, - Template = Template(), SelectedItem = "bar", }; @@ -1467,8 +1396,7 @@ public void Assigning_Selection_Should_Raise_SelectionChanged() ++raised; }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); var selection = new SelectionModel { Source = items, SingleSelect = false }; selection.Select(0); @@ -1479,56 +1407,23 @@ public void Assigning_Selection_Should_Raise_SelectionChanged() } private IEnumerable SelectedContainers(SelectingItemsControl target) { - return target.Presenter.Panel.Children - .Select((x, i) => x.Classes.Contains(":selected") ? i : -1) + return target.Presenter.RealizedElements + .Select(x => x.Classes.Contains(":selected") ? target.GetContainerIndex(x) : -1) .Where(x => x != -1); } - private FuncControlTemplate Template() + private void ClickContainer(SelectingItemsControl target, int index, KeyModifiers modifiers = KeyModifiers.None) { - return new FuncControlTemplate((control, scope) => - new ItemsPresenter - { - Name = "PART_ItemsPresenter", - [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], - [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], - }.RegisterInNameScope(scope)); + _helper.Click((Interactive)target.Presenter.RealizedElements.ElementAt(index), modifiers: modifiers); + ItemsRepeaterWorkaround(target); } - private class TestSelector : SelectingItemsControl + private void ItemsRepeaterWorkaround(SelectingItemsControl target) { - public static readonly new AvaloniaProperty SelectedItemsProperty = - SelectingItemsControl.SelectedItemsProperty; - public static readonly new DirectProperty SelectionProperty = - SelectingItemsControl.SelectionProperty; - - public TestSelector() - { - SelectionMode = SelectionMode.Multiple; - } - - public new IList SelectedItems - { - get { return base.SelectedItems; } - set { base.SelectedItems = value; } - } - - public new ISelectionModel Selection - { - get => base.Selection; - set => base.Selection = value; - } - - public new SelectionMode SelectionMode - { - get { return base.SelectionMode; } - set { base.SelectionMode = value; } - } - - public void SelectAll() => Selection.SelectAll(); - public void UnselectAll() => Selection.Clear(); - public void SelectRange(int index) => UpdateSelection(index, true, true); - public void Toggle(int index) => UpdateSelection(index, true, false, true); + // HACK: Selecting an item in ItemsRepeater causes it to be scrolled to the top of the viewport, + // even if all items can fit in the viewport. This causes items before the selected item to be + // unrealized. Work around this by scrolling to the top. + target.ScrollIntoView(0); } private class OldDataContextViewModel diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index fd52aeb9af2..220bc463925 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -48,6 +48,8 @@ public void First_Tab_Should_Be_Selected_By_Default() [Fact] public void Pre_Selecting_TabItem_Should_Set_SelectedContent_After_It_Was_Added() { + using var app = Start(); + var target = new TabControl { Template = TabControlTemplate(), @@ -63,7 +65,10 @@ public void Pre_Selecting_TabItem_Should_Set_SelectedContent_After_It_Was_Added( target.Items = items; + var root = new TestRoot(target); + ApplyTemplate(target); + root.LayoutManager.ExecuteInitialLayoutPass(); Assert.Equal(secondContent, target.SelectedContent); } @@ -183,6 +188,8 @@ public void TabItem_Templates_Should_Be_Set_Before_TabItem_ApplyTemplate() [Fact] public void DataContexts_Should_Be_Correctly_Set() { + using var app = Start(); + var items = new object[] { "Foo", @@ -203,13 +210,17 @@ public void DataContexts_Should_Be_Correctly_Set() Items = items, }; + var root = new TestRoot(target); + ApplyTemplate(target); + root.LayoutManager.ExecuteInitialLayoutPass(); ((ContentPresenter)target.ContentPart).UpdateChild(); var dataContext = ((TextBlock)target.ContentPart.Child).DataContext; Assert.Equal(items[0], dataContext); target.SelectedIndex = 1; + ((ContentPresenter)target.ContentPart).UpdateChild(); dataContext = ((Button)target.ContentPart.Child).DataContext; Assert.Equal(items[1], dataContext); @@ -242,6 +253,8 @@ public void DataContexts_Should_Be_Correctly_Set() [Fact] public void Non_IHeadered_Control_Items_Should_Be_Ignored() { + using var app = Start(); + var items = new[] { new TextBlock { Text = "foo" }, @@ -254,9 +267,16 @@ public void Non_IHeadered_Control_Items_Should_Be_Ignored() Items = items, }; + var root = new TestRoot(target) + { + Width = 100, + Height = 100 + }; + ApplyTemplate(target); + root.LayoutManager.ExecuteInitialLayoutPass(); - var logicalChildren = target.ItemsPresenterPart.Panel.GetLogicalChildren(); + var logicalChildren = target.Presenter.RealizedElements; var result = logicalChildren .OfType() @@ -292,6 +312,8 @@ public void Should_Handle_Changing_To_TabItem_With_Null_Content() [Fact] public void DataTemplate_Created_Content_Should_Be_Logical_Child_After_ApplyTemplate() { + using var app = Start(); + TabControl target = new TabControl { Template = TabControlTemplate(), @@ -300,7 +322,11 @@ public void DataTemplate_Created_Content_Should_Be_Logical_Child_After_ApplyTemp Items = new[] { "Foo" }, }; + var root = new TestRoot(target); + ApplyTemplate(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + ((ContentPresenter)target.ContentPart).UpdateChild(); var content = Assert.IsType(target.ContentPart.Child); @@ -349,6 +375,14 @@ public void Can_Have_Empty_Tab_Control() } } + private static IDisposable Start() + { + var services = TestServices.MockPlatformRenderInterface.With( + styler: new Styler(), + windowingPlatform: new MockWindowingPlatform()); + return UnitTestApplication.Start(services); + } + private IControlTemplate TabControlTemplate() { return new FuncControlTemplate((parent, scope) => @@ -401,6 +435,14 @@ private void ApplyTemplate(TabControl target) target.ContentPart.ApplyTemplate(); } + private void ItemsRepeaterWorkaround(SelectingItemsControl target) + { + // HACK: Selecting an item in ItemsRepeater causes it to be scrolled to the top of the viewport, + // even if all items can fit in the viewport. This causes items before the selected item to be + // unrealized. Work around this by scrolling to the top. + target.ScrollIntoView(0); + } + private class Item { public Item(string value) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 7022fbf4c1f..a1b93d96f36 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1,18 +1,19 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Selection; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Input; using Avalonia.Input.Platform; -using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Xunit; namespace Avalonia.Controls.UnitTests @@ -24,16 +25,14 @@ public class TreeViewTests [Fact] public void Items_Should_Be_Created() { + using var app = Start(); + var target = new TreeView { - Template = CreateTreeViewTemplate(), Items = CreateTestTreeData(), }; - var root = new TestRoot(target); - - CreateNodeDataTemplate(target); - ApplyTemplates(target); + Prepare(target); Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0)); Assert.Equal(new[] { "Child1", "Child2", "Child3" }, ExtractItemHeader(target, 1)); @@ -43,23 +42,20 @@ public void Items_Should_Be_Created() [Fact] public void Items_Should_Be_Created_Using_ItemTemplate_If_Present() { - TreeView target; + using var app = Start(); - var root = new TestRoot + var target = new TreeView { - Child = target = new TreeView - { - Template = CreateTreeViewTemplate(), - Items = CreateTestTreeData(), - ItemTemplate = new FuncTreeDataTemplate( - (_, __) => new Canvas(), - x => x.Children), - } + Items = CreateTestTreeData(), + ItemTemplate = new FuncTreeDataTemplate( + (_, __) => new Canvas(), + x => x.Children), }; - ApplyTemplates(target); + Prepare(target, createDataTemplates: false); - var items = target.ItemContainerGenerator.Index.Containers + var items = target.Presenter + .GetVisualDescendants() .OfType() .ToList(); @@ -68,406 +64,369 @@ public void Items_Should_Be_Created_Using_ItemTemplate_If_Present() } [Fact] - public void Root_ItemContainerGenerator_Containers_Should_Be_Root_Containers() + public void Items_Should_Have_Correct_IndexPath() { + using var app = Start(); + var target = new TreeView { - Template = CreateTreeViewTemplate(), Items = CreateTestTreeData(), }; - var root = new TestRoot(target); + Prepare(target); - CreateNodeDataTemplate(target); - ApplyTemplates(target); + var items = target.Presenter + .GetVisualDescendants() + .OfType() + .Select(x => x.IndexPath) + .ToList(); - var container = (TreeViewItem)target.ItemContainerGenerator.Containers.Single().ContainerControl; - var header = (TextBlock)container.Header; - Assert.Equal("Root", header.Text); + Assert.Equal( + new[] + { + new IndexPath(0), + new IndexPath(0, 0), + new IndexPath(0, 1), + new IndexPath(new[] { 0, 1, 0 }), + new IndexPath(0, 2), + }, + items); } [Fact] - public void Root_TreeContainerFromItem_Should_Return_Descendant_Item() + public void TreeContainerFromIndex_Should_Return_Descendant_Item() { - var tree = CreateTestTreeData(); + using var app = Start(); + var target = new TreeView { - Template = CreateTreeViewTemplate(), - Items = tree, + Items = CreateTestTreeData(), }; - // For TreeViewItem to find its parent TreeView, OnAttachedToLogicalTree needs - // to be called, which requires an IStyleRoot. - var root = new TestRoot(); - root.Child = target; + Prepare(target); - CreateNodeDataTemplate(target); - ApplyTemplates(target); - - var container = target.ItemContainerGenerator.Index.ContainerFromItem( - tree[0].Children[1].Children[0]); + var index = new IndexPath(new[] { 0, 1, 0 }); + var container = target.TreeContainerFromIndex(index); Assert.NotNull(container); - var header = ((TreeViewItem)container).Header; - var headerContent = ((TextBlock)header).Text; + var headerContent = ((TextBlock)container.HeaderPresenter.Child).Text; Assert.Equal("Grandchild2a", headerContent); } [Fact] - public void Clicking_Item_Should_Select_It() + public void TreeContainerFromIndex_Should_Return_Null_When_Item_Removed() { - using (Application()) + using var app = Start(); + + var items = CreateTestTreeData(); + var target = new TreeView { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - Items = tree, - }; + Items = items, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var root = Prepare(target); - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + var index = new IndexPath(new[] { 0, 1, 0 }); + var container = target.TreeContainerFromIndex(index); - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + Assert.NotNull(container); - Assert.NotNull(container); + items[0].Children[1].Children.RemoveAt(0); - _mouse.Click(container); + root.LayoutManager.ExecuteLayoutPass(); - Assert.Equal(item, target.SelectedItem); - Assert.True(container.IsSelected); - } + container = target.TreeContainerFromIndex(index); + + Assert.Null(container); } [Fact] - public void Clicking_WithControlModifier_Selected_Item_Should_Deselect_It() + public void Clicking_Item_Should_Select_It() { - using (Application()) + using var app = Start(); + + var tree = CreateTestTreeData(); + var target = new TreeView { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - Items = tree - }; + Items = tree, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + Prepare(target); - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + var item = tree[0].Children[1].Children[0]; + var container = target.TreeContainerFromIndex(new IndexPath(new[] { 0, 1, 0 })); - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + Assert.NotNull(container); - Assert.NotNull(container); + _mouse.Click(container); - target.SelectedItem = item; + Assert.Equal(item, target.SelectedItem); + Assert.True(container.IsSelected); + } - Assert.True(container.IsSelected); + [Fact] + public void Clicking_WithControlModifier_Selected_Item_Should_Deselect_It() + { + using var app = Start(); + + var tree = CreateTestTreeData(); + var target = new TreeView + { + Items = tree + }; - _mouse.Click(container, modifiers: KeyModifiers.Control); + Prepare(target); - Assert.Null(target.SelectedItem); - Assert.False(container.IsSelected); - } + var item = tree[0].Children[1].Children[0]; + var container = target.TreeContainerFromIndex(new IndexPath(new[] { 0, 1, 0 })); + + Assert.NotNull(container); + + target.SelectedItem = item; + + Assert.True(container.IsSelected); + + _mouse.Click(container, modifiers: KeyModifiers.Control); + + Assert.Null(target.SelectedItem); + Assert.False(container.IsSelected); } [Fact] public void Clicking_WithControlModifier_Not_Selected_Item_Should_Select_It() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - Items = tree - }; + using var app = Start(); - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Items = tree + }; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + Prepare(target); - var item1 = tree[0].Children[1].Children[0]; - var container1 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); + var item1 = tree[0].Children[1].Children[0]; + var container1 = target.TreeContainerFromIndex(new IndexPath(new[] { 0, 1, 0 })); - var item2 = tree[0].Children[1]; - var container2 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); + var item2 = tree[0].Children[1]; + var container2 = target.TreeContainerFromIndex(new IndexPath(new[] { 0, 1 })); - Assert.NotNull(container1); - Assert.NotNull(container2); + Assert.NotNull(container1); + Assert.NotNull(container2); - target.SelectedItem = item1; + target.SelectedItem = item1; - Assert.True(container1.IsSelected); + Assert.True(container1.IsSelected); - _mouse.Click(container2, modifiers: KeyModifiers.Control); + _mouse.Click(container2, modifiers: KeyModifiers.Control); - Assert.Equal(item2, target.SelectedItem); - Assert.False(container1.IsSelected); - Assert.True(container2.IsSelected); - } + Assert.Equal(item2, target.SelectedItem); + Assert.False(container1.IsSelected); + Assert.True(container2.IsSelected); } [Fact] public void Clicking_WithControlModifier_Selected_Item_Should_Deselect_And_Remove_From_SelectedItems() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; + using var app = Start(); - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + Prepare(target); - var rootNode = tree[0]; + var rootNode = tree[0]; - var item1 = rootNode.Children[0]; - var item2 = rootNode.Children.Last(); + var item1 = rootNode.Children[0]; + var item2 = rootNode.Children.Last(); - var item1Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); - var item2Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); + var item1Container = target.TreeContainerFromIndex(new IndexPath(0, 0)); + var item2Container = target.TreeContainerFromIndex(new IndexPath(0, 2)); - ClickContainer(item1Container, KeyModifiers.Control); - Assert.True(item1Container.IsSelected); + ClickContainer(item1Container, KeyModifiers.Control); + Assert.True(item1Container.IsSelected); - ClickContainer(item2Container, KeyModifiers.Control); - Assert.True(item2Container.IsSelected); + ClickContainer(item2Container, KeyModifiers.Control); + Assert.True(item2Container.IsSelected); - Assert.Equal(new[] { item1, item2 }, target.SelectedItems.OfType()); + Assert.Equal(new[] { item1, item2 }, target.SelectedItems.OfType()); - ClickContainer(item1Container, KeyModifiers.Control); - Assert.False(item1Container.IsSelected); + ClickContainer(item1Container, KeyModifiers.Control); + Assert.False(item1Container.IsSelected); - Assert.DoesNotContain(item1, target.SelectedItems.OfType()); - } + Assert.DoesNotContain(item1, target.SelectedItems.OfType()); } [Fact] public void Clicking_WithShiftModifier_DownDirection_Should_Select_Range_Of_Items() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; + using var app = Start(); - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + Prepare(target); - var rootNode = tree[0]; + var rootNode = tree[0]; - var from = rootNode.Children[0]; - var to = rootNode.Children.Last(); + var from = rootNode.Children[0]; + var to = rootNode.Children[2]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var fromContainer = target.TreeContainerFromIndex(new IndexPath(0, 0)); + var toContainer = target.TreeContainerFromIndex(new IndexPath(0, 2)); - ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(fromContainer, KeyModifiers.None); - Assert.True(fromContainer.IsSelected); + Assert.True(fromContainer.IsSelected); - ClickContainer(toContainer, KeyModifiers.Shift); - AssertChildrenSelected(target, rootNode); - } + ClickContainer(toContainer, KeyModifiers.Shift); + AssertChildrenSelected(target, rootNode); } [Fact] public void Clicking_WithShiftModifier_UpDirection_Should_Select_Range_Of_Items() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; + using var app = Start(); - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + Prepare(target); - var rootNode = tree[0]; + var rootNode = tree[0]; - var from = rootNode.Children.Last(); - var to = rootNode.Children[0]; + var from = rootNode.Children[2]; + var to = rootNode.Children[0]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var fromContainer = target.TreeContainerFromIndex(new IndexPath(0, 2)); + var toContainer = target.TreeContainerFromIndex(new IndexPath(0, 0)); - ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(fromContainer, KeyModifiers.None); - Assert.True(fromContainer.IsSelected); + Assert.True(fromContainer.IsSelected); - ClickContainer(toContainer, KeyModifiers.Shift); - AssertChildrenSelected(target, rootNode); - } + ClickContainer(toContainer, KeyModifiers.Shift); + AssertChildrenSelected(target, rootNode); } [Fact] public void Clicking_First_Item_Of_SelectedItems_Should_Select_Only_It() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; + using var app = Start(); - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + var tree = CreateTestTreeData(); + var target = new TreeView + { + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - var rootNode = tree[0]; + Prepare(target); - var from = rootNode.Children.Last(); - var to = rootNode.Children[0]; + var rootNode = tree[0]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var from = rootNode.Children[2]; + var to = rootNode.Children[0]; - ClickContainer(fromContainer, KeyModifiers.None); + var fromContainer = target.TreeContainerFromIndex(new IndexPath(0, 2)); + var toContainer = target.TreeContainerFromIndex(new IndexPath(0, 0)); - ClickContainer(toContainer, KeyModifiers.Shift); - AssertChildrenSelected(target, rootNode); + ClickContainer(fromContainer, KeyModifiers.None); - ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(toContainer, KeyModifiers.Shift); + AssertChildrenSelected(target, rootNode); - Assert.True(fromContainer.IsSelected); + ClickContainer(fromContainer, KeyModifiers.None); - foreach (var child in rootNode.Children) - { - if (child == from) - { - continue; - } + Assert.True(fromContainer.IsSelected); - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(child); - - Assert.False(container.IsSelected); - } + for (var i = 0; i < rootNode.Children.Count - 1; ++i) + { + var container = target.TreeContainerFromIndex(new IndexPath(0, i)); + Assert.False(container.IsSelected); } } [Fact] public void Setting_SelectedItem_Should_Set_Container_Selected() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - Items = tree, - }; + using var app = Start(); - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Items = tree, + }; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + Prepare(target); - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + var item = tree[0].Children[1].Children[0]; + var container = target.TreeContainerFromIndex(new IndexPath(new[] { 0, 1, 0 })); - Assert.NotNull(container); + Assert.NotNull(container); - target.SelectedItem = item; + target.SelectedItem = item; - Assert.True(container.IsSelected); - } + Assert.True(container.IsSelected); } [Fact] public void Setting_SelectedItem_Should_Raise_SelectedItemChanged_Event() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - Items = tree, - }; + using var app = Start(); - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Items = tree, + }; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + Prepare(target); - var item = tree[0].Children[1].Children[0]; + var item = tree[0].Children[1].Children[0]; - var called = false; - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.RemovedItems); - Assert.Equal(1, e.AddedItems.Count); - Assert.Same(item, e.AddedItems[0]); - called = true; - }; + var called = false; + target.SelectionChanged += (s, e) => + { + Assert.Empty(e.RemovedItems); + Assert.Equal(1, e.AddedItems.Count); + Assert.Same(item, e.AddedItems[0]); + called = true; + }; - target.SelectedItem = item; - Assert.True(called); - } + target.SelectedItem = item; + Assert.True(called); } [Fact] public void LogicalChildren_Should_Be_Set() { + using var app = Start(); + var target = new TreeView { - Template = CreateTreeViewTemplate(), Items = new[] { "Foo", "Bar", "Baz " }, }; - ApplyTemplates(target); + Prepare(target); var result = target.GetLogicalChildren() .OfType() - .Select(x => x.Header) + .Select(x => x.HeaderPresenter.Child) .OfType() .Select(x => x.Text) .ToList(); @@ -478,18 +437,15 @@ public void LogicalChildren_Should_Be_Set() [Fact] public void Removing_Item_Should_Remove_Itself_And_Children_From_Index() { + using var app = Start(); + var tree = CreateTestTreeData(); var target = new TreeView { - Template = CreateTreeViewTemplate(), Items = tree, }; - var root = new TestRoot(); - root.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); + Prepare(target); Assert.Equal(5, target.ItemContainerGenerator.Index.Containers.Count()); @@ -501,6 +457,8 @@ public void Removing_Item_Should_Remove_Itself_And_Children_From_Index() [Fact] public void DataContexts_Should_Be_Correctly_Set() { + using var app = Start(); + var items = new object[] { "Foo", @@ -511,7 +469,6 @@ public void DataContexts_Should_Be_Correctly_Set() var target = new TreeView { - Template = CreateTreeViewTemplate(), DataContext = "Base", DataTemplates = { @@ -520,9 +477,9 @@ public void DataContexts_Should_Be_Correctly_Set() Items = items, }; - ApplyTemplates(target); + Prepare(target); - var dataContexts = target.Presenter.Panel.Children + var dataContexts = target.Presenter.RealizedElements .Cast() .Select(x => x.DataContext) .ToList(); @@ -535,6 +492,8 @@ public void DataContexts_Should_Be_Correctly_Set() [Fact] public void Control_Item_Should_Not_Be_NameScope() { + using var app = Start(); + var items = new object[] { new TreeViewItem(), @@ -542,32 +501,29 @@ public void Control_Item_Should_Not_Be_NameScope() var target = new TreeView { - Template = CreateTreeViewTemplate(), Items = items, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); - var item = target.Presenter.Panel.LogicalChildren[0]; + var item = target.Presenter.LogicalChildren[0]; Assert.Null(NameScope.GetNameScope((TreeViewItem)item)); } [Fact] public void Should_React_To_Children_Changing() { + using var app = Start(); + var data = CreateTestTreeData(); var target = new TreeView { - Template = CreateTreeViewTemplate(), Items = data, }; - var root = new TestRoot(target); - - CreateNodeDataTemplate(target); - ApplyTemplates(target); + Prepare(target); + ExpandAll(target); Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0)); Assert.Equal(new[] { "Child1", "Child2", "Child3" }, ExtractItemHeader(target, 1)); @@ -584,6 +540,8 @@ public void Should_React_To_Children_Changing() } }; + Layout(target); + Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0)); Assert.Equal(new[] { "NewChild1" }, ExtractItemHeader(target, 1)); } @@ -591,7 +549,7 @@ public void Should_React_To_Children_Changing() [Fact] public void Keyboard_Navigation_Should_Move_To_Last_Selected_Node() { - using (Application()) + using (Start()) { var focus = FocusManager.Instance; var navigation = AvaloniaLocator.Current.GetService(); @@ -599,26 +557,20 @@ public void Keyboard_Navigation_Should_Move_To_Last_Selected_Node() var target = new TreeView { - Template = CreateTreeViewTemplate(), Items = data, }; var button = new Button(); - var root = new TestRoot + CreateRoot(new StackPanel { - Child = new StackPanel - { - Children = { target, button }, - } - }; + Children = { target, button }, + }); - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + Prepare(target); var item = data[0].Children[0]; - var node = target.ItemContainerGenerator.Index.ContainerFromItem(item); + var node = target.TreeContainerFromIndex(new IndexPath(0, 0)); Assert.NotNull(node); target.SelectedItem = item; @@ -636,25 +588,19 @@ public void Keyboard_Navigation_Should_Move_To_Last_Selected_Node() [Fact] public void Pressing_SelectAll_Gesture_Should_Select_All_Nodes() { - using (UnitTestApplication.Start()) + using (Start()) { var tree = CreateTestTreeData(); var target = new TreeView { - Template = CreateTreeViewTemplate(), Items = tree, SelectionMode = SelectionMode.Multiple }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); + Prepare(target); ExpandAll(target); var rootNode = tree[0]; - var keymap = AvaloniaLocator.Current.GetService(); var selectAllGesture = keymap.SelectAll.First(); @@ -674,30 +620,20 @@ public void Pressing_SelectAll_Gesture_Should_Select_All_Nodes() [Fact] public void Pressing_SelectAll_Gesture_With_Downward_Range_Selected_Should_Select_All_Nodes() { - using (Application()) + using (Start()) { var tree = CreateTestTreeData(); var target = new TreeView { - Template = CreateTreeViewTemplate(), Items = tree, SelectionMode = SelectionMode.Multiple }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + Prepare(target); var rootNode = tree[0]; - - var from = rootNode.Children[0]; - var to = rootNode.Children.Last(); - - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var fromContainer = target.TreeContainerFromIndex(new IndexPath(0, 0)); + var toContainer = target.TreeContainerFromIndex(new IndexPath(0, 2)); ClickContainer(fromContainer, KeyModifiers.None); ClickContainer(toContainer, KeyModifiers.Shift); @@ -721,30 +657,20 @@ public void Pressing_SelectAll_Gesture_With_Downward_Range_Selected_Should_Selec [Fact] public void Pressing_SelectAll_Gesture_With_Upward_Range_Selected_Should_Select_All_Nodes() { - using (Application()) + using (Start()) { var tree = CreateTestTreeData(); var target = new TreeView { - Template = CreateTreeViewTemplate(), Items = tree, SelectionMode = SelectionMode.Multiple }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + Prepare(target); var rootNode = tree[0]; - - var from = rootNode.Children.Last(); - var to = rootNode.Children[0]; - - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var fromContainer = target.TreeContainerFromIndex(new IndexPath(0, 2)); + var toContainer = target.TreeContainerFromIndex(new IndexPath(0, 0)); ClickContainer(fromContainer, KeyModifiers.None); ClickContainer(toContainer, KeyModifiers.Shift); @@ -768,26 +694,23 @@ public void Pressing_SelectAll_Gesture_With_Upward_Range_Selected_Should_Select_ [Fact] public void Right_Click_On_SelectedItem_Should_Not_Clear_Existing_Selection() { + using var app = Start(); + var tree = CreateTestTreeData(); var target = new TreeView { - Template = CreateTreeViewTemplate(), Items = tree, SelectionMode = SelectionMode.Multiple, }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + Prepare(target); + ExpandAll(target); target.SelectAll(); AssertChildrenSelected(target, tree[0]); Assert.Equal(5, target.SelectedItems.Count); - _mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right); + _mouse.Click(target.TreeContainerFromIndex(new IndexPath(0)), MouseButton.Right); Assert.Equal(5, target.SelectedItems.Count); } @@ -795,123 +718,92 @@ public void Right_Click_On_SelectedItem_Should_Not_Clear_Existing_Selection() [Fact] public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple, - }; + using var app = Start(); - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + var tree = CreateTestTreeData(); + var target = new TreeView + { + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; - var rootNode = tree[0]; - var to = rootNode.Children[0]; - var then = rootNode.Children[1]; + Prepare(target); + ExpandAll(target); - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(rootNode); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - var thenContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(then); + var fromContainer = target.TreeContainerFromIndex(new IndexPath(0)); + var toContainer = target.TreeContainerFromIndex(new IndexPath(0, 0)); + var thenContainer = target.TreeContainerFromIndex(new IndexPath(0, 1)); - ClickContainer(fromContainer, KeyModifiers.None); - ClickContainer(toContainer, KeyModifiers.Shift); + ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(toContainer, KeyModifiers.Shift); - Assert.Equal(2, target.SelectedItems.Count); + Assert.Equal(2, target.SelectedItems.Count); - _mouse.Click(thenContainer, MouseButton.Right); + _mouse.Click(thenContainer, MouseButton.Right); - Assert.Equal(1, target.SelectedItems.Count); - } + Assert.Equal(1, target.SelectedItems.Count); } [Fact] public void Shift_Right_Click_Should_Not_Select_Multiple() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple, - }; + using var app = Start(); - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + Prepare(target); + ExpandAll(target); - var rootNode = tree[0]; - var from = rootNode.Children[0]; - var to = rootNode.Children[1]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var fromContainer = target.TreeContainerFromIndex(new IndexPath(0, 0)); + var toContainer = target.TreeContainerFromIndex(new IndexPath(0, 1)); - _mouse.Click(fromContainer); - _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Shift); + _mouse.Click(fromContainer); + _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Shift); - Assert.Equal(1, target.SelectedItems.Count); - } + Assert.Equal(1, target.SelectedItems.Count); } [Fact] public void Ctrl_Right_Click_Should_Not_Select_Multiple() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple, - }; + using var app = Start(); - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + Prepare(target); + ExpandAll(target); - var rootNode = tree[0]; - var from = rootNode.Children[0]; - var to = rootNode.Children[1]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var fromContainer = target.TreeContainerFromIndex(new IndexPath(0, 0)); + var toContainer = target.TreeContainerFromIndex(new IndexPath(0, 1)); - _mouse.Click(fromContainer); - _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Control); + _mouse.Click(fromContainer); + _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Control); - Assert.Equal(1, target.SelectedItems.Count); - } + Assert.Equal(1, target.SelectedItems.Count); } [Fact] public void TreeViewItems_Level_Should_Be_Set() { + using var app = Start(); + var tree = CreateTestTreeData(); var target = new TreeView { - Template = CreateTreeViewTemplate(), Items = tree, }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); + Prepare(target); ExpandAll(target); Assert.Equal(0, GetItem(target, 0).Level); @@ -924,18 +816,15 @@ public void TreeViewItems_Level_Should_Be_Set() [Fact] public void TreeViewItems_Level_Should_Be_Set_For_Derived_TreeView() { + using var app = Start(); + var tree = CreateTestTreeData(); var target = new DerivedTreeView { - Template = CreateTreeViewTemplate(), Items = tree, }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); + Prepare(target); ExpandAll(target); Assert.Equal(0, GetItem(target, 0).Level); @@ -948,19 +837,16 @@ public void TreeViewItems_Level_Should_Be_Set_For_Derived_TreeView() [Fact] public void Adding_Node_To_Removed_And_ReAdded_Parent_Should_Not_Crash() { + using var app = Start(); + // Issue #2985 var tree = CreateTestTreeData(); var target = new TreeView { - Template = CreateTreeViewTemplate(), Items = tree, }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); + Prepare(target); ExpandAll(target); var parent = tree[0]; @@ -969,8 +855,7 @@ public void Adding_Node_To_Removed_And_ReAdded_Parent_Should_Not_Crash() parent.Children.Remove(node); parent.Children.Add(node); - var item = target.ItemContainerGenerator.Index.ContainerFromItem(node); - ApplyTemplates(new[] { item }); + var item = target.TreeContainerFromIndex(new IndexPath(new[] { 0, 0, 1 })); // #2985 causes ArgumentException here. node.Children.Add(new Node()); @@ -980,36 +865,30 @@ public void Adding_Node_To_Removed_And_ReAdded_Parent_Should_Not_Crash() public void Auto_Expanding_In_Style_Should_Not_Break_Range_Selection() { /// Issue #2980. - using (Application()) + using (Start()) { var target = new DerivedTreeView { - Template = CreateTreeViewTemplate(), SelectionMode = SelectionMode.Multiple, Items = new List - { - new Node { Value = "Root1", }, - new Node { Value = "Root2", }, - }, + { + new Node { Value = "Root1", }, + new Node { Value = "Root2", }, + }, }; - var visualRoot = new TestRoot + var style = new Style(x => x.OfType()) { - Styles = + Setters = { - new Style(x => x.OfType()) - { - Setters = - { - new Setter(TreeViewItem.IsExpandedProperty, true), - }, - }, + new Setter(TreeViewItem.IsExpandedProperty, true), }, - Child = target, }; - CreateNodeDataTemplate(target); - ApplyTemplates(target); + var root = CreateRoot(target); + root.Styles.Add(style); + + Prepare(target); _mouse.Click(GetItem(target, 0)); _mouse.Click(GetItem(target, 1), modifiers: KeyModifiers.Shift); @@ -1019,59 +898,51 @@ public void Auto_Expanding_In_Style_Should_Not_Break_Range_Selection() [Fact] public void Removing_TreeView_From_Root_Should_Preserve_TreeViewItems() { + using var app = Start(); + // Issue #3328 var tree = CreateTestTreeData(); var target = new TreeView { - Template = CreateTreeViewTemplate(), Items = tree, }; - var root = new TestRoot(); - root.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); + var root = Prepare(target); ExpandAll(target); - Assert.Equal(5, target.ItemContainerGenerator.Index.Containers.Count()); + Assert.Equal(1, target.Presenter.RealizedElements.Count()); root.Child = null; - Assert.Equal(5, target.ItemContainerGenerator.Index.Containers.Count()); - Assert.Equal(1, target.Presenter.Panel.Children.Count); + Assert.Equal(1, target.Presenter.RealizedElements.Count()); - var rootNode = Assert.IsType(target.Presenter.Panel.Children[0]); - Assert.Equal(3, rootNode.ItemContainerGenerator.Containers.Count()); - Assert.Equal(3, rootNode.Presenter.Panel.Children.Count); + var rootNode = (TreeViewItem)target.TryGetContainer(0); + Assert.Equal(3, rootNode.Presenter.RealizedElements.Count()); - var child2Node = Assert.IsType(rootNode.Presenter.Panel.Children[1]); - Assert.Equal(1, child2Node.ItemContainerGenerator.Containers.Count()); - Assert.Equal(1, child2Node.Presenter.Panel.Children.Count); + var child2Node = Assert.IsType(rootNode.TryGetContainer(1)); + Assert.Equal(1, child2Node.Presenter.RealizedElements.Count()); } [Fact] public void Clearing_TreeView_Items_Clears_Index() { + using var app = Start(); + // Issue #3551 var tree = CreateTestTreeData(); var target = new TreeView { - Template = CreateTreeViewTemplate(), Items = tree, }; - var root = new TestRoot(); - root.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); + Prepare(target); var rootNode = tree[0]; var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(rootNode); Assert.NotNull(container); + var root = (TestRoot)target.Parent; root.Child = null; tree.Clear(); @@ -1079,36 +950,54 @@ public void Clearing_TreeView_Items_Clears_Index() Assert.Empty(target.ItemContainerGenerator.Index.Containers); } - private void ApplyTemplates(TreeView tree) + private TestRoot Prepare(TreeView tree, bool createDataTemplates = true) { - tree.ApplyTemplate(); - tree.Presenter.ApplyTemplate(); - ApplyTemplates(tree.Presenter.Panel.Children); - } + var root = tree.GetVisualRoot() as ILayoutRoot ?? CreateRoot(tree); - private void ApplyTemplates(IEnumerable controls) - { - foreach (TreeViewItem control in controls) + if (createDataTemplates) { - control.Template = CreateTreeViewItemTemplate(); - control.ApplyTemplate(); - control.Presenter.ApplyTemplate(); - control.HeaderPresenter.ApplyTemplate(); - ApplyTemplates(control.Presenter.Panel.Children); + CreateNodeDataTemplate(tree); } + + root.LayoutManager.ExecuteInitialLayoutPass(); + + return tree.Parent as TestRoot; } - private TreeViewItem GetItem(TreeView target, params int[] indexes) + private void Layout(TreeView target) { - var c = (ItemsControl)target; + var root = (ILayoutRoot)target.GetVisualRoot(); + root.LayoutManager.ExecuteLayoutPass(); + } - foreach (var index in indexes) + private TestRoot CreateRoot(IControl child) + { + return new TestRoot { - var item = ((IList)c.Items)[index]; - c = (ItemsControl)target.ItemContainerGenerator.Index.ContainerFromItem(item); - } + Styles = + { + new Style(x => x.Is()) + { + Setters = + { + new Setter(TreeView.TemplateProperty, TreeViewTemplate()), + } + }, + new Style(x => x.OfType()) + { + Setters = + { + new Setter(TreeView.TemplateProperty, TreeViewItemTemplate()), + } + }, + }, + Child = child, + }; + } - return (TreeViewItem)c; + private TreeViewItem GetItem(TreeView target, params int[] indexes) + { + return target.TreeContainerFromIndex(new IndexPath(indexes)); } private IList CreateTestTreeData() @@ -1149,16 +1038,17 @@ private void CreateNodeDataTemplate(IControl control) control.DataTemplates.Add(new TestTreeDataTemplate()); } - private IControlTemplate CreateTreeViewTemplate() + private IControlTemplate TreeViewTemplate() { return new FuncControlTemplate((parent, scope) => new ItemsPresenter { Name = "PART_ItemsPresenter", [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], + [~ItemsPresenter.LayoutProperty] = parent[~ItemsControl.LayoutProperty], }.RegisterInNameScope(scope)); } - private IControlTemplate CreateTreeViewItemTemplate() + private IControlTemplate TreeViewItemTemplate() { return new FuncControlTemplate((parent, scope) => new Panel { @@ -1168,11 +1058,13 @@ private IControlTemplate CreateTreeViewItemTemplate() { Name = "PART_HeaderPresenter", [~ContentPresenter.ContentProperty] = parent[~TreeViewItem.HeaderProperty], + [~ContentPresenter.ContentTemplateProperty] = parent[~TreeViewItem.HeaderTemplateProperty], }.RegisterInNameScope(scope), new ItemsPresenter { Name = "PART_ItemsPresenter", [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], + [~ItemsPresenter.LayoutProperty] = parent[~ItemsControl.LayoutProperty], }.RegisterInNameScope(scope) } }); @@ -1180,28 +1072,27 @@ private IControlTemplate CreateTreeViewItemTemplate() private void ExpandAll(TreeView tree) { - foreach (var i in tree.ItemContainerGenerator.Containers) + foreach (var i in tree.Presenter.RealizedElements) { - tree.ExpandSubTree((TreeViewItem)i.ContainerControl); + tree.ExpandSubTree((TreeViewItem)i); } } private List ExtractItemHeader(TreeView tree, int level) { - return ExtractItemContent(tree.Presenter.Panel, 0, level) - .Select(x => x.Header) + return ExtractItemContent(tree.Presenter, 0, level) + .Select(x => x.HeaderPresenter.Child) .OfType() .Select(x => x.Text) .ToList(); } - private IEnumerable ExtractItemContent(IPanel panel, int currentLevel, int level) + private IEnumerable ExtractItemContent(IItemsPresenter presenter, int currentLevel, int level) { - foreach (TreeViewItem container in panel.Children) + foreach (TreeViewItem container in presenter.RealizedElements) { if (container.Template == null) { - container.Template = CreateTreeViewItemTemplate(); container.ApplyTemplate(); } @@ -1211,7 +1102,7 @@ private IEnumerable ExtractItemContent(IPanel panel, int currentLe } else { - foreach (var child in ExtractItemContent(container.Presenter.Panel, currentLevel + 1, level)) + foreach (var child in ExtractItemContent(container.Presenter, currentLevel + 1, level)) { yield return child; } @@ -1226,22 +1117,22 @@ private void ClickContainer(IControl container, KeyModifiers modifiers) private void AssertChildrenSelected(TreeView treeView, Node rootNode) { - foreach (var child in rootNode.Children) + for (var i = 0; i < rootNode.Children.Count; ++i) { - var container = (TreeViewItem)treeView.ItemContainerGenerator.Index.ContainerFromItem(child); - + var container = treeView.TreeContainerFromIndex(new IndexPath(0, i)); Assert.True(container.IsSelected); } } - private IDisposable Application() + private static IDisposable Start() { - return UnitTestApplication.Start( - TestServices.MockThreadingInterface.With( - focusManager: new FocusManager(), - keyboardDevice: () => new KeyboardDevice(), - keyboardNavigation: new KeyboardNavigationHandler(), - inputManager: new InputManager())); + var services = TestServices.MockPlatformRenderInterface.With( + focusManager: new FocusManager(), + keyboardDevice: () => new KeyboardDevice(), + keyboardNavigation: new KeyboardNavigationHandler(), + styler: new Styler(), + threadingInterface: TestServices.MockThreadingInterface.ThreadingInterface); + return UnitTestApplication.Start(services); } private class Node : NotifyingBase diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 0b812762400..acbaa868146 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -296,7 +296,7 @@ public void TreeView_Is_Freed() // Do a layout and make sure that TreeViewItems get realized. window.LayoutManager.ExecuteInitialLayoutPass(); - Assert.Single(target.ItemContainerGenerator.Containers); + Assert.Single(target.Presenter.RealizedElements); // Clear the content and ensure the TreeView is removed. window.Content = null; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 8a82ad048b3..df1e6b93764 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using System.Reactive.Subjects; using System.Text; using System.Threading.Tasks; @@ -425,7 +426,8 @@ public void InfersDataTemplateTypeFromParentCollectionItemsType() target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - Assert.Equal(dataContext.ListProperty[0], (string)((ContentPresenter)target.Presenter.Panel.Children[0]).Content); + var presenter = (ContentPresenter)target.Presenter.RealizedElements.First(); + Assert.Equal(dataContext.ListProperty[0], presenter.Content); } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs index 033b670bf4d..daa367d95f5 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.UnitTests; @@ -60,7 +61,7 @@ public void DataTemplate_Can_Contain_Named_UserControl() itemsControl.ApplyTemplate(); itemsControl.Presenter.ApplyTemplate(); - Assert.Equal(2, itemsControl.Presenter.Panel.Children.Count); + Assert.Equal(2, itemsControl.Presenter.RealizedElements.Count()); } } diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs index 53bdff5dff7..c9be84e035a 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs @@ -66,7 +66,11 @@ public void Should_Use_ViewModelViewHost_As_Data_Template_By_Default() var view = new ExampleView(); view.ViewModel.Items.Add(new NestedViewModel()); - var child = view.List.Presenter.Panel.Children[0]; + view.List.Template = GetTemplate(); + view.List.ApplyTemplate(); + view.List.Presenter.ApplyTemplate(); + + var child = view.List.Presenter.RealizedElements.First(); var container = (ContentPresenter) child; container.UpdateChild(); @@ -79,7 +83,11 @@ public void ViewModelViewHost_Should_Resolve_And_Embedd_Appropriate_View_Model() var view = new ExampleView(); view.ViewModel.Items.Add(new NestedViewModel()); - var child = view.List.Presenter.Panel.Children[0]; + view.List.Template = GetTemplate(); + view.List.ApplyTemplate(); + view.List.Presenter.ApplyTemplate(); + + var child = view.List.Presenter.RealizedElements.First(); var container = (ContentPresenter) child; container.UpdateChild(); @@ -106,7 +114,7 @@ public void Should_Not_Use_View_Model_View_Host_When_Item_Template_Is_Set() var view = new ExampleView(control => control.ItemTemplate = GetItemTemplate()); view.ViewModel.Items.Add(new NestedViewModel()); - var child = view.List.Presenter.Panel.Children[0]; + var child = view.List.Presenter.RealizedElements.ElementAt(0); var container = (ContentPresenter) child; container.UpdateChild(); @@ -119,7 +127,7 @@ public void Should_Not_Use_View_Model_View_Host_When_Data_Templates_Are_Not_Empt var view = new ExampleView(control => control.DataTemplates.Add(GetItemTemplate())); view.ViewModel.Items.Add(new NestedViewModel()); - var child = view.List.Presenter.Panel.Children[0]; + var child = view.List.Presenter.RealizedElements.ElementAt(0); var container = (ContentPresenter) child; container.UpdateChild(); From fe9898e967b414e466339149edb198f4d4b6c512 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 11 Aug 2020 18:09:45 +0200 Subject: [PATCH 03/39] Don't recycle inline items. Mark inline items (e.g. a `ListBoxItem` appearing directly in a `ListBox.Items` collection) as non-recyclable. --- samples/ControlCatalog/Pages/ListBoxPage.xaml | 38 ++++++++++++++++++ .../Generators/ItemContainerGenerator.cs | 40 ++++++++++++------- .../Generators/ItemContainerGenerator`1.cs | 2 + .../Generators/TreeItemContainerGenerator.cs | 2 + 4 files changed, 68 insertions(+), 14 deletions(-) diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index edf3d41bf54..47f678a885a 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -9,7 +9,45 @@ Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16"> + + + Inline items + + Item 0 + Item 1 + Item 2 + Item 3 + Item 4 + Item 5 + Item 6 + Item 7 + Item 8 + Item 9 + Item 10 + Item 11 + Item 12 + Item 13 + Item 14 + Item 15 + Item 16 + Item 17 + Item 18 + Item 19 + Item 20 + Item 21 + Item 22 + Item 23 + Item 24 + Item 25 + Item 26 + Item 27 + Item 28 + Item 29 + + + + Bound items with virtualization public class ItemContainerGenerator : IItemContainerGenerator { + private static readonly AttachedProperty PreventRecycleProperty = + AvaloniaProperty.RegisterAttached("PreventRecycle"); + private RecyclePool _recyclePool = new RecyclePool(); public ItemContainerGenerator(ItemsControl owner) @@ -30,33 +33,40 @@ public IControl Build(object param) public IControl GetElement(ElementFactoryGetArgs args) { - var result = _recyclePool.TryGetElement(string.Empty, args.Parent); - - if (result is null) + if (DataIsContainer(args.Data)) + { + var result = (Control)args.Data; + result.SetValue(PreventRecycleProperty, true); + return result; + } + else { - result = CreateContainer(args); + var result = _recyclePool.TryGetElement(string.Empty, args.Parent); - if (result.Parent == null) + if (result is null) { - ((ISetLogicalParent)result).SetParent(Owner); + result = CreateContainer(args); + + if (result.Parent == null) + { + ((ISetLogicalParent)result).SetParent(Owner); + } } - } - return result; + return result; + } } public void RecycleElement(ElementFactoryRecycleArgs args) { - _recyclePool.PutElement(args.Element, string.Empty, args.Parent); + if (!args.Element.GetValue(PreventRecycleProperty)) + { + _recyclePool.PutElement(args.Element, string.Empty, args.Parent); + } } protected virtual IControl CreateContainer(ElementFactoryGetArgs args) { - if (args.Data is IControl c) - { - return c; - } - var result = new ContentPresenter(); result.Bind( ContentPresenter.ContentProperty, @@ -73,5 +83,7 @@ protected virtual IControl CreateContainer(ElementFactoryGetArgs args) return result; } + + protected virtual bool DataIsContainer(object data) => data is Control; } } diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs index 5fea8c4c295..3cf87741b2b 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs @@ -59,5 +59,7 @@ protected override IControl CreateContainer(ElementFactoryGetArgs args) return result; } + + protected override bool DataIsContainer(object data) => data is T; } } diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index 3b8deae9f7d..0b22a756235 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -100,6 +100,8 @@ protected override IControl CreateContainer(ElementFactoryGetArgs args) return result; } + protected override bool DataIsContainer(object data) => data is T; + private ITreeDataTemplate GetTreeDataTemplate(object item, IDataTemplate? primary) { var template = Owner.FindDataTemplate(item, primary) ?? FuncDataTemplate.Default; From 7aafa7538ef115d20aad8702ca592145afc2cf9c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 11 Aug 2020 18:33:58 +0200 Subject: [PATCH 04/39] Removed IVirtualizingPanel. --- src/Avalonia.Controls/IVirtualizingPanel.cs | 84 ------ .../VirtualizingStackPanel.cs | 255 ------------------ .../VirtualizingStackPanelTests.cs | 225 ---------------- 3 files changed, 564 deletions(-) delete mode 100644 src/Avalonia.Controls/IVirtualizingPanel.cs delete mode 100644 src/Avalonia.Controls/VirtualizingStackPanel.cs delete mode 100644 tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs deleted file mode 100644 index 8c4dc809342..00000000000 --- a/src/Avalonia.Controls/IVirtualizingPanel.cs +++ /dev/null @@ -1,84 +0,0 @@ -using Avalonia.Layout; - -namespace Avalonia.Controls -{ - /// - /// A panel that can be used to virtualize items. - /// - public interface IVirtualizingPanel : IPanel - { - /// - /// Gets or sets the controller for the virtualizing panel. - /// - /// - /// A virtualizing controller is responsible for maintaining the controls in the virtualizing - /// panel. This property will be set by the controller when virtualization is initialized. - /// Note that this property may remain null if the panel is added to a control that does - /// not act as a virtualizing controller. - /// - IVirtualizingController Controller { get; set; } - - /// - /// Gets a value indicating whether the panel is full. - /// - /// - /// This property should return false until enough children are added to fill the space - /// passed into the last measure or arrange in the direction of scroll. It should be - /// updated immediately after a child is added or removed. - /// - bool IsFull { get; } - - /// - /// Gets the number of items that can be removed while keeping the panel full. - /// - /// - /// This property should return the number of children that are completely out of the - /// panel's current bounds in the direction of scroll. It should be updated after an - /// arrange. - /// - int OverflowCount { get; } - - /// - /// Gets the direction of scroll. - /// - Orientation ScrollDirection { get; } - - /// - /// Gets the average size of the materialized items in the direction of scroll. - /// - double AverageItemSize { get; } - - /// - /// Gets or sets a size in pixels by which the content is overflowing the panel, in the - /// direction of scroll. - /// - /// - /// This may be non-zero even when is zero if the last item - /// overflows the panel bounds. - /// - double PixelOverflow { get; } - - /// - /// Gets or sets the current pixel offset of the items in the direction of scroll. - /// - double PixelOffset { get; set; } - - /// - /// Gets or sets the current scroll offset in the cross axis. - /// - double CrossAxisOffset { get; set; } - - /// - /// Invalidates the measure of the control and forces a call to - /// on the next measure. - /// - /// - /// The implementation for this method should call - /// and also ensure that the next call to - /// calls - /// on the next measure even if - /// the available size hasn't changed. - /// - void ForceInvalidateMeasure(); - } -} diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs deleted file mode 100644 index da8bbe4fcf3..00000000000 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ /dev/null @@ -1,255 +0,0 @@ -using System; -using System.Collections.Specialized; -using Avalonia.Controls.Primitives; -using Avalonia.Input; -using Avalonia.Layout; - -namespace Avalonia.Controls -{ - public class VirtualizingStackPanel : StackPanel, IVirtualizingPanel - { - private Size _availableSpace; - private double _takenSpace; - private int _canBeRemoved; - private double _averageItemSize; - private int _averageCount; - private double _pixelOffset; - private double _crossAxisOffset; - private bool _forceRemeasure; - - bool IVirtualizingPanel.IsFull - { - get - { - return Orientation == Orientation.Horizontal ? - _takenSpace >= _availableSpace.Width : - _takenSpace >= _availableSpace.Height; - } - } - - IVirtualizingController IVirtualizingPanel.Controller { get; set; } - int IVirtualizingPanel.OverflowCount => _canBeRemoved; - Orientation IVirtualizingPanel.ScrollDirection => Orientation; - double IVirtualizingPanel.AverageItemSize => _averageItemSize; - - double IVirtualizingPanel.PixelOverflow - { - get - { - var bounds = Orientation == Orientation.Horizontal ? - _availableSpace.Width : _availableSpace.Height; - return Math.Max(0, _takenSpace - bounds); - } - } - - double IVirtualizingPanel.PixelOffset - { - get { return _pixelOffset; } - - set - { - if (_pixelOffset != value) - { - _pixelOffset = value; - InvalidateArrange(); - } - } - } - - double IVirtualizingPanel.CrossAxisOffset - { - get { return _crossAxisOffset; } - - set - { - if (_crossAxisOffset != value) - { - _crossAxisOffset = value; - InvalidateArrange(); - } - } - } - - private IVirtualizingController Controller => ((IVirtualizingPanel)this).Controller; - - void IVirtualizingPanel.ForceInvalidateMeasure() - { - InvalidateMeasure(); - _forceRemeasure = true; - } - - protected override Size MeasureOverride(Size availableSize) - { - if (_forceRemeasure || availableSize != ((ILayoutable)this).PreviousMeasure) - { - _forceRemeasure = false; - _availableSpace = availableSize; - Controller?.UpdateControls(); - } - - return base.MeasureOverride(availableSize); - } - - protected override Size ArrangeOverride(Size finalSize) - { - _availableSpace = finalSize; - _canBeRemoved = 0; - _takenSpace = 0; - _averageItemSize = 0; - _averageCount = 0; - var result = base.ArrangeOverride(finalSize); - _takenSpace += _pixelOffset; - Controller?.UpdateControls(); - return result; - } - - protected override void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) - { - base.ChildrenChanged(sender, e); - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - foreach (IControl control in e.NewItems) - { - UpdateAdd(control); - } - - break; - - case NotifyCollectionChangedAction.Remove: - foreach (IControl control in e.OldItems) - { - UpdateRemove(control); - } - - break; - } - } - - protected override IInputElement GetControlInDirection(NavigationDirection direction, IControl from) - { - if (from == null) - return null; - - var logicalScrollable = Parent as ILogicalScrollable; - - if (logicalScrollable?.IsLogicalScrollEnabled == true) - { - return logicalScrollable.GetControlInDirection(direction, from); - } - else - { - return base.GetControlInDirection(direction, from); - } - } - - internal override void ArrangeChild( - IControl child, - Rect rect, - Size panelSize, - Orientation orientation) - { - if (orientation == Orientation.Vertical) - { - rect = new Rect( - rect.X - _crossAxisOffset, - rect.Y - _pixelOffset, - rect.Width, - rect.Height); - child.Arrange(rect); - - if (rect.Y >= _availableSpace.Height) - { - ++_canBeRemoved; - } - - if (rect.Bottom >= _takenSpace) - { - _takenSpace = rect.Bottom; - } - - AddToAverageItemSize(rect.Height); - } - else - { - rect = new Rect( - rect.X - _pixelOffset, - rect.Y - _crossAxisOffset, - rect.Width, - rect.Height); - child.Arrange(rect); - - if (rect.X >= _availableSpace.Width) - { - ++_canBeRemoved; - } - - if (rect.Right >= _takenSpace) - { - _takenSpace = rect.Right; - } - - AddToAverageItemSize(rect.Width); - } - } - - private void UpdateAdd(IControl child) - { - var bounds = Bounds; - var spacing = Spacing; - - child.Measure(_availableSpace); - ++_averageCount; - - if (Orientation == Orientation.Vertical) - { - var height = child.DesiredSize.Height; - _takenSpace += height + spacing; - AddToAverageItemSize(height); - } - else - { - var width = child.DesiredSize.Width; - _takenSpace += width + spacing; - AddToAverageItemSize(width); - } - } - - private void UpdateRemove(IControl child) - { - var bounds = Bounds; - var spacing = Spacing; - - if (Orientation == Orientation.Vertical) - { - var height = child.DesiredSize.Height; - _takenSpace -= height + spacing; - RemoveFromAverageItemSize(height); - } - else - { - var width = child.DesiredSize.Width; - _takenSpace -= width + spacing; - RemoveFromAverageItemSize(width); - } - - if (_canBeRemoved > 0) - { - --_canBeRemoved; - } - } - - private void AddToAverageItemSize(double value) - { - ++_averageCount; - _averageItemSize += (value - _averageItemSize) / _averageCount; - } - - private void RemoveFromAverageItemSize(double value) - { - _averageItemSize = ((_averageItemSize * _averageCount) - value) / (_averageCount - 1); - --_averageCount; - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs deleted file mode 100644 index a319a95e1fc..00000000000 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ /dev/null @@ -1,225 +0,0 @@ -using Avalonia.Controls.Primitives; -using Avalonia.Input; -using Avalonia.LogicalTree; -using Moq; -using Xunit; - -namespace Avalonia.Controls.UnitTests -{ - public class VirtualizingStackPanelTests - { - public class Vertical - { - [Fact] - public void Measure_Invokes_Controller_UpdateControls() - { - var target = (IVirtualizingPanel)new VirtualizingStackPanel(); - var controller = new Mock(); - - target.Controller = controller.Object; - target.Measure(new Size(100, 100)); - - controller.Verify(x => x.UpdateControls(), Times.Once()); - } - - [Fact] - public void Measure_Invokes_Controller_UpdateControls_If_AvailableSize_Changes() - { - var target = (IVirtualizingPanel)new VirtualizingStackPanel(); - var controller = new Mock(); - - target.Controller = controller.Object; - target.Measure(new Size(100, 100)); - target.InvalidateMeasure(); - target.Measure(new Size(100, 100)); - target.InvalidateMeasure(); - target.Measure(new Size(100, 101)); - - controller.Verify(x => x.UpdateControls(), Times.Exactly(2)); - } - - [Fact] - public void Measure_Does_Not_Invoke_Controller_UpdateControls_If_AvailableSize_Is_The_Same() - { - var target = (IVirtualizingPanel)new VirtualizingStackPanel(); - var controller = new Mock(); - - target.Controller = controller.Object; - target.Measure(new Size(100, 100)); - target.InvalidateMeasure(); - target.Measure(new Size(100, 100)); - - controller.Verify(x => x.UpdateControls(), Times.Once()); - } - - [Fact] - public void Measure_Invokes_Controller_UpdateControls_If_AvailableSize_Is_The_Same_After_ForceInvalidateMeasure() - { - var target = (IVirtualizingPanel)new VirtualizingStackPanel(); - var controller = new Mock(); - - target.Controller = controller.Object; - target.Measure(new Size(100, 100)); - target.ForceInvalidateMeasure(); - target.Measure(new Size(100, 100)); - - controller.Verify(x => x.UpdateControls(), Times.Exactly(2)); - } - - [Fact] - public void Arrange_Invokes_Controller_UpdateControls() - { - var target = (IVirtualizingPanel)new VirtualizingStackPanel(); - var controller = new Mock(); - - target.Controller = controller.Object; - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 110, 110)); - - controller.Verify(x => x.UpdateControls(), Times.Exactly(2)); - } - - [Fact] - public void Reports_IsFull_False_Until_Measure_Height_Is_Reached() - { - var target = (IVirtualizingPanel)new VirtualizingStackPanel(); - - target.Measure(new Size(100, 100)); - - Assert.Equal(new Size(0, 0), target.DesiredSize); - Assert.Equal(new Size(0, 0), target.Bounds.Size); - - Assert.False(target.IsFull); - Assert.Equal(0, target.OverflowCount); - target.Children.Add(new Canvas { Width = 50, Height = 50 }); - Assert.False(target.IsFull); - Assert.Equal(0, target.OverflowCount); - target.Children.Add(new Canvas { Width = 50, Height = 50 }); - Assert.True(target.IsFull); - Assert.Equal(0, target.OverflowCount); - } - - [Fact] - public void Reports_Overflow_After_Arrange() - { - var target = (IVirtualizingPanel)new VirtualizingStackPanel(); - - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(target.DesiredSize)); - - Assert.Equal(new Size(0, 0), target.Bounds.Size); - - target.Children.Add(new Canvas { Width = 50, Height = 50 }); - target.Children.Add(new Canvas { Width = 50, Height = 50 }); - target.Children.Add(new Canvas { Width = 50, Height = 50 }); - target.Children.Add(new Canvas { Width = 50, Height = 50 }); - Assert.Equal(0, target.OverflowCount); - - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(target.DesiredSize)); - - Assert.Equal(2, target.OverflowCount); - } - - [Fact] - public void Reports_Correct_Overflow_During_Arrange() - { - var target = (IVirtualizingPanel)new VirtualizingStackPanel(); - var controller = new Mock(); - var called = false; - - target.Children.Add(new Canvas { Width = 50, Height = 50 }); - target.Children.Add(new Canvas { Width = 50, Height = 52 }); - target.Measure(new Size(100, 100)); - - controller.Setup(x => x.UpdateControls()).Callback(() => - { - Assert.Equal(2, target.PixelOverflow); - Assert.Equal(0, target.OverflowCount); - called = true; - }); - - target.Controller = controller.Object; - target.Arrange(new Rect(target.DesiredSize)); - - Assert.True(called); - } - - [Fact] - public void Reports_PixelOverflow_After_Arrange() - { - var target = (IVirtualizingPanel)new VirtualizingStackPanel(); - - target.Children.Add(new Canvas { Width = 50, Height = 50 }); - target.Children.Add(new Canvas { Width = 50, Height = 52 }); - - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(target.DesiredSize)); - - Assert.Equal(2, target.PixelOverflow); - } - - [Fact] - public void Reports_PixelOverflow_After_Arrange_Smaller_Than_Measure() - { - var target = (IVirtualizingPanel)new VirtualizingStackPanel(); - - target.Children.Add(new Canvas { Width = 50, Height = 50 }); - target.Children.Add(new Canvas { Width = 50, Height = 52 }); - - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 50, 50)); - - Assert.Equal(52, target.PixelOverflow); - } - - [Fact] - public void Reports_PixelOverflow_With_PixelOffset() - { - var target = (IVirtualizingPanel)new VirtualizingStackPanel(); - - target.Children.Add(new Canvas { Width = 50, Height = 50 }); - target.Children.Add(new Canvas { Width = 50, Height = 52 }); - target.PixelOffset = 2; - - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(target.DesiredSize)); - - Assert.Equal(2, target.PixelOverflow); - } - - [Fact] - public void PixelOffset_Can_Be_More_Than_Child_Without_Affecting_IsFull() - { - var target = (IVirtualizingPanel)new VirtualizingStackPanel(); - - target.Children.Add(new Canvas { Width = 50, Height = 50 }); - target.Children.Add(new Canvas { Width = 50, Height = 52 }); - target.PixelOffset = 55; - - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(target.DesiredSize)); - - Assert.Equal(55, target.PixelOffset); - Assert.Equal(2, target.PixelOverflow); - Assert.True(target.IsFull); - } - - [Fact] - public void Passes_Navigation_Request_To_ILogicalScrollable_Parent() - { - var presenter = new Mock().As(); - var scrollable = presenter.As(); - var target = (IVirtualizingPanel)new VirtualizingStackPanel(); - var from = new Canvas(); - - scrollable.Setup(x => x.IsLogicalScrollEnabled).Returns(true); - - ((ISetLogicalParent)target).SetParent(presenter.Object); - ((INavigableContainer)target).GetControl(NavigationDirection.Next, from, false); - - scrollable.Verify(x => x.GetControlInDirection(NavigationDirection.Next, from)); - } - } - } -} \ No newline at end of file From 98a757c50e87f3da4eb958b78b772ca3ae9a8ce2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 14 Aug 2020 16:15:19 +0200 Subject: [PATCH 05/39] Tweaked selection. - Move interaction logic out of `SelectingItemsControl` - Don't select item when getting focus - Use ctrl+direction to move focus without changing selection - Bring focused control into view --- src/Avalonia.Controls/Control.cs | 12 ++- src/Avalonia.Controls/ListBox.cs | 59 +++++++++++---- .../Primitives/SelectingItemsControl.cs | 74 ++++++++++++++----- src/Avalonia.Input/NavigationDirection.cs | 12 +++ .../ListBoxTests.cs | 13 +++- .../ListBoxTests_Multiple.cs | 65 ++++++++++------ .../ListBoxTests_Selection.cs | 46 ++++++------ 7 files changed, 196 insertions(+), 85 deletions(-) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 41370d8464b..7c15788de25 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -155,9 +155,15 @@ protected override void OnGotFocus(GotFocusEventArgs e) { base.OnGotFocus(e); - if (IsFocused && - (e.NavigationMethod == NavigationMethod.Tab || - e.NavigationMethod == NavigationMethod.Directional)) + if (!IsFocused) + { + return; + } + + this.BringIntoView(); + + if (e.NavigationMethod == NavigationMethod.Tab || + e.NavigationMethod == NavigationMethod.Directional) { var adornerLayer = AdornerLayer.GetAdornerLayer(this); diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index ab3481cc452..ebf57178fb9 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -1,8 +1,11 @@ using System.Collections; +using System.Collections.Generic; +using System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Controls.Selection; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Layout; using Avalonia.VisualTree; @@ -40,6 +43,7 @@ public class ListBox : SelectingItemsControl SelectingItemsControl.SelectionModeProperty; private IScrollable? _scroll; + private bool _rangeSelecting; static ListBox() { @@ -51,8 +55,8 @@ static ListBox() /// public IScrollable? Scroll { - get { return _scroll; } - private set { SetAndRaise(ScrollProperty, ref _scroll, value); } + get => _scroll; + private set => SetAndRaise(ScrollProperty, ref _scroll, value); } /// @@ -78,8 +82,8 @@ public IScrollable? Scroll /// public new SelectionMode SelectionMode { - get { return base.SelectionMode; } - set { base.SelectionMode = value; } + get => base.SelectionMode; + set => base.SelectionMode = value; } /// @@ -92,7 +96,6 @@ public IScrollable? Scroll /// public void UnselectAll() => Selection.Clear(); - /// protected override IItemContainerGenerator CreateItemContainerGenerator() { return new ItemContainerGenerator( @@ -101,22 +104,48 @@ protected override IItemContainerGenerator CreateItemContainerGenerator() ListBoxItem.ContentTemplateProperty); } - /// - protected override void OnGotFocus(GotFocusEventArgs e) + protected override void OnKeyDown(KeyEventArgs e) { - base.OnGotFocus(e); + if (!e.Handled) + { + var direction = e.Key.ToNavigationDirection(); + var ctrl = e.KeyModifiers.HasFlagCustom(KeyModifiers.Control); + var shift = e.KeyModifiers.HasFlagCustom(KeyModifiers.Shift); + + if (direction.HasValue && (!ctrl || shift)) + { + try + { + _rangeSelecting = shift; + e.Handled = MoveSelection( + direction.Value, + shift); + } + finally + { + _rangeSelecting = false; + } + } + } - if (e.NavigationMethod == NavigationMethod.Directional) + if (!e.Handled) { - e.Handled = UpdateSelectionFromEventSource( - e.Source, - true, - (e.KeyModifiers & KeyModifiers.Shift) != 0, - (e.KeyModifiers & KeyModifiers.Control) != 0); + var keymap = AvaloniaLocator.Current.GetService(); + bool Match(List gestures) => gestures.Any(g => g.Matches(e)); + + if (ItemCount > 0 && + Match(keymap.SelectAll) && + (((SelectionMode & SelectionMode.Multiple) != 0) || + (SelectionMode & SelectionMode.Toggle) != 0)) + { + Selection.SelectAll(); + e.Handled = true; + } } + + base.OnKeyDown(e); } - /// protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index ddfd4a1149c..b6132a69646 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -285,6 +285,33 @@ public override void EndInit() return null; } + /// + /// Tries to get the index of the container that was the source of an event. + /// + /// The control that raised the event. + /// The container index or -1 if the event did not originate in a container. + protected int GetContainerIndexFromEventSource(IInteractive eventSource) + { + var parent = (IVisual)eventSource; + + while (parent != null) + { + if (parent is IControl control && control.LogicalParent == this) + { + var index = GetContainerIndex(control); + + if (index != -1) + { + return index; + } + } + + parent = parent.VisualParent; + } + + return -1; + } + protected override void ItemsViewChanged(ItemsSourceView? oldView, ItemsSourceView newView) { base.ItemsViewChanged(oldView, newView); @@ -327,6 +354,11 @@ protected override void OnContainerClearing(ElementClearingEventArgs e) if (Presenter is InputElement inputElement) { + if (e.Element.IsFocused) + { + this.Focus(); + } + if (KeyboardNavigation.GetTabOnceActiveElement(inputElement) == e.Element) { KeyboardNavigation.SetTabOnceActiveElement(inputElement, null); @@ -427,31 +459,35 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs /// Moves the selection in the specified direction relative to the current selection. /// /// The direction to move. - /// Whether to wrap when the selection reaches the first or last item. + /// Whether the range modifier is enabled (i.e. shift key). /// True if the selection was moved; otherwise false. - protected bool MoveSelection(NavigationDirection direction, bool wrap) + protected bool MoveSelection( + NavigationDirection direction, + bool rangeModifier) { - var from = SelectedIndex != -1 ? TryGetContainer(SelectedIndex) : null; - return MoveSelection(from, direction, wrap); - } + if (direction == NavigationDirection.First || direction == NavigationDirection.Last) + { + var index = direction == NavigationDirection.First ? 0 : ItemCount - 1; + UpdateSelection(index, true, rangeModifier); + ScrollIntoView(index); - /// - /// Moves the selection in the specified direction relative to the specified container. - /// - /// The container which serves as a starting point for the movement. - /// The direction to move. - /// Whether to wrap when the selection reaches the first or last item. - /// True if the selection was moved; otherwise false. - protected bool MoveSelection(IControl? from, NavigationDirection direction, bool wrap) - { - if (Presenter is INavigableContainer container && - GetNextControl(container, direction, from, wrap) is IControl next) + var c = TryGetContainer(index); + + if (c is object) + { + System.Diagnostics.Debug.WriteLine(c.DataContext); + FocusManager.Instance?.Focus(c, direction.ToNavigationMethod()); + } + } + else { - var index = GetContainerIndex(next); + var focus = FocusManager.Instance; + var next = focus?.FindNextElement(direction); - if (index != -1) + if (next is IControl c && GetContainerIndex(c) != -1) { - SelectedIndex = index; + UpdateSelection(c, true, rangeModifier); + focus?.Focus(c, direction.ToNavigationMethod()); return true; } } diff --git a/src/Avalonia.Input/NavigationDirection.cs b/src/Avalonia.Input/NavigationDirection.cs index 0c4a4921817..b3047a2ff98 100644 --- a/src/Avalonia.Input/NavigationDirection.cs +++ b/src/Avalonia.Input/NavigationDirection.cs @@ -110,5 +110,17 @@ public static bool IsDirectional(this NavigationDirection direction) return null; } } + + /// + /// Converts a into a . + /// + /// The navigation direction. + public static NavigationMethod ToNavigationMethod( + this NavigationDirection direction) + { + return direction <= NavigationDirection.Previous ? + NavigationMethod.Tab : + NavigationMethod.Directional; + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index c88b73fa410..56d42e3de7b 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -360,13 +360,14 @@ public void Clicking_Item_Should_Raise_BringIntoView_For_Correct_Control() // Click item 9. _mouse.Click(item); - Assert.Equal(1, raised); + Assert.Equal(2, raised); } private static IDisposable Start() { var services = TestServices.MockPlatformRenderInterface.With( focusManager: new FocusManager(), + keyboardDevice: () => new KeyboardDevice(), styler: new Styler(), windowingPlatform: new MockWindowingPlatform()); return UnitTestApplication.Start(services); @@ -407,6 +408,16 @@ private void Layout(ListBox target) root.LayoutManager.ExecuteLayoutPass(); } + private void KeyDown(IControl target, Key key, KeyModifiers modifiers = KeyModifiers.None) + { + target.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = key, + KeyModifiers = modifiers, + }); + } + private static FuncControlTemplate ListBoxTemplate() { return new FuncControlTemplate((parent, scope) => diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs index 1c411225498..89330c5c7ae 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs @@ -7,7 +7,7 @@ namespace Avalonia.Controls.UnitTests public partial class ListBoxTests { [Fact] - public void Focusing_Item_With_Shift_And_Arrow_Key_Should_Add_To_Selection() + public void Shift_And_Arrow_Key_Should_Add_To_Selection() { using var app = Start(); @@ -20,19 +20,18 @@ public void Focusing_Item_With_Shift_And_Arrow_Key_Should_Add_To_Selection() Prepare(target); target.SelectedItem = "Foo"; + target.TryGetContainer(0).Focus(); - target.Presenter.RealizedElements.ElementAt(1).RaiseEvent(new GotFocusEventArgs - { - RoutedEvent = InputElement.GotFocusEvent, - NavigationMethod = NavigationMethod.Directional, - KeyModifiers = KeyModifiers.Shift - }); + KeyDown(target, Key.Down, KeyModifiers.Shift); Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); + Assert.NotNull(target.TryGetContainer(1)); + Assert.Same(target.TryGetContainer(1), FocusManager.Instance.Current); + Assert.Equal(0, target.Selection.AnchorIndex); } [Fact] - public void Focusing_Item_With_Ctrl_And_Arrow_Key_Should_Add_To_Selection() + public void Ctrl_And_Arrow_Key_Should_Move_Focus_But_Not_Change_Selection() { using var app = Start(); @@ -45,22 +44,45 @@ public void Focusing_Item_With_Ctrl_And_Arrow_Key_Should_Add_To_Selection() Prepare(target); target.SelectedItem = "Foo"; + target.TryGetContainer(0).Focus(); + + KeyDown(target, Key.Down, KeyModifiers.Control); + + Assert.Equal(new[] { "Foo" }, target.SelectedItems); + Assert.NotNull(target.TryGetContainer(1)); + Assert.Same(target.TryGetContainer(1), FocusManager.Instance.Current); + } - target.Presenter.RealizedElements.ElementAt(1).RaiseEvent(new GotFocusEventArgs + [Fact] + public void Ctrl_Shift_And_Arrow_Key_Should_Add_To_Selection() + { + using var app = Start(); + + var target = new ListBox { - RoutedEvent = InputElement.GotFocusEvent, - NavigationMethod = NavigationMethod.Directional, - KeyModifiers = KeyModifiers.Control - }); + Items = new[] { "Foo", "Bar", "Baz " }, + SelectionMode = SelectionMode.Multiple + }; + + Prepare(target); + + target.SelectedItem = "Foo"; + target.TryGetContainer(0).Focus(); + + KeyDown(target, Key.Down, KeyModifiers.Control | KeyModifiers.Shift); Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); + Assert.NotNull(target.TryGetContainer(1)); + Assert.Same(target.TryGetContainer(1), FocusManager.Instance.Current); + Assert.Equal(0, target.Selection.AnchorIndex); } + [Fact] - public void Focusing_Selected_Item_With_Ctrl_And_Arrow_Key_Should_Remove_From_Selection() + public void SelectAll_Gesture_Should_Select_All_Items() { using var app = Start(); - + var target = new ListBox { Items = new[] { "Foo", "Bar", "Baz " }, @@ -69,17 +91,12 @@ public void Focusing_Selected_Item_With_Ctrl_And_Arrow_Key_Should_Remove_From_Se Prepare(target); - target.SelectedItems.Add("Foo"); - target.SelectedItems.Add("Bar"); + target.SelectedItem = "Foo"; + target.TryGetContainer(0).Focus(); - target.Presenter.RealizedElements.ElementAt(0).RaiseEvent(new GotFocusEventArgs - { - RoutedEvent = InputElement.GotFocusEvent, - NavigationMethod = NavigationMethod.Directional, - KeyModifiers = KeyModifiers.Control - }); + KeyDown(target, Key.A, KeyModifiers.Control); - Assert.Equal(new[] { "Bar" }, target.SelectedItems); + Assert.Equal(3, target.Selection.SelectedIndexes.Count); } } } diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Selection.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Selection.cs index b18c874ad01..8afbf6687aa 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Selection.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Selection.cs @@ -11,28 +11,7 @@ namespace Avalonia.Controls.UnitTests public partial class ListBoxTests { [Fact] - public void Focusing_Item_With_Tab_Should_Not_Select_It() - { - using var app = Start(); - - var target = new ListBox - { - Items = new[] { "Foo", "Bar", "Baz " }, - }; - - Prepare(target); - - target.Presenter.RealizedElements.First().RaiseEvent(new GotFocusEventArgs - { - RoutedEvent = InputElement.GotFocusEvent, - NavigationMethod = NavigationMethod.Tab, - }); - - Assert.Equal(-1, target.SelectedIndex); - } - - [Fact] - public void Focusing_Item_With_Arrow_Key_Should_Select_It() + public void Focusing_Item_Should_Not_Select_It() { using var app = Start(); @@ -49,7 +28,7 @@ public void Focusing_Item_With_Arrow_Key_Should_Select_It() NavigationMethod = NavigationMethod.Directional, }); - Assert.Equal(0, target.SelectedIndex); + Assert.Equal(-1, target.SelectedIndex); } [Fact] @@ -161,6 +140,27 @@ public void Clicking_Another_Item_Should_Select_It_When_SelectionMode_Toggle() Assert.Equal(0, target.SelectedIndex); } + [Fact] + public void Down_Key_Should_Select_Next_Item() + { + using var app = Start(); + + var target = new ListBox + { + Items = new[] { "Foo", "Bar", "Baz " }, + }; + + Prepare(target); + _mouse.Click(target.Presenter.RealizedElements.First()); + + Assert.Equal(0, target.SelectedIndex); + + KeyDown(target, Key.Down); + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal(1, target.Selection.AnchorIndex); + } + [Fact] public void Setting_Item_IsSelected_Sets_ListBox_Selection() { From 4e2c128ea64688ecb4a8179341df4a27f1479453 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 7 Sep 2020 23:07:23 +0200 Subject: [PATCH 06/39] Don't clear TabOnceActiveElement on clearing. `ItemsRepeater` should handle this for us (though doesn't always seem to get it right, due to a bug I think). --- .../Primitives/SelectingItemsControl.cs | 14 ----------- .../Primitives/SelectingItemsControlTests.cs | 23 ------------------- 2 files changed, 37 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index b6132a69646..ec3cac19ed8 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -351,19 +351,6 @@ protected override void OnContainerClearing(ElementClearingEventArgs e) { base.OnContainerClearing(e); MarkContainerSelected(e.Element, false); - - if (Presenter is InputElement inputElement) - { - if (e.Element.IsFocused) - { - this.Focus(); - } - - if (KeyboardNavigation.GetTabOnceActiveElement(inputElement) == e.Element) - { - KeyboardNavigation.SetTabOnceActiveElement(inputElement, null); - } - } } protected override void OnContainerIndexChanged(ElementIndexChangedEventArgs e) @@ -475,7 +462,6 @@ protected bool MoveSelection( if (c is object) { - System.Diagnostics.Debug.WriteLine(c.DataContext); FocusManager.Instance?.Focus(c, direction.ToNavigationMethod()); } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index cc615e01f46..1f3a9ef6dea 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -925,29 +925,6 @@ public void Setting_SelectedItem_With_Pointer_Should_Set_TabOnceActiveElement() selected); } - [Fact] - public void Removing_SelectedItem_Should_Clear_TabOnceActiveElement() - { - var items = new ObservableCollection(new[] { "Foo", "Bar", "Baz " }); - - var target = new ListBox - { - Template = Template(), - Items = items, - }; - - Prepare(target); - - var selected = target.Presenter.RealizedElements.ElementAt(1); - _helper.Down(selected); - - items.RemoveAt(1); - - var presenter = (InputElement)target.Presenter; - - Assert.Null(KeyboardNavigation.GetTabOnceActiveElement(presenter)); - } - [Fact] public void Resetting_Items_Collection_Should_Retain_Selection() { From 6015c7c2fdbdacffff271345ee63134b8842827c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 8 Sep 2020 10:07:09 +0200 Subject: [PATCH 07/39] Fix remaining failing TreeView tests. --- .../Generators/TreeContainerIndex.cs | 19 ----------------- .../Generators/TreeItemContainerGenerator.cs | 18 ++++++++++++---- src/Avalonia.Controls/TreeViewItem.cs | 21 ++++++++++++++++--- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs index f0da370f73a..ba09a58263f 100644 --- a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs +++ b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs @@ -68,25 +68,6 @@ public void Remove(IControl container) new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0))); } - /// - /// Removes a set of containers from the index. - /// - /// The index of the first item. - /// The item containers. - public void Remove(int startingIndex, IEnumerable containers) - { - foreach (var container in containers) - { - var item = _containerToItem[container.ContainerControl]; - _containerToItem.Remove(container.ContainerControl); - _itemToContainer.Remove(item); - } - - Dematerialized?.Invoke( - this, - new ItemContainerEventArgs(startingIndex, containers.ToList())); - } - /// /// Gets the container for an item. /// diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index 0b22a756235..df60cdd8e77 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Linq; +using System.Reactive.Linq; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.LogicalTree; @@ -72,6 +73,7 @@ public void UpdateIndex() } } } + protected override IControl CreateContainer(ElementFactoryGetArgs args) { if (args.Data is T c) @@ -80,8 +82,16 @@ protected override IControl CreateContainer(ElementFactoryGetArgs args) } var result = new T(); - var template = GetTreeDataTemplate(args.Data, Owner.ItemTemplate); - var itemsSelector = template.ItemsSelector(args.Data); + var dataCapture = args.Data; + var itemsSelector = result.GetObservable(Control.DataContextProperty) + .Select(x => + { + var template = GetTreeDataTemplate(x, Owner.ItemTemplate); + var itemsSelector = template.ItemsSelector(dataCapture); + return itemsSelector?.Observable ?? + Observable.Never().StartWith(itemsSelector?.Value); + }) + .Switch(); result.Bind( HeaderProperty, @@ -94,7 +104,7 @@ protected override IControl CreateContainer(ElementFactoryGetArgs args) if (itemsSelector != null) { - BindingOperations.Apply(result, ItemsProperty, itemsSelector, null); + result.Bind(ItemsProperty, itemsSelector); } return result; @@ -102,7 +112,7 @@ protected override IControl CreateContainer(ElementFactoryGetArgs args) protected override bool DataIsContainer(object data) => data is T; - private ITreeDataTemplate GetTreeDataTemplate(object item, IDataTemplate? primary) + private ITreeDataTemplate GetTreeDataTemplate(object? item, IDataTemplate? primary) { var template = Owner.FindDataTemplate(item, primary) ?? FuncDataTemplate.Default; var treeTemplate = template as ITreeDataTemplate ?? new WrapperTreeDataTemplate(template); diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index bdf1683b723..42bbbc3a0d9 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -123,7 +123,7 @@ protected override void OnContainerClearing(ElementClearingEventArgs e) { base.OnContainerClearing(e); - ItemContainerGenerator.Index.Remove(e.Element); + ItemContainerGenerator.Index?.Remove(e.Element); TreeView?.RaiseTreeContainerClearing(e); if (e.Element is TreeViewItem item) @@ -162,7 +162,7 @@ protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs { base.OnDetachedFromLogicalTree(e); - ItemContainerGenerator.UpdateIndex(); + UpdateIndex(); var (_, owner) = FindOwner(); @@ -246,9 +246,24 @@ private void OnParentChanged(AvaloniaPropertyChangedEventArgs e) // If we're not attached to the logical tree, then OnDetachedFromLogicalTree isn't going to be // called when the item is removed. This results in the item not being removed from the index, // causing #3551. In this case, update the index when Parent is changed to null. - ItemContainerGenerator.UpdateIndex(); + UpdateIndex(); TreeView = null; } } + + private void UpdateIndex() + { + var index = ItemContainerGenerator.Index; + + ItemContainerGenerator.UpdateIndex(); + + if (ItemContainerGenerator.Index != index) + { + foreach (var c in Presenter.RealizedElements) + { + index.Remove(c); + } + } + } } } From 2f87558f420dd94865082e6c2a6cfe00d1bf47a1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 8 Sep 2020 18:28:42 +0200 Subject: [PATCH 08/39] Make DevTools tree update again. We need to make `ItemsSourceView` handle `IReadOnlyList` as well as plain `IList`. This however exposed a problem where `CollectionChanged` events were still not getting to their handlers in the correct order, so did a bit of refactoring of `ItemsSourceView`. --- src/Avalonia.Controls/ItemsControl.cs | 11 +- src/Avalonia.Controls/ItemsSourceView.cs | 171 ++++++++++++++---- .../Selection/SelectionNodeBase.cs | 2 +- 3 files changed, 136 insertions(+), 48 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index a83c16f003e..611f1e0b40d 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -377,15 +377,8 @@ private void CreateItemsView() _itemsView?.Dispose(); } - if (_items is object) - { - _itemsView = new ItemsSourceView(_items); - _itemsView.AddListener(this); - } - else - { - _itemsView = ItemsSourceView.Empty; - } + _itemsView = ItemsSourceView.GetOrCreateOrEmpty(_items)!; + _itemsView.AddListener(this); ItemsViewChanged(oldView, _itemsView); RaisePropertyChanged(ItemsViewProperty, oldView, _itemsView); diff --git a/src/Avalonia.Controls/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs index b77663f335c..6235d28b69e 100644 --- a/src/Avalonia.Controls/ItemsSourceView.cs +++ b/src/Avalonia.Controls/ItemsSourceView.cs @@ -25,15 +25,20 @@ namespace Avalonia.Controls /// view of the Items. That way, each component does not need to know if the source is an /// IEnumerable, an IList, or something else. /// - public class ItemsSourceView : INotifyCollectionChanged, IDisposable, IReadOnlyList + public class ItemsSourceView : INotifyCollectionChanged, + IDisposable, + IReadOnlyList, + ICollectionChangedListener { /// /// Gets an empty /// - public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty()); + public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty()); - private protected readonly IList _inner; - private INotifyCollectionChanged? _notifyCollectionChanged; + private readonly IList? _list; + private readonly IReadOnlyList? _readOnlyList; + private readonly INotifyCollectionChanged? _incc; + private NotifyCollectionChangedEventHandler? _collectionChanged; /// /// Initializes a new instance of the ItemsSourceView class for the specified data source. @@ -43,26 +48,35 @@ public ItemsSourceView(IEnumerable source) { source = source ?? throw new ArgumentNullException(nameof(source)); + if (source is ItemsSourceView) + { + throw new InvalidOperationException("Cannot wrap an ItemsSourceView in another."); + } + if (source is IList list) { - _inner = list; + _list = list; + _incc = source as INotifyCollectionChanged; } - else if (source is IEnumerable objectEnumerable) + else if (source is IReadOnlyList readOnlyList) { - _inner = new List(objectEnumerable); + _readOnlyList = readOnlyList; + _incc = source as INotifyCollectionChanged; + } + else if (source is IEnumerable objectEnumerable) + { + _list = new List(objectEnumerable); } else { - _inner = new List(source.Cast()); + _list = new List(source.Cast()); } - - ListenToCollectionChanges(); } /// /// Gets the number of items in the collection. /// - public int Count => _inner.Count; + public int Count => _list?.Count ?? _readOnlyList!.Count; /// /// Gets a value that indicates whether the items source can provide a unique key for each item. @@ -82,15 +96,43 @@ public ItemsSourceView(IEnumerable source) /// /// Occurs when the collection has changed to indicate the reason for the change and which items changed. /// - public event NotifyCollectionChangedEventHandler? CollectionChanged; + public event NotifyCollectionChangedEventHandler? CollectionChanged + { + add + { + if (_incc is object) + { + if (_collectionChanged is null) + { + CollectionChangedEventManager.Instance.AddListener(_incc, this); + } + + _collectionChanged += value; + } + } + remove + { + if (_incc is object && _collectionChanged is object) + { + _collectionChanged -= value; + + if (_collectionChanged is null && _incc is object) + { + CollectionChangedEventManager.Instance.RemoveListener(_incc, this); + } + } + } + } /// public void Dispose() { - if (_notifyCollectionChanged != null) + if (_collectionChanged is object && _incc is object) { - _notifyCollectionChanged.CollectionChanged -= OnCollectionChanged; + CollectionChangedEventManager.Instance.RemoveListener(_incc, this); } + + _collectionChanged = null; } /// @@ -98,11 +140,43 @@ public void Dispose() /// /// The index. /// The item. - public object? GetAt(int index) => _inner[index]; + public object? GetAt(int index) => _readOnlyList is object ? _readOnlyList[index] : _list![index]; - public int IndexOf(object? item) => _inner.IndexOf(item); + /// + /// Gets the index of the specified item in the collection. + /// + /// The item. + /// The index of the item if -1 if not present. + public int IndexOf(object? item) => _readOnlyList is object ? + _readOnlyList.IndexOf(item) : _list!.IndexOf(item); - public static ItemsSourceView GetOrCreate(IEnumerable? items) + /// + /// Gets an for the source items, or null if + /// is null. + /// + /// The source items. + public static ItemsSourceView? GetOrCreate(IEnumerable? items) + { + if (items is ItemsSourceView isv) + { + return isv; + } + else if (items is null) + { + return null; + } + else + { + return new ItemsSourceView(items); + } + } + + /// + /// Gets an for the source items, or if + /// is null. + /// + /// The source items. + public static ItemsSourceView GetOrCreateOrEmpty(IEnumerable? items) { if (items is ItemsSourceView isv) { @@ -146,7 +220,7 @@ public int IndexFromKey(string key) internal void AddListener(ICollectionChangedListener listener) { - if (_inner is INotifyCollectionChanged incc) + if (_list is INotifyCollectionChanged incc) { CollectionChangedEventManager.Instance.AddListener(incc, listener); } @@ -154,39 +228,35 @@ internal void AddListener(ICollectionChangedListener listener) internal void RemoveListener(ICollectionChangedListener listener) { - if (_inner is INotifyCollectionChanged incc) + if (_list is INotifyCollectionChanged incc) { CollectionChangedEventManager.Instance.RemoveListener(incc, listener); } } - public Enumerator GetEnumerator() => new Enumerator(_inner); - IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public Enumerator GetEnumerator() => new Enumerator(this); + IEnumerator IEnumerable.GetEnumerator() => _readOnlyList?.GetEnumerator() ?? _list!.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args) + void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) { - CollectionChanged?.Invoke(this, args); } - private void ListenToCollectionChanges() + void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) { - if (_inner is INotifyCollectionChanged incc) - { - incc.CollectionChanged += OnCollectionChanged; - _notifyCollectionChanged = incc; - } } - private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) { - OnItemsSourceChanged(e); + // Raise CollectionChanged after all listeners who subscribed via AddListener have had + // chance to handle the event in PreChanged and Changed. + _collectionChanged?.Invoke(this, e); } - public struct Enumerator : IEnumerator + public struct Enumerator : IEnumerator { private IEnumerator _innerEnumerator; - public Enumerator(IList inner) => _innerEnumerator = inner.GetEnumerator(); + public Enumerator(IEnumerable inner) => _innerEnumerator = inner.GetEnumerator(); public object Current => _innerEnumerator.Current; object IEnumerator.Current => Current; public void Dispose() => (_innerEnumerator as IDisposable)?.Dispose(); @@ -231,12 +301,37 @@ private ItemsSourceView(IEnumerable source) /// The index. /// The item. [return: MaybeNull] - public new T GetAt(int index) => (T)_inner[index]; + public new T GetAt(int index) => (T)base.GetAt(index); - public IEnumerator GetEnumerator() => _inner.Cast().GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); + public new IEnumerator GetEnumerator() => ((IEnumerable)this).Cast().GetEnumerator(); - public static new ItemsSourceView GetOrCreate(IEnumerable? items) + /// + /// Gets an for the source items, or null if + /// is null. + /// + /// The source items. + public static new ItemsSourceView? GetOrCreate(IEnumerable? items) + { + if (items is ItemsSourceView isv) + { + return isv; + } + else if (items is null) + { + return null; + } + else + { + return new ItemsSourceView(items); + } + } + + /// + /// Gets an for the source items, or if + /// is null. + /// + /// The source items. + public static new ItemsSourceView GetOrCreateOrEmpty(IEnumerable? items) { if (items is ItemsSourceView isv) { diff --git a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs index 230575074a4..dcfa1d4a462 100644 --- a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs +++ b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs @@ -25,7 +25,7 @@ protected IEnumerable? Source { ItemsView?.RemoveListener(this); _source = value; - ItemsView = value is object ? ItemsSourceView.GetOrCreate(value) : null; + ItemsView = ItemsSourceView.GetOrCreate(value); ItemsView?.AddListener(this); } } From 8d23f4485ee328e160fc4674df73afbf9216a606 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 8 Sep 2020 19:00:11 +0200 Subject: [PATCH 09/39] Update TabOnceActiveElement on prepare. --- .../Primitives/SelectingItemsControl.cs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index ec3cac19ed8..eed4e4bb09b 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -344,7 +344,15 @@ protected override void ItemsViewCollectionChanged(NotifyCollectionChangedEventA protected override void OnContainerPrepared(ElementPreparedEventArgs e) { base.OnContainerPrepared(e); + MarkContainerSelected(e.Element, _selection?.IsSelected(e.Index) ?? false); + + if (Selection.AnchorIndex == e.Index) + { + KeyboardNavigation.SetTabOnceActiveElement( + (InputElement)Presenter!, + e.Element); + } } protected override void OnContainerClearing(ElementClearingEventArgs e) @@ -544,14 +552,6 @@ protected void UpdateSelection( Selection.Clear(); Selection.Select(index); } - - if (Presenter != null) - { - var container = TryGetContainer(index); - KeyboardNavigation.SetTabOnceActiveElement( - (InputElement)Presenter, - container); - } } /// @@ -616,11 +616,19 @@ protected bool UpdateSelectionFromEventSource( /// The event args. private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e) { - if (e.PropertyName == nameof(ISelectionModel.AnchorIndex) && AutoScrollToSelectedItem) + if (e.PropertyName == nameof(ISelectionModel.AnchorIndex)) { - if (Selection.AnchorIndex > 0) + if (Selection.AnchorIndex > 0 && Presenter is object) { - ScrollIntoView(Selection.AnchorIndex); + if (AutoScrollToSelectedItem) + { + ScrollIntoView(Selection.AnchorIndex); + } + + var container = TryGetContainer(Selection.AnchorIndex); + KeyboardNavigation.SetTabOnceActiveElement( + (InputElement)Presenter, + container); } } else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex)) From 6d07d96c41b254ec4d1d866b42a467bd07b71c5e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Sep 2020 15:41:28 +0200 Subject: [PATCH 10/39] Fix some tests broken by merge. --- .../Primitives/SelectingItemsControl.cs | 38 ++++++++++++------- src/Avalonia.Input/KeyboardNavigation.cs | 8 ++-- .../Primitives/SelectingItemsControlTests.cs | 2 + 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 75f16e74662..49e05e27e4e 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -430,23 +430,13 @@ protected override void ItemsViewCollectionChanged(NotifyCollectionChangedEventA protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); - AutoScrollToSelectedItemIfNecessary(); + AutoScrollToSelectedItemOnLayoutUpdated(); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - - void ExecuteScrollWhenLayoutUpdated(object sender, EventArgs e) - { - LayoutUpdated -= ExecuteScrollWhenLayoutUpdated; - AutoScrollToSelectedItemIfNecessary(); - } - - if (AutoScrollToSelectedItem) - { - LayoutUpdated += ExecuteScrollWhenLayoutUpdated; - } + AutoScrollToSelectedItemOnLayoutUpdated(); } /// @@ -456,7 +446,7 @@ protected override void OnContainerPrepared(ElementPreparedEventArgs e) MarkContainerSelected(e.Element, _selection?.IsSelected(e.Index) ?? false); - if (Selection.AnchorIndex == e.Index) + if (_selection?.AnchorIndex == e.Index) { KeyboardNavigation.SetTabOnceActiveElement( (InputElement)Presenter!, @@ -715,6 +705,14 @@ private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEvent { _hasScrolledToSelectedItem = false; AutoScrollToSelectedItemIfNecessary(); + + if (Presenter is object) + { + var container = TryGetContainer(Selection.AnchorIndex); + KeyboardNavigation.SetTabOnceActiveElement( + (InputElement)Presenter, + container); + } } else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex) && _oldSelectedIndex != SelectedIndex) { @@ -804,6 +802,20 @@ Presenter is object && } } + private void AutoScrollToSelectedItemOnLayoutUpdated() + { + void ExecuteScrollWhenLayoutUpdated(object sender, EventArgs e) + { + LayoutUpdated -= ExecuteScrollWhenLayoutUpdated; + AutoScrollToSelectedItemIfNecessary(); + } + + if (AutoScrollToSelectedItem) + { + LayoutUpdated += ExecuteScrollWhenLayoutUpdated; + } + } + /// /// Called when a container raises the . /// diff --git a/src/Avalonia.Input/KeyboardNavigation.cs b/src/Avalonia.Input/KeyboardNavigation.cs index 722215f8b72..48a125ecac3 100644 --- a/src/Avalonia.Input/KeyboardNavigation.cs +++ b/src/Avalonia.Input/KeyboardNavigation.cs @@ -25,8 +25,8 @@ public static class KeyboardNavigation /// attached property set to , this property /// defines to which child the focus should move. /// - public static readonly AttachedProperty TabOnceActiveElementProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty TabOnceActiveElementProperty = + AvaloniaProperty.RegisterAttached( "TabOnceActiveElement", typeof(KeyboardNavigation)); @@ -68,7 +68,7 @@ public static void SetTabNavigation(InputElement element, KeyboardNavigationMode /// /// The container. /// The active element for the container. - public static IInputElement GetTabOnceActiveElement(InputElement element) + public static IInputElement? GetTabOnceActiveElement(InputElement element) { return element.GetValue(TabOnceActiveElementProperty); } @@ -78,7 +78,7 @@ public static IInputElement GetTabOnceActiveElement(InputElement element) /// /// The container. /// The active element for the container. - public static void SetTabOnceActiveElement(InputElement element, IInputElement value) + public static void SetTabOnceActiveElement(InputElement element, IInputElement? value) { element.SetValue(TabOnceActiveElementProperty, value); } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 4125e18c296..ce92c6f6b4f 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1504,6 +1504,8 @@ public void AutoScrollToSelectedItem_Scrolls_When_Reattached_To_Visual_Tree_If_S target.SelectedIndex = 1; root.Child = target; + Layout(target); + Assert.True(raised); } From 3050c7129c799329167fd8c77fde34366f633747 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Sep 2020 17:24:32 +0200 Subject: [PATCH 11/39] Fix some more tests broken by merge. --- .../Selection/InternalSelectionModel.cs | 16 ++++++---------- .../SelectingItemsControlTests_Multiple.cs | 1 + 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs index a28e4b27858..d4228352945 100644 --- a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs +++ b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs @@ -48,7 +48,7 @@ public IList WritableSelectedItems { UnsubscribeFromSelectedItems(); _writableSelectedItems = value; - SyncFromSelectedItems(); + SyncFromSelectedItems(_writableSelectedItems); SubscribeToSelectedItems(); if (ItemsView is null) @@ -87,11 +87,7 @@ private protected override void SetSource(IEnumerable? value) } else { - foreach (var i in oldSelection) - { - var index = ItemsView!.IndexOf(i); - Select(index); - } + SyncFromSelectedItems(oldSelection); } } @@ -116,9 +112,9 @@ private void SyncToSelectedItems() } } - private void SyncFromSelectedItems() + private void SyncFromSelectedItems(IList? selectedItems) { - if (Source is null || _writableSelectedItems is null) + if (Source is null || selectedItems is null) { return; } @@ -130,7 +126,7 @@ private void SyncFromSelectedItems() using (BatchUpdate()) { Clear(); - Add(_writableSelectedItems); + Add(selectedItems); } } finally @@ -186,7 +182,7 @@ private void OnSelectionChanged(object sender, SelectionModelSelectionChangedEve } } - private void OnSourceReset(object sender, EventArgs e) => SyncFromSelectedItems(); + private void OnSourceReset(object sender, EventArgs e) => SyncFromSelectedItems(_writableSelectedItems); private void OnSelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 2a37c553172..3aea5022f8f 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1025,6 +1025,7 @@ public void Removing_Item_Before_SelectedItem_Should_Update_Selection() Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems); items.RemoveAt(0); + Layout(target); Assert.Equal(0, target.SelectedIndex); Assert.Equal("Bar", target.SelectedItem); From f3f7c3e3e5130aec3583e0980bd9e2f07119a3c8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Sep 2020 17:42:48 +0200 Subject: [PATCH 12/39] Copy ICollection.CopyTo implementation from the BCL. Did not handle differing types. --- src/Avalonia.Base/Collections/AvaloniaList.cs | 68 ++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index f201cfab1f4..d43b4e04bb5 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -543,7 +543,73 @@ void IList.RemoveAt(int index) /// void ICollection.CopyTo(Array array, int index) { - _inner.CopyTo((T[])array, index); + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + if (array.Rank != 1) + { + throw new ArgumentException("Multi-dimensional arrays are not supported."); + } + + if (array.GetLowerBound(0) != 0) + { + throw new ArgumentException("Non-zero lower bounds are not supported."); + } + + if (index < 0) + { + throw new ArgumentException("Invalid index."); + } + + if (array.Length - index < Count) + { + throw new ArgumentException("The target array is too small."); + } + + if (array is T[] tArray) + { + _inner.CopyTo(tArray, index); + } + else + { + // + // Catch the obvious case assignment will fail. + // We can't find all possible problems by doing the check though. + // For example, if the element type of the Array is derived from T, + // we can't figure out if we can successfully copy the element beforehand. + // + Type targetType = array.GetType().GetElementType()!; + Type sourceType = typeof(T); + if (!(targetType.IsAssignableFrom(sourceType) || sourceType.IsAssignableFrom(targetType))) + { + throw new ArgumentException("Invalid array type"); + } + + // + // We can't cast array of value type to object[], so we don't support + // widening of primitive types here. + // + object[] objects = array as object[]; + if (objects == null) + { + throw new ArgumentException("Invalid array type"); + } + + int count = _inner.Count; + try + { + for (int i = 0; i < count; i++) + { + objects[index++] = _inner[i]; + } + } + catch (ArrayTypeMismatchException) + { + throw new ArgumentException("Invalid array type"); + } + } } /// From ee082de4f144b82c34bcc34dd97070fdd185ebbd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Sep 2020 17:46:07 +0200 Subject: [PATCH 13/39] Short-circuit setting source to current value. --- src/Avalonia.Controls/Selection/InternalSelectionModel.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs index d4228352945..22689e9e457 100644 --- a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs +++ b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs @@ -63,6 +63,11 @@ public IList WritableSelectedItems private protected override void SetSource(IEnumerable? value) { + if (Source == value) + { + return; + } + object?[]? oldSelection = null; if (Source is object && value is object) From 0b4fd78129866144b8ad07e66932775d52f31c52 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Sep 2020 17:53:33 +0200 Subject: [PATCH 14/39] Fix more failing tests. --- .../CompiledBindingExtensionTests.cs | 4 +--- .../Xaml/DataTemplateTests.cs | 4 +--- .../AutoDataTemplateBindingHookTest.cs | 15 +++++++++++++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index df1e6b93764..a2535da5a1d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -422,9 +422,7 @@ public void InfersDataTemplateTypeFromParentCollectionItemsType() window.DataContext = dataContext; - window.ApplyTemplate(); - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + window.LayoutManager.ExecuteInitialLayoutPass(); var presenter = (ContentPresenter)target.Presenter.RealizedElements.First(); Assert.Equal(dataContext.ListProperty[0], presenter.Content); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs index daa367d95f5..cf5419e84c0 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs @@ -57,9 +57,7 @@ public void DataTemplate_Can_Contain_Named_UserControl() window.DataContext = new[] { "item1", "item2" }; - window.ApplyTemplate(); - itemsControl.ApplyTemplate(); - itemsControl.Presenter.ApplyTemplate(); + window.LayoutManager.ExecuteInitialLayoutPass(); Assert.Equal(2, itemsControl.Presenter.RealizedElements.Count()); } diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs index c9be84e035a..b12ac510f2f 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs @@ -12,6 +12,7 @@ using Splat; using System.Threading.Tasks; using System; +using Avalonia.Layout; namespace Avalonia.ReactiveUI.UnitTests { @@ -68,7 +69,7 @@ public void Should_Use_ViewModelViewHost_As_Data_Template_By_Default() view.List.Template = GetTemplate(); view.List.ApplyTemplate(); - view.List.Presenter.ApplyTemplate(); + Layout(view.List.Presenter); var child = view.List.Presenter.RealizedElements.First(); var container = (ContentPresenter) child; @@ -85,7 +86,7 @@ public void ViewModelViewHost_Should_Resolve_And_Embedd_Appropriate_View_Model() view.List.Template = GetTemplate(); view.List.ApplyTemplate(); - view.List.Presenter.ApplyTemplate(); + Layout(view.List.Presenter); var child = view.List.Presenter.RealizedElements.First(); var container = (ContentPresenter) child; @@ -114,6 +115,8 @@ public void Should_Not_Use_View_Model_View_Host_When_Item_Template_Is_Set() var view = new ExampleView(control => control.ItemTemplate = GetItemTemplate()); view.ViewModel.Items.Add(new NestedViewModel()); + Layout(view.List.Presenter); + var child = view.List.Presenter.RealizedElements.ElementAt(0); var container = (ContentPresenter) child; container.UpdateChild(); @@ -127,6 +130,8 @@ public void Should_Not_Use_View_Model_View_Host_When_Data_Templates_Are_Not_Empt var view = new ExampleView(control => control.DataTemplates.Add(GetItemTemplate())); view.ViewModel.Items.Add(new NestedViewModel()); + Layout(view.List.Presenter); + var child = view.List.Presenter.RealizedElements.ElementAt(0); var container = (ContentPresenter) child; container.UpdateChild(); @@ -134,6 +139,12 @@ public void Should_Not_Use_View_Model_View_Host_When_Data_Templates_Are_Not_Empt Assert.IsType(container.Child); } + private static void Layout(ILayoutable target) + { + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + } + private static FuncDataTemplate GetItemTemplate() { return new FuncDataTemplate((parent, scope) => new TextBlock()); From faf52f75539a4e0a3cf54e9a2712141d6aeee7c3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Sep 2020 18:01:25 +0200 Subject: [PATCH 15/39] Fix more failing tests. Ensure necessary assemblies are loaded. --- .../Converters/PointsListTypeConverterTests.cs | 8 ++++++++ .../AvaloniaActivationForViewFetcherTest.cs | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs index 3b729e9cd80..42c492ee41a 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using System.Reflection; using Avalonia.Controls.Shapes; +using Avalonia.Data; using Avalonia.Markup.Xaml.Converters; using Xunit; @@ -7,6 +9,12 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters { public class PointsListTypeConverterTests { + public PointsListTypeConverterTests() + { + // Ensure needed assemblies are loaded. + Assembly.Load(typeof(RelativeSource).Assembly.GetName()); + } + [Theory] [InlineData("1,2 3,4")] [InlineData("1 2 3 4")] diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs index fda85031359..20ed1e6d599 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -12,6 +12,9 @@ using Splat; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; +using Avalonia.Markup.Xaml.XamlIl.Runtime; +using Avalonia.Data; +using System.Reflection; namespace Avalonia.ReactiveUI.UnitTests { @@ -111,6 +114,10 @@ public AvaloniaActivationForViewFetcherTest() Locator.CurrentMutable.RegisterConstant( new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + + // Ensure needed assemblies are loaded. + Assembly.Load(typeof(XamlLoadException).Assembly.GetName()); + Assembly.Load(typeof(RelativeSource).Assembly.GetName()); } [Fact] From d27c93de66c633f7dd0fd7ceb74775329215fb5f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Sep 2020 19:32:13 +0200 Subject: [PATCH 16/39] Fix TabControl tests. --- src/Avalonia.Controls/ItemsControl.cs | 5 +- src/Avalonia.Controls/TabControl.cs | 38 +++----- .../TabControlTests.cs | 86 +++++++++++++------ 3 files changed, 79 insertions(+), 50 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 611f1e0b40d..5babf4dbf16 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -428,7 +428,10 @@ void Add(IList items) { foreach (ILogical l in items) { - LogicalChildren.Add(l); + if (!LogicalChildren.Contains(l)) + { + LogicalChildren.Add(l); + } } } diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 95c74c6d042..bfb4aae27cc 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -145,14 +145,19 @@ protected override void OnContainerPrepared(ElementPreparedEventArgs e) { base.OnContainerPrepared(e); - if (SelectedContent is object || SelectedIndex == -1) + if (e.Index == SelectedIndex) { - return; + UpdateSelectedContent(e.Element as IContentControl); } + } + + protected override void OnContainerIndexChanged(ElementIndexChangedEventArgs e) + { + base.OnContainerIndexChanged(e); - if (e.Element is IContentControl c) + if (e.NewIndex == SelectedIndex) { - UpdateSelectedContent(c); + UpdateSelectedContent(e.Element as IContentControl); } } @@ -162,34 +167,19 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs if (change.Property == SelectedIndexProperty) { - var newIndex = change.NewValue.GetValueOrDefault(-1); - - if (newIndex == -1) - { - SelectedContentTemplate = null; - SelectedContent = null; - return; - } - - var container = TryGetContainer(newIndex); - - if (container is IContentControl c) - { - UpdateSelectedContent(c); - } + var container = TryGetContainer(SelectedIndex) as IContentControl; + UpdateSelectedContent(container); } } - private void UpdateSelectedContent(IContentControl item) + private void UpdateSelectedContent(IContentControl? container) { - if (SelectedContentTemplate != item.ContentTemplate) + if (container is null) { - SelectedContentTemplate = item.ContentTemplate; + SelectedContent = SelectedContentTemplate = null; } else { - var container = SelectedItem as IContentControl ?? - ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as IContentControl; SelectedContentTemplate = container?.ContentTemplate; SelectedContent = container?.Content; } diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index d6e763b24cf..778137b3233 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -7,10 +7,12 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; +using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; using Avalonia.Styling; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Xunit; namespace Avalonia.Controls.UnitTests @@ -20,10 +22,11 @@ public class TabControlTests [Fact] public void First_Tab_Should_Be_Selected_By_Default() { + using var app = Start(); + TabItem selected; var target = new TabControl { - Template = TabControlTemplate(), Items = new[] { (selected = new TabItem @@ -39,7 +42,7 @@ public void First_Tab_Should_Be_Selected_By_Default() } }; - target.ApplyTemplate(); + Prepare(target); Assert.Equal(0, target.SelectedIndex); Assert.Equal(selected, target.SelectedItem); @@ -50,10 +53,7 @@ public void Pre_Selecting_TabItem_Should_Set_SelectedContent_After_It_Was_Added( { using var app = Start(); - var target = new TabControl - { - Template = TabControlTemplate(), - }; + var target = new TabControl(); const string secondContent = "Second"; @@ -65,17 +65,16 @@ public void Pre_Selecting_TabItem_Should_Set_SelectedContent_After_It_Was_Added( target.Items = items; - var root = new TestRoot(target); - - ApplyTemplate(target); - root.LayoutManager.ExecuteInitialLayoutPass(); + Prepare(target); Assert.Equal(secondContent, target.SelectedContent); } [Fact] - public void Logical_Children_Should_Be_TabItems() + public void Logical_Children_Should_Be_TabItems_Plus_Content() { + using var app = Start(); + var items = new[] { new TabItem @@ -90,18 +89,25 @@ public void Logical_Children_Should_Be_TabItems() var target = new TabControl { - Template = TabControlTemplate(), Items = items, }; - Assert.Equal(items, target.GetLogicalChildren()); - target.ApplyTemplate(); - Assert.Equal(items, target.GetLogicalChildren()); + var logicalChildren = (IList)target.GetLogicalChildren(); + Assert.Equal(items, logicalChildren); + + Prepare(target); + + Assert.Equal(3, logicalChildren.Count); + Assert.Same(items[0], logicalChildren[0]); + Assert.Same(items[1], logicalChildren[1]); + Assert.IsType(logicalChildren[2]); } [Fact] public void Removal_Should_Set_First_Tab() { + using var app = Start(); + var collection = new ObservableCollection() { new TabItem @@ -123,17 +129,18 @@ public void Removal_Should_Set_First_Tab() var target = new TabControl { - Template = TabControlTemplate(), Items = collection, }; Prepare(target); target.SelectedItem = collection[1]; + Layout(target); Assert.Same(collection[1], target.SelectedItem); Assert.Equal(collection[1].Content, target.SelectedContent); collection.RemoveAt(1); + Layout(target); Assert.Same(collection[0], target.SelectedItem); Assert.Equal(collection[0].Content, target.SelectedContent); @@ -142,6 +149,8 @@ public void Removal_Should_Set_First_Tab() [Fact] public void Removal_Should_Set_New_Item0_When_Item0_Selected() { + using var app = Start(); + var collection = new ObservableCollection() { new TabItem @@ -163,7 +172,6 @@ public void Removal_Should_Set_New_Item0_When_Item0_Selected() var target = new TabControl { - Template = TabControlTemplate(), Items = collection, }; @@ -182,7 +190,7 @@ public void Removal_Should_Set_New_Item0_When_Item0_Selected() [Fact] public void Removal_Should_Set_New_Item0_When_Item0_Selected_With_DataTemplate() { - using var app = UnitTestApplication.Start(TestServices.StyledWindow); + using var app = Start(); var collection = new ObservableCollection() { @@ -193,7 +201,6 @@ public void Removal_Should_Set_New_Item0_When_Item0_Selected_With_DataTemplate() var target = new TabControl { - Template = TabControlTemplate(), Items = collection, }; @@ -204,6 +211,7 @@ public void Removal_Should_Set_New_Item0_When_Item0_Selected_With_DataTemplate() Assert.Equal(collection[0], target.SelectedContent); collection.RemoveAt(0); + Layout(target); Assert.Same(collection[0], target.SelectedItem); Assert.Equal(collection[0], target.SelectedContent); @@ -458,7 +466,7 @@ private static IDisposable Start() return UnitTestApplication.Start(services); } - private IControlTemplate TabControlTemplate() + private static IControlTemplate TabControlTemplate() { return new FuncControlTemplate((parent, scope) => new StackPanel @@ -481,7 +489,7 @@ private IControlTemplate TabControlTemplate() }); } - private IControlTemplate TabItemTemplate() + private static IControlTemplate TabItemTemplate() { return new FuncControlTemplate((parent, scope) => new ContentPresenter @@ -492,11 +500,39 @@ private IControlTemplate TabItemTemplate() }.RegisterInNameScope(scope)); } - private void Prepare(TabControl target) + private static void Prepare(TabControl target) { - ApplyTemplate(target); - target.Measure(Size.Infinity); - target.Arrange(new Rect(target.DesiredSize)); + var root = new TestRoot + { + Child = target, + Width = 100, + Height = 100, + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(TabControl.TemplateProperty, TabControlTemplate()), + }, + }, + new Style(x => x.OfType()) + { + Setters = + { + new Setter(TabItem.TemplateProperty, TabItemTemplate()), + }, + }, + }, + }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + } + + private void Layout(TabControl target) + { + var root = (TestRoot)target.GetVisualRoot(); + root.LayoutManager.ExecuteLayoutPass(); } private void ApplyTemplate(TabControl target) From 2158b60bc6fea13bf58d90f44272662f1eb254fa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 18 Sep 2020 00:26:09 +0200 Subject: [PATCH 17/39] Don't try to SelectAll with single selection. It throws. Also remove the key handling from `SelectingItemsControl` - it shouldn't handle interaction. --- src/Avalonia.Controls/ListBox.cs | 3 +-- .../Primitives/SelectingItemsControl.cs | 20 ------------------- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 1ada555704e..44497154f06 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -135,8 +135,7 @@ protected override void OnKeyDown(KeyEventArgs e) if (ItemCount > 0 && Match(keymap.SelectAll) && - (((SelectionMode & SelectionMode.Multiple) != 0) || - (SelectionMode & SelectionMode.Toggle) != 0)) + SelectionMode.HasFlagCustom(SelectionMode.Multiple)) { Selection.SelectAll(); e.Handled = true; diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 49e05e27e4e..6eefa3f8a9e 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -490,26 +490,6 @@ protected override void OnInitialized() } } - protected override void OnKeyDown(KeyEventArgs e) - { - base.OnKeyDown(e); - - if (!e.Handled) - { - var keymap = AvaloniaLocator.Current.GetService(); - bool Match(List gestures) => gestures.Any(g => g.Matches(e)); - - if (ItemCount > 0 && - Match(keymap.SelectAll) && - (((SelectionMode & SelectionMode.Multiple) != 0) || - (SelectionMode & SelectionMode.Toggle) != 0)) - { - Selection.SelectAll(); - e.Handled = true; - } - } - } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); From dbcfa0e507ab07aae917ded71749e24f166a64b1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 18 Sep 2020 00:28:56 +0200 Subject: [PATCH 18/39] Don't try to realize index -1. `Algorithm_GetAnchorForTargetElement` can return -1 and when it does so we try to realize that item, which obviously doesn't exist, so boom. --- src/Avalonia.Layout/FlowLayoutAlgorithm.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs index cd7f725f185..eace54d2e00 100644 --- a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs @@ -211,7 +211,7 @@ private int GetAnchorIndex( anchorPosition = new Point(anchorBounds.X, anchorBounds.Y); } } - else + else if (anchorIndex >= 0) { // It is possible to end up in a situation during a collection change where GetAnchorForTargetElement returns an index // which is not in the realized range. Eg. insert one item at index 0 for a grid layout. From b524009d2b6b3663fa1e1c4eda51cb82e8871536 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 18 Sep 2020 00:43:58 +0200 Subject: [PATCH 19/39] Remove unused code and fix nullability. --- src/Avalonia.Controls/ListBox.cs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 44497154f06..05998c908db 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -27,7 +27,7 @@ public class ListBox : SelectingItemsControl /// /// Defines the property. /// - public static readonly new DirectProperty SelectedItemsProperty = + public static readonly new DirectProperty SelectedItemsProperty = SelectingItemsControl.SelectedItemsProperty; /// @@ -43,7 +43,6 @@ public class ListBox : SelectingItemsControl SelectingItemsControl.SelectionModeProperty; private IScrollable? _scroll; - private bool _rangeSelecting; static ListBox() { @@ -60,7 +59,7 @@ public IScrollable? Scroll } /// - public new IList SelectedItems + public new IList? SelectedItems { get => base.SelectedItems; set => base.SelectedItems = value; @@ -114,17 +113,9 @@ protected override void OnKeyDown(KeyEventArgs e) if (direction.HasValue && (!ctrl || shift)) { - try - { - _rangeSelecting = shift; - e.Handled = MoveSelection( - direction.Value, - shift); - } - finally - { - _rangeSelecting = false; - } + e.Handled = MoveSelection( + direction.Value, + shift); } } From 590e6bf17a4193b7ed4184d67ef63d5243aeb296 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 18 Sep 2020 13:38:05 +0200 Subject: [PATCH 20/39] Update list box page in control catalog. - Display number of realized containers - Allow switching between data templated items and inline `ListBoxItem`s --- samples/ControlCatalog/Pages/ListBoxPage.xaml | 16 ++++- .../ControlCatalog/Pages/ListBoxPage.xaml.cs | 23 +++++++ .../ViewModels/ListBoxPageViewModel.cs | 63 ++++++++++++++++--- 3 files changed, 92 insertions(+), 10 deletions(-) diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index 3521ad71a97..a155759a27f 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -1,6 +1,8 @@ + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + x:Class="ControlCatalog.Pages.ListBoxPage" + d:DesignWidth="800" d:DesignHeight="600"> ListBox @@ -12,12 +14,20 @@ AlwaysSelected AutoScrollToSelectedItem - + + Initializing... + + + + From DataTemplate + Inline ListBoxItems + - diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs index 59c8fc0cbe9..b57bf40aa69 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs @@ -1,4 +1,6 @@ +using System; using Avalonia.Controls; +using Avalonia.Controls.Presenters; using Avalonia.Markup.Xaml; using ControlCatalog.ViewModels; @@ -6,10 +8,31 @@ namespace ControlCatalog.Pages { public class ListBoxPage : UserControl { + private ListBox _listBox; + private IItemsPresenter _presenter; + private TextBlock _realizedCount; + public ListBoxPage() { InitializeComponent(); DataContext = new ListBoxPageViewModel(); + + _listBox = this.FindControl("listBox"); + _realizedCount = this.FindControl("realizedCount"); + LayoutUpdated += OnLayoutUpdated; + } + + private void OnLayoutUpdated(object sender, EventArgs e) + { + LayoutUpdated -= OnLayoutUpdated; + _presenter = _listBox.Presenter; + _presenter.VisualChildren.CollectionChanged += (s, e) => UpdateRealizedCount(); + UpdateRealizedCount(); + } + + private void UpdateRealizedCount() + { + _realizedCount.Text = $"{_presenter.VisualChildren.Count} containers realized"; } private void InitializeComponent() diff --git a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs index f75bc32105e..71d362565b5 100644 --- a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reactive; @@ -10,16 +12,29 @@ namespace ControlCatalog.ViewModels { public class ListBoxPageViewModel : ReactiveObject { + private IList _items; private bool _multiple; private bool _toggle; private bool _alwaysSelected; private bool _autoScrollToSelectedItem = true; + private ItemTypes _itemType; private int _counter; private ObservableAsPropertyHelper _selectionMode; public ListBoxPageViewModel() { - Items = new ObservableCollection(Enumerable.Range(1, 10000).Select(i => GenerateItem())); + this.WhenAnyValue(x => x.ItemType) + .Subscribe(x => + { + Items = x switch + { + ItemTypes.FromDataTemplate => new ObservableCollection( + Enumerable.Range(0, 10000).Select(i => GenerateContent())), + ItemTypes.ListBoxItems => new ObservableCollection( + Enumerable.Range(0, 200).Select(i => GenerateItem())), + _ => throw new NotSupportedException(), + }; + }); Selection = new SelectionModel(); Selection.Select(1); @@ -34,7 +49,17 @@ public ListBoxPageViewModel() (a ? SelectionMode.AlwaysSelected : 0)) .ToProperty(this, x => x.SelectionMode); - AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem())); + AddItemCommand = ReactiveCommand.Create(() => + { + if (ItemType == ItemTypes.FromDataTemplate) + { + Items.Add(GenerateContent()); + } + else + { + Items.Add(GenerateItem()); + } + }); RemoveItemCommand = ReactiveCommand.Create(() => { @@ -50,15 +75,23 @@ public ListBoxPageViewModel() { var random = new Random(); - using (Selection.BatchUpdate()) + if (Items.Count > 0) { - Selection.Clear(); - Selection.Select(random.Next(Items.Count - 1)); + using (Selection.BatchUpdate()) + { + Selection.Clear(); + Selection.Select(random.Next(Items.Count - 1)); + } } }); } - public ObservableCollection Items { get; } + public IList Items + { + get => _items; + private set => this.RaiseAndSetIfChanged(ref _items, value); + } + public SelectionModel Selection { get; } public SelectionMode SelectionMode => _selectionMode.Value; @@ -86,10 +119,26 @@ public bool AutoScrollToSelectedItem set => this.RaiseAndSetIfChanged(ref _autoScrollToSelectedItem, value); } + public ItemTypes ItemType + { + get => _itemType; + set => this.RaiseAndSetIfChanged(ref _itemType, value); + } + public ReactiveCommand AddItemCommand { get; } public ReactiveCommand RemoveItemCommand { get; } public ReactiveCommand SelectRandomItemCommand { get; } - private string GenerateItem() => $"Item {_counter++.ToString()}"; + private string GenerateContent() => $"Item {_counter++.ToString()}"; + private ListBoxItem GenerateItem() => new ListBoxItem + { + Content = "ListBoxItem " + GenerateContent() + }; + + public enum ItemTypes + { + FromDataTemplate, + ListBoxItems, + } } } From cc153a5d056406b82b630ed6c758afdd489d4a35 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 18 Sep 2020 13:42:54 +0200 Subject: [PATCH 21/39] Removed unused field. And update summary. --- .../Generators/ItemContainerGenerator.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index 228595b3d15..eee9744ce2a 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -7,8 +7,13 @@ namespace Avalonia.Controls.Generators { /// - /// Creates containers for items and maintains a list of created containers. + /// Creates containers for items in an . /// + /// + /// As implemented in , creates a + /// as a container. To create a different type of container use + /// . + /// public class ItemContainerGenerator : IItemContainerGenerator { private static readonly AttachedProperty PreventRecycleProperty = @@ -22,7 +27,6 @@ public ItemContainerGenerator(ItemsControl owner) } public ItemsControl Owner { get; } - public bool SupportsRecycling => false; public bool Match(object data) => true; From 01de92c1c3e70648b8ff603ff01fb30e2bf8351d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 21 Sep 2020 16:43:04 +0200 Subject: [PATCH 22/39] Make sure inline items are removed... ...when removed from source items. --- .../ViewModels/ListBoxPageViewModel.cs | 4 +- .../Repeater/ItemsRepeater.cs | 19 ++- src/Avalonia.Controls/Repeater/ViewManager.cs | 34 +++- .../Repeater/VirtualizationInfo.cs | 3 +- .../ItemsControlTests.cs | 156 ++++++++++++------ .../ListBoxTests.cs | 133 +++++++++++++++ 6 files changed, 283 insertions(+), 66 deletions(-) diff --git a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs index 71d362565b5..c2108559076 100644 --- a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs @@ -36,7 +36,7 @@ public ListBoxPageViewModel() }; }); - Selection = new SelectionModel(); + Selection = new SelectionModel(); Selection.Select(1); _selectionMode = this.WhenAnyValue( @@ -92,7 +92,7 @@ public IList Items private set => this.RaiseAndSetIfChanged(ref _items, value); } - public SelectionModel Selection { get; } + public SelectionModel Selection { get; } public SelectionMode SelectionMode => _selectionMode.Value; public bool Multiple diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 17c1094fcc2..44259315d86 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -297,13 +297,20 @@ protected override Size MeasureOverride(Size availableSize) desiredSize = layout.Measure(layoutContext, availableSize); extent = new Rect(LayoutOrigin.X, LayoutOrigin.Y, desiredSize.Width, desiredSize.Height); - // Clear auto recycle candidate elements that have not been kept alive by layout - i.e layout did not - // call GetElementAt(index). - foreach (var element in Children) + // Clear elements: + // - Inline elements that were marked as ToRemove due to a collection change + // - Auto recycle candidate elements that have not been kept alive by layout - i.e layout did not + // call GetElementAt(index) + for (var i = Children.Count -1; i >= 0; --i) { + var element = Children[i]; var virtInfo = GetVirtualizationInfo(element); - if (virtInfo.Owner == ElementOwner.Layout && + if (virtInfo.ToRemove) + { + Children.RemoveAt(i); + } + else if (virtInfo.Owner == ElementOwner.Layout && virtInfo.AutoRecycleCandidate && !virtInfo.KeepAlive) { @@ -574,12 +581,15 @@ private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceVi throw new AvaloniaInternalException("Cannot set ItemsSourceView during layout."); } + var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + ItemsSourceView?.Dispose(); ItemsSourceView = newValue; if (oldValue != null) { oldValue.CollectionChanged -= OnItemsSourceViewChanged; + OnItemsSourceViewChanged(oldValue, args); } if (newValue != null) @@ -589,7 +599,6 @@ private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceVi if (Layout != null) { - var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); try { diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index 5c3b3764a52..ef18a06a45b 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -78,6 +78,7 @@ public void ClearElement(IControl element, bool isClearedDueToCollectionChange) var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); var index = virtInfo.Index; bool cleared = + ClearInlineElement(virtInfo, isClearedDueToCollectionChange) || ClearElementToUniqueIdResetPool(element, virtInfo) || ClearElementToPinnedPool(element, virtInfo, isClearedDueToCollectionChange); @@ -394,9 +395,10 @@ public void OnItemsSourceChanged(object sender, NotifyCollectionChangedEventArgs var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); var dataIndex = virtInfo.Index; - if (virtInfo.IsRealized) + if (virtInfo.IsRealized || virtInfo.IsInlineElement) { - if (virtInfo.AutoRecycleCandidate && oldStartIndex <= dataIndex && dataIndex < oldStartIndex + oldCount) + if ((virtInfo.AutoRecycleCandidate || virtInfo.IsInlineElement) && + oldStartIndex <= dataIndex && dataIndex < oldStartIndex + oldCount) { // If we are doing the mapping, remove the element who's data was removed. _owner.ClearElementImpl(element); @@ -417,17 +419,12 @@ public void OnItemsSourceChanged(object sender, NotifyCollectionChangedEventArgs // running layout, we dont have to clear all the elements again. if (!_isDataSourceStableResetPending) { - if (_owner.ItemsSourceView.HasKeyIndexMapping) - { - _isDataSourceStableResetPending = true; - } - // Walk through all the elements and make sure they are cleared, they will go into - // the stable id reset pool. + // the stable id reset pool or if they are inline elements marked for removal. foreach (var element in _owner.Children) { var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); - if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate) + if ((virtInfo.IsRealized && virtInfo.AutoRecycleCandidate) || virtInfo.IsInlineElement) { _owner.ClearElementImpl(element); } @@ -641,6 +638,10 @@ IControl GetElement() element.DataContext = data; virtInfo.MustClearDataContext = true; } + else + { + virtInfo.IsInlineElement = data == element; + } virtInfo.MoveOwnershipToLayoutFromElementFactory( index, @@ -670,6 +671,21 @@ IControl GetElement() return element; } + private bool ClearInlineElement(VirtualizationInfo virtInfo, bool isClearedDueToCollectionChange) + { + if (virtInfo.IsInlineElement) + { + if (isClearedDueToCollectionChange) + { + virtInfo.ToRemove = true; + } + + return true; + } + + return false; + } + private bool ClearElementToUniqueIdResetPool(IControl element, VirtualizationInfo virtInfo) { if (_isDataSourceStableResetPending) diff --git a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs index 7a639419c18..855a51ace0c 100644 --- a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs +++ b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs @@ -27,7 +27,6 @@ internal enum ElementOwner internal class VirtualizationInfo { private int _pinCounter; - private object _data; public Rect ArrangeBounds { get; set; } public bool AutoRecycleCandidate { get; set; } @@ -37,6 +36,8 @@ internal class VirtualizationInfo public bool IsRealized => IsHeldByLayout || Owner == ElementOwner.PinnedPool; public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool; public bool MustClearDataContext { get; set; } + public bool IsInlineElement { get; set; } + public bool ToRemove { get; set; } public bool KeepAlive { get; set; } public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory; public string UniqueId { get; private set; } diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index b6cf7ca4b92..5a131e1da32 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; @@ -5,7 +6,9 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.LogicalTree; +using Avalonia.Styling; using Avalonia.UnitTests; using Xunit; @@ -334,6 +337,28 @@ public void Should_Clear_Containers_When_ItemsPresenter_Changes() Assert.Empty(target.Presenter.RealizedElements); } + [Fact] + public void Control_Items_Should_Be_Removed_From_Presenter_When_Removed_From_Items() + { + using var app = Start(); + + var items = new AvaloniaList { new Canvas() }; + var target = new ItemsControl + { + Items = items, + }; + + Prepare(target); + + var presenterPanel = (IPanel)target.Presenter; + Assert.Equal(1, presenterPanel.Children.Count); + + items.RemoveAt(0); + Layout(target); + + Assert.Equal(0, presenterPanel.Children.Count); + } + [Fact] public void Empty_Class_Should_Initially_Be_Applied() { @@ -453,70 +478,62 @@ public void Control_Item_Should_Not_Be_NameScope() [Fact] public void Focuses_Next_Item_On_Key_Down() { - using (UnitTestApplication.Start(TestServices.RealFocus)) - { - var items = new object[] - { - new Button { Height = 10 }, - new Button { Height = 10 }, - }; + using var app = Start(); - var target = new ItemsControl - { - Template = GetTemplate(), - Items = items, - }; + var items = new object[] + { + new Button { Height = 10 }, + new Button { Height = 10 }, + }; - var root = new TestRoot { Child = target }; + var target = new ItemsControl + { + Items = items, + }; - Layout(root); - target.Presenter.RealizedElements.First().Focus(); + Prepare(target); + target.Presenter.RealizedElements.First().Focus(); - target.RaiseEvent(new KeyEventArgs - { - RoutedEvent = InputElement.KeyDownEvent, - Key = Key.Down, - }); + target.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Down, + }); - Assert.Equal( - target.Presenter.RealizedElements.ElementAt(1), - FocusManager.Instance.Current); - } + Assert.Equal( + target.Presenter.RealizedElements.ElementAt(1), + FocusManager.Instance.Current); } [Fact] public void Does_Not_Focus_Non_Focusable_Item_On_Key_Down() { - using (UnitTestApplication.Start(TestServices.RealFocus)) + using var app = Start(); + + var items = new object[] { - var items = new object[] - { new Button { Height = 10 }, new Button { Height = 10, Focusable = false }, new Button { Height = 10 }, - }; - - var target = new ItemsControl - { - Template = GetTemplate(), - Items = items, - }; + }; - var root = new TestRoot { Child = target }; + var target = new ItemsControl + { + Items = items, + }; - Layout(root); - target.Presenter.RealizedElements.First().Focus(); + Prepare(target); + target.Presenter.RealizedElements.First().Focus(); - target.RaiseEvent(new KeyEventArgs - { - RoutedEvent = InputElement.KeyDownEvent, - Key = Key.Down, - }); + target.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Down, + }); - Assert.Equal( - target.Presenter.RealizedElements.ElementAt(2), - FocusManager.Instance.Current); - } + Assert.Equal( + target.Presenter.RealizedElements.ElementAt(2), + FocusManager.Instance.Current); } [Fact] @@ -543,10 +560,51 @@ public void Detaching_Then_Reattaching_To_Logical_Tree_Twice_Does_Not_Throw() root.Child = target; } + private static IDisposable Start() + { + var services = TestServices.MockPlatformRenderInterface.With( + focusManager: new FocusManager(), + keyboardDevice: () => new KeyboardDevice(), + keyboardNavigation: new KeyboardNavigationHandler(), + inputManager: new InputManager(), + styler: new Styler(), + windowingPlatform: new MockWindowingPlatform()); + return UnitTestApplication.Start(services); + } + + private static void Prepare(ItemsControl target) + { + var root = new TestRoot + { + Child = target, + Width = 100, + Height = 100, + Styles = + { + new Style(x => x.Is()) + { + Setters = + { + new Setter(ListBox.TemplateProperty, GetTemplate()), + }, + }, + }, + }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + } + private static void Layout(IControl target) { - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); + if (target.VisualRoot is ILayoutRoot root) + { + root.LayoutManager.ExecuteLayoutPass(); + } + else + { + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + } } private class Item @@ -559,7 +617,7 @@ public Item(string value) public string Value { get; } } - private FuncControlTemplate GetTemplate() + private static FuncControlTemplate GetTemplate() { return new FuncControlTemplate((parent, scope) => { diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 56d42e3de7b..323b119bc54 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -85,6 +85,139 @@ public void ListBoxItem_Containers_Should_Be_Generated() Assert.Equal(items, text); } + [Fact] + public void ListBoxItem_Items_Should_Be_Removed_From_Presenter_When_Items_Cleared() + { + using var app = Start(); + + var items = new AvaloniaList( + Enumerable.Range(0, 100) + .Select(x => new ListBoxItem + { + Content = "Item " + x, + })); + var target = new ListBox + { + Items = items, + }; + + Prepare(target); + + var presenterPanel = (IPanel)target.Presenter; + + // Some, but not all items should be realized. + Assert.NotEmpty(presenterPanel.Children); + Assert.True(presenterPanel.Children.Count < 100); + + // Page to the bottom to ensure all items are realized. + var scroller = (ScrollViewer)target.Scroll; + while (scroller.Offset.Y + scroller.Viewport.Height < scroller.Extent.Height) + { + scroller.PageDown(); + Layout(target); + } + + // All items should now be realized. + Assert.Equal(100, presenterPanel.Children.Count); + + // Clear items. + items.Clear(); + Layout(target); + + // And ensure they're all removed as children of presenter. + Assert.Equal(0, presenterPanel.Children.Count); + } + + [Fact] + public void ListBoxItem_Items_Should_Be_Removed_From_Presenter_When_Removed_From_Items() + { + using var app = Start(); + + var items = new AvaloniaList( + Enumerable.Range(0, 100) + .Select(x => new ListBoxItem + { + Content = "Item " + x, + })); + var target = new ListBox + { + Items = items, + }; + + Prepare(target); + + var presenterPanel = (IPanel)target.Presenter; + + // Some, but not all items should be realized. + Assert.NotEmpty(presenterPanel.Children); + Assert.True(presenterPanel.Children.Count < 100); + + // Page to the bottom to ensure all items are realized. + var scroller = (ScrollViewer)target.Scroll; + while (scroller.Offset.Y + scroller.Viewport.Height < scroller.Extent.Height) + { + scroller.PageDown(); + Layout(target); + } + + // All items should now be realized. + Assert.Equal(100, presenterPanel.Children.Count); + + // Clear items. + while (items.Count > 0) + { + items.RemoveAt(0); + } + + Layout(target); + + // And ensure they're all removed as children of presenter. + Assert.Equal(0, presenterPanel.Children.Count); + } + + [Fact] + public void ListBoxItem_Items_Should_Be_Removed_From_Presenter_When_Items_Reassigned() + { + using var app = Start(); + + var items = new AvaloniaList( + Enumerable.Range(0, 100) + .Select(x => new ListBoxItem + { + Content = "Item " + x, + })); + var target = new ListBox + { + Items = items, + }; + + Prepare(target); + + var presenterPanel = (IPanel)target.Presenter; + + // Some, but not all items should be realized. + Assert.NotEmpty(presenterPanel.Children); + Assert.True(presenterPanel.Children.Count < 100); + + // Page to the bottom to ensure all items are realized. + var scroller = (ScrollViewer)target.Scroll; + while (scroller.Offset.Y + scroller.Viewport.Height < scroller.Extent.Height) + { + scroller.PageDown(); + Layout(target); + } + + // All items should now be realized. + Assert.Equal(100, presenterPanel.Children.Count); + + // Clear items. + target.Items = null; + Layout(target); + + // And ensure they're all removed as children of presenter. + Assert.Equal(0, presenterPanel.Children.Count); + } + [Fact] public void LogicalChildren_Should_Be_Set_For_DataTemplate_Generated_Items() { From 46e4551cbaa3a9719bfbb656b6a7a0a98fa19617 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 21 Oct 2020 11:25:14 +0200 Subject: [PATCH 23/39] Fix non-compiling tests. --- .../ListBoxTests.cs | 95 +++++++++---------- .../TreeViewTests.cs | 47 ++++----- 2 files changed, 67 insertions(+), 75 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 80ff5f9d659..ed0ae71a566 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -496,6 +496,52 @@ public void Clicking_Item_Should_Raise_BringIntoView_For_Correct_Control() Assert.Equal(2, raised); } + [Fact] + public void Adding_And_Selecting_Item_With_AutoScrollToSelectedItem_Should_NotHide_FirstItem() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var items = new AvaloniaList(); + + var wnd = new Window() { Width = 100, Height = 100, IsVisible = true }; + + var target = new ListBox() + { + VerticalAlignment = VerticalAlignment.Top, + AutoScrollToSelectedItem = true, + Width = 50, + ItemTemplate = new FuncDataTemplate((c, _) => new Border() { Height = 10 }), + Items = items, + }; + wnd.Content = target; + + var lm = wnd.LayoutManager; + + lm.ExecuteInitialLayoutPass(); + + items.Add("Item 1"); + target.Selection.Select(0); + lm.ExecuteLayoutPass(); + + Assert.Equal(1, target.Presenter.RealizedElements.Count()); + + items.Add("Item 2"); + target.Selection.Select(1); + lm.ExecuteLayoutPass(); + + Assert.Equal(2, target.Presenter.RealizedElements.Count()); + + //make sure we have enough space to show all items + Assert.True(target.Presenter.Bounds.Height >= target.Presenter.RealizedElements.Sum(c => c.Bounds.Height)); + + //make sure we show items and they completelly visible, not only partially + var e0 = target.Presenter.RealizedElements.ElementAt(0); + var e1 = target.Presenter.RealizedElements.ElementAt(1); + Assert.True(e0.Bounds.Top >= 0 && e0.Bounds.Bottom <= target.Presenter.Bounds.Height, "First item is not completely visible."); + Assert.True(e1.Bounds.Top >= 0 && e1.Bounds.Bottom <= target.Presenter.Bounds.Height, "Second item is not completely visible."); + } + } + private static IDisposable Start() { var services = TestServices.MockPlatformRenderInterface.With( @@ -551,54 +597,7 @@ private void KeyDown(IControl target, Key key, KeyModifiers modifiers = KeyModif }); } - [Fact] - public void Adding_And_Selecting_Item_With_AutoScrollToSelectedItem_Should_NotHide_FirstItem() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) - { - var items = new AvaloniaList(); - - var wnd = new Window() { Width = 100, Height = 100, IsVisible = true }; - - var target = new ListBox() - { - VerticalAlignment = Layout.VerticalAlignment.Top, - AutoScrollToSelectedItem = true, - Width = 50, - VirtualizationMode = ItemVirtualizationMode.Simple, - ItemTemplate = new FuncDataTemplate((c, _) => new Border() { Height = 10 }), - Items = items, - }; - wnd.Content = target; - - var lm = wnd.LayoutManager; - - lm.ExecuteInitialLayoutPass(); - - var panel = target.Presenter.Panel; - - items.Add("Item 1"); - target.Selection.Select(0); - lm.ExecuteLayoutPass(); - - Assert.Equal(1, panel.Children.Count); - - items.Add("Item 2"); - target.Selection.Select(1); - lm.ExecuteLayoutPass(); - - Assert.Equal(2, panel.Children.Count); - - //make sure we have enough space to show all items - Assert.True(panel.Bounds.Height >= panel.Children.Sum(c => c.Bounds.Height)); - - //make sure we show items and they completelly visible, not only partially - Assert.True(panel.Children[0].Bounds.Top >= 0 && panel.Children[0].Bounds.Bottom <= panel.Bounds.Height, "first item is not completelly visible!"); - Assert.True(panel.Children[1].Bounds.Top >= 0 && panel.Children[1].Bounds.Bottom <= panel.Bounds.Height, "second item is not completelly visible!"); - } - } - - private FuncControlTemplate ListBoxTemplate() + private static FuncControlTemplate ListBoxTemplate() { return new FuncControlTemplate((parent, scope) => new ScrollViewer diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index f301f65ef29..78cbe80e6fd 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -418,40 +418,33 @@ public void Setting_SelectedItem_Should_Raise_SelectedItemChanged_Event() [Fact] public void Bound_SelectedItem_Should_Not_Be_Cleared_when_Changing_Selection() { - using (Application()) - { - var dataContext = new TestDataContext(); - - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - DataContext = dataContext - }; + using var app = Start(); + var dataContext = new TestDataContext(); - target.Bind(TreeView.ItemsProperty, new Binding("Items")); - target.Bind(TreeView.SelectedItemProperty, new Binding("SelectedItem")); + var target = new TreeView + { + DataContext = dataContext + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + target.Bind(TreeView.ItemsProperty, new Binding("Items")); + target.Bind(TreeView.SelectedItemProperty, new Binding("SelectedItem")); - CreateNodeDataTemplate(target); - ApplyTemplates(target); + Prepare(target); - var selectedValues = new List(); + var selectedValues = new List(); - dataContext.PropertyChanged += (_, e) => - { - if (e.PropertyName == nameof(TestDataContext.SelectedItem)) - selectedValues.Add(dataContext.SelectedItem); - }; - selectedValues.Add(dataContext.SelectedItem); + dataContext.PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(TestDataContext.SelectedItem)) + selectedValues.Add(dataContext.SelectedItem); + }; + selectedValues.Add(dataContext.SelectedItem); - _mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Left); - _mouse.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Left); + _mouse.Click(target.Presenter.RealizedElements.ElementAt(0), MouseButton.Left); + _mouse.Click(target.Presenter.RealizedElements.ElementAt(2), MouseButton.Left); - Assert.Equal(3, selectedValues.Count); - Assert.Equal(new[] { null, "Item 0", "Item 2" }, selectedValues.ToArray()); - } + Assert.Equal(3, selectedValues.Count); + Assert.Equal(new[] { null, "Item 0", "Item 2" }, selectedValues.ToArray()); } [Fact] From 512449471bfb1c564f44e6f6db256fe1df1cc1f4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 1 Dec 2020 22:09:25 +0100 Subject: [PATCH 24/39] Changes missed by merge. --- .../ViewModels/ListBoxPageViewModel.cs | 8 +- .../ViewModels/MainWindowViewModel.cs | 10 +- .../Controls/ComboBox.xaml | 99 ++++++++++--------- .../TreeViewTests.cs | 15 +-- .../CompiledBindingExtensionTests.cs | 1 + 5 files changed, 65 insertions(+), 68 deletions(-) diff --git a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs index 31ea99b68b3..a32434df528 100644 --- a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs @@ -92,7 +92,7 @@ public IList Items } public SelectionModel Selection { get; } - public SelectionMode SelectionMode => _selectionMode.Value; + public IObservable SelectionMode => _selectionMode; public bool Multiple { @@ -124,9 +124,9 @@ public ItemTypes ItemType set => this.RaiseAndSetIfChanged(ref _itemType, value); } - public ReactiveCommand AddItemCommand { get; } - public ReactiveCommand RemoveItemCommand { get; } - public ReactiveCommand SelectRandomItemCommand { get; } + public MiniCommand AddItemCommand { get; } + public MiniCommand RemoveItemCommand { get; } + public MiniCommand SelectRandomItemCommand { get; } private string GenerateContent() => $"Item {_counter++.ToString()}"; private ListBoxItem GenerateItem() => new ListBoxItem diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs index 5aaa4125c40..96dbbc1a839 100644 --- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs @@ -80,11 +80,11 @@ public ScrollBarVisibility VerticalScrollBarVisibility public IEnumerable ScrollBarVisibilities => Enum.GetValues(typeof(ScrollBarVisibility)).Cast(); - public MiniCommand AddItemCommand { get; private set; } - public MiniCommand RecreateCommand { get; private set; } - public MiniCommand RemoveItemCommand { get; private set; } - public MiniCommand SelectFirstCommand { get; private set; } - public MiniCommand SelectLastCommand { get; private set; } + public MiniCommand AddItemCommand { get; private set; } + public MiniCommand RecreateCommand { get; private set; } + public MiniCommand RemoveItemCommand { get; private set; } + public MiniCommand SelectFirstCommand { get; private set; } + public MiniCommand SelectLastCommand { get; private set; } public void RandomizeSize() { diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml index b53afa0c479..b156481d68a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml @@ -39,7 +39,7 @@ - + @@ -98,51 +98,52 @@ IsVisible="False" HorizontalAlignment="Right" /> - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -224,15 +225,15 @@ - + - + - +