Browse Source

Merge branch 'master' into master

pull/10024/head
Dong Bin 4 years ago
committed by GitHub
parent
commit
0833d40ae1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      samples/ControlCatalog/Pages/CarouselPage.xaml
  2. 2
      samples/ControlCatalog/Pages/DialogsPage.xaml
  3. 60
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  4. 2
      samples/ControlCatalog/Pages/TabControlPage.xaml
  5. 4
      samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
  6. 5
      samples/VirtualizationDemo/MainWindow.xaml
  7. 10
      samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
  8. 2
      src/Avalonia.Base/Animation/PageSlide.cs
  9. 2
      src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs
  10. 5
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  11. 5
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml
  12. 52
      src/Avalonia.Controls/Carousel.cs
  13. 34
      src/Avalonia.Controls/ComboBox.cs
  14. 5
      src/Avalonia.Controls/ContextMenu.cs
  15. 5
      src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs
  16. 114
      src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
  17. 18
      src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs
  18. 49
      src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs
  19. 313
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  20. 102
      src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
  21. 42
      src/Avalonia.Controls/Generators/ItemContainerInfo.cs
  22. 21
      src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs
  23. 105
      src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs
  24. 166
      src/Avalonia.Controls/Generators/TreeContainerIndex.cs
  25. 166
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  26. 19
      src/Avalonia.Controls/IVirtualizingController.cs
  27. 89
      src/Avalonia.Controls/IVirtualizingPanel.cs
  28. 18
      src/Avalonia.Controls/ItemVirtualizationMode.cs
  29. 532
      src/Avalonia.Controls/ItemsControl.cs
  30. 249
      src/Avalonia.Controls/ItemsSourceView.cs
  31. 25
      src/Avalonia.Controls/ListBox.cs
  32. 10
      src/Avalonia.Controls/Menu.cs
  33. 17
      src/Avalonia.Controls/MenuBase.cs
  34. 19
      src/Avalonia.Controls/MenuItem.cs
  35. 276
      src/Avalonia.Controls/Presenters/CarouselPresenter.cs
  36. 18
      src/Avalonia.Controls/Presenters/IItemsPresenter.cs
  37. 26
      src/Avalonia.Controls/Presenters/IItemsPresenterHost.cs
  38. 125
      src/Avalonia.Controls/Presenters/ItemContainerSync.cs
  39. 303
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  40. 106
      src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs
  41. 606
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  42. 223
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  43. 308
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  44. 160
      src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
  45. 16
      src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs
  46. 91
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  47. 9
      src/Avalonia.Controls/Primitives/TabStrip.cs
  48. 6
      src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs
  49. 3
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  50. 1
      src/Avalonia.Controls/Selection/SelectionModel.cs
  51. 9
      src/Avalonia.Controls/Selection/SelectionNodeBase.cs
  52. 64
      src/Avalonia.Controls/TabControl.cs
  53. 2
      src/Avalonia.Controls/TopLevel.cs
  54. 217
      src/Avalonia.Controls/TreeView.cs
  55. 36
      src/Avalonia.Controls/TreeViewItem.cs
  56. 49
      src/Avalonia.Controls/Utils/CollectionUtils.cs
  57. 356
      src/Avalonia.Controls/VirtualizingCarouselPanel.cs
  58. 241
      src/Avalonia.Controls/VirtualizingPanel.cs
  59. 1111
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  60. 21
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs
  61. 3
      src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml
  62. 4
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
  63. 26
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
  64. 21
      src/Avalonia.Themes.Fluent/Controls/Carousel.xaml
  65. 5
      src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
  66. 2
      src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml
  67. 4
      src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml
  68. 5
      src/Avalonia.Themes.Fluent/Controls/ListBox.xaml
  69. 6
      src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml
  70. 4
      src/Avalonia.Themes.Fluent/Controls/Menu.xaml
  71. 2
      src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml
  72. 2
      src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
  73. 2
      src/Avalonia.Themes.Fluent/Controls/TabControl.xaml
  74. 4
      src/Avalonia.Themes.Fluent/Controls/TabStrip.xaml
  75. 1
      src/Avalonia.Themes.Fluent/Controls/TreeView.xaml
  76. 2
      src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
  77. 24
      src/Avalonia.Themes.Simple/Controls/Carousel.xaml
  78. 5
      src/Avalonia.Themes.Simple/Controls/ComboBox.xaml
  79. 2
      src/Avalonia.Themes.Simple/Controls/ContextMenu.xaml
  80. 2
      src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml
  81. 5
      src/Avalonia.Themes.Simple/Controls/ListBox.xaml
  82. 3
      src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml
  83. 4
      src/Avalonia.Themes.Simple/Controls/Menu.xaml
  84. 2
      src/Avalonia.Themes.Simple/Controls/MenuFlyoutPresenter.xaml
  85. 2
      src/Avalonia.Themes.Simple/Controls/MenuItem.xaml
  86. 2
      src/Avalonia.Themes.Simple/Controls/TabControl.xaml
  87. 2
      src/Avalonia.Themes.Simple/Controls/TabStrip.xaml
  88. 1
      src/Avalonia.Themes.Simple/Controls/TreeView.xaml
  89. 2
      src/Avalonia.Themes.Simple/Controls/TreeViewItem.xaml
  90. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs
  91. 4
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  92. 2
      src/iOS/Avalonia.iOS/TextInputResponder.cs
  93. 233
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  94. 2
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  95. 173
      tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs
  96. 42
      tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs
  97. 152
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  98. 11
      tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs
  99. 212
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  100. 1
      tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs

4
samples/ControlCatalog/Pages/CarouselPage.xaml

@ -12,7 +12,7 @@
</Button>
<Carousel Name="carousel" Grid.Column="1">
<Carousel.PageTransition>
<PageSlide Duration="0.25" Orientation="Vertical" />
<PageSlide Duration="0.25" Orientation="Horizontal" />
</Carousel.PageTransition>
<Image Source="/Assets/delicate-arch-896885_640.jpg"/>
<Image Source="/Assets/hirsch-899118_640.jpg"/>
@ -35,7 +35,7 @@
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock VerticalAlignment="Center">Orientation</TextBlock>
<ComboBox Name="orientation" SelectedIndex="1" VerticalAlignment="Center">
<ComboBox Name="orientation" SelectedIndex="0" VerticalAlignment="Center">
<ComboBoxItem>Horizontal</ComboBoxItem>
<ComboBoxItem>Vertical</ComboBoxItem>
</ComboBox>

2
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -46,7 +46,7 @@
Classes="h2"
IsVisible="False"
Text="Last picker results:" />
<ItemsPresenter x:Name="PickerLastResults" />
<ItemsControl x:Name="PickerLastResults" />
<TextBox Name="BookmarkContainer" Watermark="Bookmark" />
<TextBox Name="OpenedFileContent"

60
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -24,7 +24,7 @@ namespace ControlCatalog.Pages
{
this.InitializeComponent();
var results = this.Get<ItemsPresenter>("PickerLastResults");
var results = this.Get<ItemsControl>("PickerLastResults");
var resultsVisible = this.Get<TextBlock>("PickerLastResultsVisible");
var bookmarkContainer = this.Get<TextBox>("BookmarkContainer");
var openedFileContent = this.Get<TextBox>("OpenedFileContent");
@ -36,18 +36,18 @@ namespace ControlCatalog.Pages
{
if (this.Get<CheckBox>("UseFilters").IsChecked != true)
return new List<FileDialogFilter>();
return new List<FileDialogFilter>
{
new FileDialogFilter
{
Name = "Text files (.txt)", Extensions = new List<string> {"txt"}
},
new FileDialogFilter
{
Name = "All files",
Extensions = new List<string> {"*"}
}
};
return new List<FileDialogFilter>
{
new FileDialogFilter
{
Name = "Text files (.txt)", Extensions = new List<string> {"txt"}
},
new FileDialogFilter
{
Name = "All files",
Extensions = new List<string> {"*"}
}
};
}
List<FilePickerFileType>? GetFileTypes()
@ -55,10 +55,10 @@ namespace ControlCatalog.Pages
if (this.Get<CheckBox>("UseFilters").IsChecked != true)
return null;
return new List<FilePickerFileType>
{
FilePickerFileTypes.All,
FilePickerFileTypes.TextPlain
};
{
FilePickerFileTypes.All,
FilePickerFileTypes.TextPlain
};
}
this.Get<Button>("OpenFile").Click += async delegate
@ -205,15 +205,15 @@ namespace ControlCatalog.Pages
await using var stream = await file.OpenWriteAsync();
await using var reader = new System.IO.StreamWriter(stream);
#else
using var stream = await file.OpenWriteAsync();
using var reader = new System.IO.StreamWriter(stream);
using var stream = await file.OpenWriteAsync();
using var reader = new System.IO.StreamWriter(stream);
#endif
await reader.WriteLineAsync(openedFileContent.Text);
lastSelectedDirectory = await file.GetParentAsync();
}
await SetPickerResult(file is null ? null : new [] {file});
await SetPickerResult(file is null ? null : new[] { file });
};
this.Get<Button>("OpenFolderPicker").Click += async delegate
{
@ -243,7 +243,7 @@ namespace ControlCatalog.Pages
: null;
await SetPickerResult(folder is null ? null : new[] { folder });
lastSelectedDirectory = folder;
};
@ -260,23 +260,23 @@ namespace ControlCatalog.Pages
var props = await item.GetBasicPropertiesAsync();
resultText += @$"Size: {props.Size}
DateCreated: {props.DateCreated}
DateModified: {props.DateModified}
CanBookmark: {item.CanBookmark}
";
DateCreated: {props.DateCreated}
DateModified: {props.DateModified}
CanBookmark: {item.CanBookmark}
";
if (item is IStorageFile file)
{
resultText += @$"
CanOpenRead: {file.CanOpenRead}
CanOpenWrite: {file.CanOpenWrite}
Content:
";
CanOpenRead: {file.CanOpenRead}
CanOpenWrite: {file.CanOpenWrite}
Content:
";
if (file.CanOpenRead)
{
#if NET6_0_OR_GREATER
await using var stream = await file.OpenReadAsync();
#else
using var stream = await file.OpenReadAsync();
using var stream = await file.OpenReadAsync();
#endif
using var reader = new System.IO.StreamReader(stream);

2
samples/ControlCatalog/Pages/TabControlPage.xaml

@ -53,7 +53,7 @@
<TabControl
Items="{Binding Tabs}"
Margin="0 16"
HeaderDisplayMemberBinding="{Binding Header, x:DataType=viewModels:TabControlPageViewModelItem}"
DisplayMemberBinding="{Binding Header, x:DataType=viewModels:TabControlPageViewModelItem}"
TabStripPlacement="{Binding TabPlacement}">
<TabControl.ContentTemplate>
<DataTemplate x:DataType="viewModels:TabControlPageViewModelItem">

4
samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml

@ -168,9 +168,7 @@
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
<ItemsPresenter Name="PART_ItemsPresenter"
HorizontalAlignment="Stretch"
ItemTemplate="{TemplateBinding ItemTemplate}"
Items="{TemplateBinding Items}">
HorizontalAlignment="Stretch">
<ItemsPresenter.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel x:Name="HamburgerItemsPanel"

5
samples/VirtualizationDemo/MainWindow.xaml

@ -9,10 +9,8 @@
<DockPanel LastChildFill="True" Margin="16">
<StackPanel DockPanel.Dock="Right"
Margin="16 0 0 0"
MinWidth="150"
Width="150"
Spacing="4">
<ComboBox Items="{Binding VirtualizationModes}"
SelectedItem="{Binding VirtualizationMode}"/>
<ComboBox Items="{Binding Orientations}"
SelectedItem="{Binding Orientation}"/>
<TextBox Watermark="Item Count"
@ -49,7 +47,6 @@
Items="{Binding Items}"
Selection="{Binding Selection}"
SelectionMode="Multiple"
VirtualizationMode="{Binding VirtualizationMode}"
ScrollViewer.HorizontalScrollBarVisibility="{Binding HorizontalScrollBarVisibility, Mode=TwoWay}"
ScrollViewer.VerticalScrollBarVisibility="{Binding VerticalScrollBarVisibility, Mode=TwoWay}">
<ListBox.ItemsPanel>

10
samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs

@ -21,7 +21,6 @@ namespace VirtualizationDemo.ViewModels
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 @@ namespace VirtualizationDemo.ViewModels
public IEnumerable<ScrollBarVisibility> ScrollBarVisibilities =>
Enum.GetValues(typeof(ScrollBarVisibility)).Cast<ScrollBarVisibility>();
public ItemVirtualizationMode VirtualizationMode
{
get { return _virtualizationMode; }
set { this.RaiseAndSetIfChanged(ref _virtualizationMode, value); }
}
public IEnumerable<ItemVirtualizationMode> VirtualizationModes =>
Enum.GetValues(typeof(ItemVirtualizationMode)).Cast<ItemVirtualizationMode>();
public MiniCommand AddItemCommand { get; private set; }
public MiniCommand RecreateCommand { get; private set; }
public MiniCommand RemoveItemCommand { get; private set; }

2
src/Avalonia.Base/Animation/PageSlide.cs

@ -79,7 +79,6 @@ namespace Avalonia.Animation
var animation = new Animation
{
Easing = SlideOutEasing,
FillMode = FillMode.Forward,
Children =
{
new KeyFrame
@ -110,7 +109,6 @@ namespace Avalonia.Animation
to.IsVisible = true;
var animation = new Animation
{
FillMode = FillMode.Forward,
Easing = SlideInEasing,
Children =
{

2
src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs

@ -71,7 +71,6 @@ public class Rotate3DTransition: PageSlide
{
Easing = SlideOutEasing,
Duration = Duration,
FillMode = FillMode.Forward,
Children =
{
CreateKeyFrame(0d, 0d, 2),
@ -90,7 +89,6 @@ public class Rotate3DTransition: PageSlide
{
Easing = SlideInEasing,
Duration = Duration,
FillMode = FillMode.Forward,
Children =
{
CreateKeyFrame(0d, 90d * (forward ? 1 : -1), 1),

5
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml

@ -98,11 +98,8 @@
IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{TemplateBinding Padding}"
VirtualizationMode="{TemplateBinding VirtualizationMode}" />
Margin="{TemplateBinding Padding}"/>
</ScrollViewer>
</Border>
</ControlTemplate>

5
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml

@ -98,11 +98,8 @@
IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{TemplateBinding Padding}"
VirtualizationMode="{TemplateBinding VirtualizationMode}" />
Margin="{TemplateBinding Padding}"/>
</ScrollViewer>
</Border>
</ControlTemplate>

52
src/Avalonia.Controls/Carousel.cs

@ -2,7 +2,6 @@ using Avalonia.Animation;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.Input;
namespace Avalonia.Controls
{
@ -11,12 +10,6 @@ namespace Avalonia.Controls
/// </summary>
public class Carousel : SelectingItemsControl
{
/// <summary>
/// Defines the <see cref="IsVirtualized"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsVirtualizedProperty =
AvaloniaProperty.Register<Carousel, bool>(nameof(IsVirtualized), true);
/// <summary>
/// Defines the <see cref="PageTransition"/> property.
/// </summary>
@ -28,7 +21,9 @@ namespace Avalonia.Controls
/// <see cref="Carousel"/>.
/// </summary>
private static readonly ITemplate<Panel> PanelTemplate =
new FuncTemplate<Panel>(() => new Panel());
new FuncTemplate<Panel>(() => new VirtualizingCarouselPanel());
private IScrollable? _scroller;
/// <summary>
/// Initializes static members of the <see cref="Carousel"/> class.
@ -38,18 +33,6 @@ namespace Avalonia.Controls
SelectionModeProperty.OverrideDefaultValue<Carousel>(SelectionMode.AlwaysSelected);
ItemsPanelProperty.OverrideDefaultValue<Carousel>(PanelTemplate);
}
/// <summary>
/// Gets or sets a value indicating whether the items in the carousel are virtualized.
/// </summary>
/// <remarks>
/// When the carousel is virtualized, only the active page is held in memory.
/// </remarks>
public bool IsVirtualized
{
get { return GetValue(IsVirtualizedProperty); }
set { SetValue(IsVirtualizedProperty, value); }
}
/// <summary>
/// Gets or sets the transition to use when moving between pages.
@ -65,7 +48,7 @@ namespace Avalonia.Controls
/// </summary>
public void Next()
{
if (SelectedIndex < Items.Count() - 1)
if (SelectedIndex < ItemCount - 1)
{
++SelectedIndex;
}
@ -81,5 +64,32 @@ namespace Avalonia.Controls
--SelectedIndex;
}
}
protected override Size ArrangeOverride(Size finalSize)
{
var result = base.ArrangeOverride(finalSize);
if (_scroller is not null)
_scroller.Offset = new(SelectedIndex, 0);
return result;
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_scroller = e.NameScope.Find<IScrollable>("PART_ScrollViewer");
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == SelectedIndexProperty && _scroller is not null)
{
var value = change.GetNewValue<int>();
_scroller.Offset = new(value, 0);
}
}
}
}

34
src/Avalonia.Controls/ComboBox.cs

@ -53,12 +53,6 @@ namespace Avalonia.Controls
public static readonly DirectProperty<ComboBox, object?> SelectionBoxItemProperty =
AvaloniaProperty.RegisterDirect<ComboBox, object?>(nameof(SelectionBoxItem), o => o.SelectionBoxItem);
/// <summary>
/// Defines the <see cref="VirtualizationMode"/> property.
/// </summary>
public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty =
ItemsPresenter.VirtualizationModeProperty.AddOwner<ComboBox>();
/// <summary>
/// Defines the <see cref="PlaceholderText"/> property.
/// </summary>
@ -153,15 +147,6 @@ namespace Avalonia.Controls
set => SetValue(PlaceholderForegroundProperty, value);
}
/// <summary>
/// Gets or sets the virtualization mode for the items.
/// </summary>
public ItemVirtualizationMode VirtualizationMode
{
get => GetValue(VirtualizationModeProperty);
set => SetValue(VirtualizationModeProperty, value);
}
/// <summary>
/// Gets or sets the horizontal alignment of the content within the control.
/// </summary>
@ -180,16 +165,6 @@ namespace Avalonia.Controls
set => SetValue(VerticalContentAlignmentProperty, value);
}
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<ComboBoxItem>(
this,
ComboBoxItem.ContentProperty,
ComboBoxItem.ContentTemplateProperty);
}
/// <inheritdoc/>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
@ -202,6 +177,9 @@ namespace Avalonia.Controls
UpdateFlowDirection();
}
protected internal override Control CreateContainerForItemOverride() => new ComboBoxItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is ComboBoxItem;
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)
{
@ -490,11 +468,11 @@ namespace Avalonia.Controls
private void SelectFocusedItem()
{
foreach (ItemContainerInfo dropdownItem in ItemContainerGenerator.Containers)
foreach (var dropdownItem in GetRealizedContainers())
{
if (dropdownItem.ContainerControl.IsFocused)
if (dropdownItem.IsFocused)
{
SelectedIndex = dropdownItem.Index;
SelectedIndex = IndexFromContainer(dropdownItem);
break;
}
}

5
src/Avalonia.Controls/ContextMenu.cs

@ -316,11 +316,6 @@ namespace Avalonia.Controls
remove => _popupHostChangedHandler -= value;
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
}
private void Open(Control control, Control placementTarget, bool requestedByPointer)
{
if (IsOpen)

5
src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs

@ -35,11 +35,6 @@ namespace Avalonia.Controls
throw new NotSupportedException("Use MenuFlyout.ShowAt(Control) instead");
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);

114
src/Avalonia.Controls/Generators/IItemContainerGenerator.cs

@ -1,114 +0,0 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Styling;
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Creates containers for items and maintains a list of created containers.
/// </summary>
public interface IItemContainerGenerator
{
/// <summary>
/// Gets the currently realized containers.
/// </summary>
IEnumerable<ItemContainerInfo> Containers { get; }
/// <summary>
/// Gets or sets the theme to be applied to the items in the control.
/// </summary>
ControlTheme? ItemContainerTheme { get; set; }
/// <summary>
/// Gets or sets the data template used to display the items in the control.
/// </summary>
IDataTemplate? ItemTemplate { get; set; }
/// <summary>
/// Gets or sets the binding to use to bind to the member of an item used for displaying
/// </summary>
IBinding? DisplayMemberBinding { get; set; }
/// <summary>
/// Gets the ContainerType, or null if its an untyped ContainerGenerator.
/// </summary>
Type? ContainerType { get; }
/// <summary>
/// Signaled whenever new containers are materialized.
/// </summary>
event EventHandler<ItemContainerEventArgs>? Materialized;
/// <summary>
/// Event raised whenever containers are dematerialized.
/// </summary>
event EventHandler<ItemContainerEventArgs>? Dematerialized;
/// <summary>
/// Event raised whenever containers are recycled.
/// </summary>
event EventHandler<ItemContainerEventArgs>? Recycled;
/// <summary>
/// Creates a container control for an item.
/// </summary>
/// <param name="index">
/// The index of the item of data in the control's items.
/// </param>
/// <param name="item">The item.</param>
/// <returns>The created controls.</returns>
ItemContainerInfo Materialize(int index, object item);
/// <summary>
/// Removes a set of created containers.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item in the control's items.
/// </param>
/// <param name="count">The the number of items to remove.</param>
/// <returns>The removed containers.</returns>
IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count);
/// <summary>
/// Inserts space for newly inserted containers in the index.
/// </summary>
/// <param name="index">The index at which space should be inserted.</param>
/// <param name="count">The number of blank spaces to create.</param>
void InsertSpace(int index, int count);
/// <summary>
/// Removes a set of created containers and updates the index of later containers to fill
/// the gap.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item in the control's items.
/// </param>
/// <param name="count">The the number of items to remove.</param>
/// <returns>The removed containers.</returns>
IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count);
bool TryRecycle(int oldIndex, int newIndex, object item);
/// <summary>
/// Clears all created containers and returns the removed controls.
/// </summary>
/// <returns>The removed controls.</returns>
IEnumerable<ItemContainerInfo> Clear();
/// <summary>
/// Gets the container control representing the item with the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The container, or null if no container created.</returns>
Control? ContainerFromIndex(int index);
/// <summary>
/// Gets the index of the specified container control.
/// </summary>
/// <param name="container">The container.</param>
/// <returns>The index of the container, or -1 if not found.</returns>
int IndexFromContainer(Control? container);
}
}

18
src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs

@ -1,18 +0,0 @@
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Creates containers for tree items and maintains a list of created containers.
/// </summary>
public interface ITreeItemContainerGenerator : IItemContainerGenerator
{
/// <summary>
/// Gets the container index for the tree.
/// </summary>
TreeContainerIndex? Index { get; }
/// <summary>
/// Updates the index based on the parent <see cref="TreeView"/>.
/// </summary>
void UpdateIndex();
}
}

49
src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs

@ -1,49 +0,0 @@
using System;
using System.Collections.Generic;
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Provides details for the <see cref="IItemContainerGenerator.Materialized"/>
/// and <see cref="IItemContainerGenerator.Dematerialized"/> events.
/// </summary>
public class ItemContainerEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
/// </summary>
/// <param name="container">The container.</param>
public ItemContainerEventArgs(ItemContainerInfo container)
{
StartingIndex = container.Index;
Containers = new[] { container };
}
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
/// </summary>
/// <param name="startingIndex">The index of the first container in the source items.</param>
/// <param name="containers">The containers.</param>
/// <remarks>
/// TODO: Do we really need to pass in StartingIndex here? The ItemContainerInfo objects
/// have an index, and what happens if the contains passed in aren't sequential?
/// </remarks>
public ItemContainerEventArgs(
int startingIndex,
IList<ItemContainerInfo> containers)
{
StartingIndex = startingIndex;
Containers = containers;
}
/// <summary>
/// Gets the containers.
/// </summary>
public IList<ItemContainerInfo> Containers { get; }
/// <summary>
/// Gets the index of the first container in the source items.
/// </summary>
public int StartingIndex { get; }
}
}

313
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@ -1,262 +1,113 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Styling;
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Creates containers for items and maintains a list of created containers.
/// Generates containers for an <see cref="ItemsControl"/>.
/// </summary>
public class ItemContainerGenerator : IItemContainerGenerator
/// <remarks>
/// When creating a container for an item from a <see cref="VirtualizingPanel"/>, the following
/// method order should be followed:
///
/// - <see cref="IsItemItsOwnContainer(Control)"/> should first be called if the item is
/// derived from the <see cref="Control"/> class. If this method returns true then the
/// item itself should be used as the container.
/// - If <see cref="IsItemItsOwnContainer(Control)"/> returns false then
/// <see cref="CreateContainer"/> should be called to create a new container.
/// - <see cref="PrepareItemContainer(Control, object?, int)"/> method should be called for the
/// container.
/// - The container should then be added to the panel using
/// <see cref="VirtualizingPanel.AddInternalChild(Control)"/>
/// - Finally, <see cref="ItemContainerPrepared(Control, object?, int)"/> should be called.
/// - When the item is ready to be recycled, <see cref="ClearItemContainer(Control)"/> should
/// be called if <see cref="IsItemItsOwnContainer(Control)"/> returned false.
///
/// NOTE: Although this class is similar to that found in WPF/UWP, in Avalonia this class only
/// concerns itself with generating and clearing item containers; it does not maintain a
/// record of the currently realized containers, that responsibility is delegated to the
/// items panel.
/// </remarks>
public class ItemContainerGenerator
{
private SortedDictionary<int, ItemContainerInfo> _containers = new SortedDictionary<int, ItemContainerInfo>();
private readonly ItemsControl _owner;
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerGenerator"/> class.
/// </summary>
/// <param name="owner">The owner control.</param>
public ItemContainerGenerator(Control owner)
{
Owner = owner ?? throw new ArgumentNullException(nameof(owner));
}
/// <inheritdoc/>
public IEnumerable<ItemContainerInfo> Containers => _containers.Values;
/// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs>? Materialized;
/// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs>? Dematerialized;
/// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs>? Recycled;
internal ItemContainerGenerator(ItemsControl owner) => _owner = owner;
/// <summary>
/// Gets or sets the theme to be applied to the items in the control.
/// Creates a new container control.
/// </summary>
public ControlTheme? ItemContainerTheme { get; set; }
/// <returns>The newly created container control.</returns>
/// <remarks>
/// Before calling this method, <see cref="IsItemItsOwnContainer(Control)"/> should be
/// called to determine whether the item itself should be used as a container. After
/// calling this method, <see cref="PrepareItemContainer(Control, object, int)"/> should
/// be called to prepare the container to display the specified item.
/// </remarks>
public Control CreateContainer() => _owner.CreateContainerForItemOverride();
/// <summary>
/// Gets or sets the data template used to display the items in the control.
/// Determines whether the specified item is (or is eligible to be) its own container.
/// </summary>
public IDataTemplate? ItemTemplate { get; set; }
/// <inheritdoc />
public IBinding? DisplayMemberBinding { get; set; }
/// <param name="container">The item.</param>
/// <returns>true if the item is its own container, otherwise false.</returns>
/// <remarks>
/// Whereas in WPF/UWP, non-control items can be their own container, in Avalonia only
/// control items may be; the caller is responsible for checking if each item is a control
/// and calling this method before creating a new container.
/// </remarks>
public bool IsItemItsOwnContainer(Control container) => _owner.IsItemItsOwnContainerOverride(container);
/// <summary>
/// Gets the owner control.
/// Prepares the specified element as the container for the corresponding item.
/// </summary>
public Control Owner { get; }
/// <inheritdoc/>
public virtual Type? ContainerType => null;
/// <inheritdoc/>
public ItemContainerInfo Materialize(int index, object item)
{
var container = new ItemContainerInfo(CreateContainer(item)!, item, index);
_containers.Add(container.Index, container);
Materialized?.Invoke(this, new ItemContainerEventArgs(container));
return container;
}
/// <inheritdoc/>
public virtual IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count)
{
var result = new List<ItemContainerInfo>();
for (int i = startingIndex; i < startingIndex + count; ++i)
{
result.Add(_containers[i]);
_containers.Remove(i);
}
Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result));
return result;
}
/// <inheritdoc/>
public virtual void InsertSpace(int index, int count)
{
if (count > 0)
{
var toMove = _containers.Where(x => x.Key >= index)
.OrderByDescending(x => x.Key)
.ToArray();
foreach (var i in toMove)
{
_containers.Remove(i.Key);
i.Value.Index += count;
_containers.Add(i.Value.Index, i.Value);
}
}
}
/// <inheritdoc/>
public virtual IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count)
{
var result = new List<ItemContainerInfo>();
if (count > 0)
{
for (var i = startingIndex; i < startingIndex + count; ++i)
{
if (_containers.TryGetValue(i, out var found))
{
result.Add(found);
}
_containers.Remove(i);
}
var toMove = _containers.Where(x => x.Key >= startingIndex)
.OrderBy(x => x.Key).ToArray();
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.Length > 0)
{
var containers = toMove.Select(x => x.Value).ToArray();
Recycled?.Invoke(this, new ItemContainerEventArgs(containers[0].Index, containers));
}
}
return result;
}
/// <inheritdoc/>
public virtual bool TryRecycle(int oldIndex, int newIndex, object item) => false;
/// <inheritdoc/>
public virtual IEnumerable<ItemContainerInfo> Clear()
{
var result = Containers.ToArray();
_containers.Clear();
if (result.Length > 0)
{
Dematerialized?.Invoke(this, new ItemContainerEventArgs(0, result));
}
return result;
}
/// <inheritdoc/>
public Control? ContainerFromIndex(int index)
{
ItemContainerInfo? result;
_containers.TryGetValue(index, out result);
return result?.ContainerControl;
}
/// <inheritdoc/>
public int IndexFromContainer(Control? container)
{
foreach (var i in _containers)
{
if (i.Value.ContainerControl == container)
{
return i.Key;
}
}
return -1;
}
/// <param name="container">The element that's used to display the specified item.</param>
/// <param name="item">The item to display.</param>
/// <param name="index">The index of the item to display.</param>
/// <remarks>
/// If <see cref="IsItemItsOwnContainer(Control)"/> is true for an item, then this method
/// only needs to be called a single time, otherwise this method should be called after the
/// container is created, and each subsequent time the container is recycled to display a
/// new item.
/// </remarks>
public void PrepareItemContainer(Control container, object? item, int index) =>
_owner.PrepareItemContainer(container, item, index);
/// <summary>
/// Creates the container for an item.
/// Notifies the <see cref="ItemsControl"/> that a container has been fully prepared to
/// display an item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>The created container control.</returns>
protected virtual Control? CreateContainer(object item)
{
var result = item as Control;
if (result == null)
{
result = new ContentPresenter();
if (DisplayMemberBinding is not null)
{
result.SetValue(StyledElement.DataContextProperty, item, BindingPriority.Style);
result.Bind(ContentPresenter.ContentProperty, DisplayMemberBinding, BindingPriority.Style);
}
else
{
result.SetValue(ContentPresenter.ContentProperty, item, BindingPriority.Style);
}
if (ItemTemplate != null)
{
result.SetValue(
ContentPresenter.ContentTemplateProperty,
ItemTemplate,
BindingPriority.Style);
}
}
if (ItemContainerTheme != null)
{
result.SetValue(
StyledElement.ThemeProperty,
ItemContainerTheme,
BindingPriority.Template);
}
return result;
}
/// <param name="container">The container control.</param>
/// <param name="item">The item being displayed.</param>
/// <param name="index">The index of the item being displayed.</param>
/// <remarks>
/// This method should be called when a container has been fully prepared and added
/// to the logical and visual trees, but may be called before a layout pass has completed.
/// It should be called regardless of the result of
/// <see cref="IsItemItsOwnContainer(Control)"/>.
/// </remarks>
public void ItemContainerPrepared(Control container, object? item, int index) =>
_owner.ItemContainerPrepared(container, item, index);
/// <summary>
/// Moves a container.
/// Called when the index for a container changes due to an insertion or removal in the
/// items collection.
/// </summary>
/// <param name="container">The container whose index changed.</param>
/// <param name="oldIndex">The old index.</param>
/// <param name="newIndex">The new index.</param>
/// <param name="item">The new item.</param>
/// <returns>The container info.</returns>
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;
}
public void ItemContainerIndexChanged(Control container, int oldIndex, int newIndex) =>
_owner.ItemContainerIndexChanged(container, oldIndex, newIndex);
/// <summary>
/// Gets all containers with an index that fall within a range.
/// Undoes the effects of the <see cref="PrepareItemContainer(Control, object, int)"/> method.
/// </summary>
/// <param name="index">The first index.</param>
/// <param name="count">The number of elements in the range.</param>
/// <returns>The containers.</returns>
protected IEnumerable<ItemContainerInfo> GetContainerRange(int index, int count)
{
return _containers.Where(x => x.Key >= index && x.Key < index + count).Select(x => x.Value);
}
/// <param name="container">The container control.</param>
public void ClearItemContainer(Control container) => _owner.ClearItemContainer(container);
/// <summary>
/// Raises the <see cref="Recycled"/> event.
/// </summary>
/// <param name="e">The event args.</param>
protected void RaiseRecycled(ItemContainerEventArgs e)
{
Recycled?.Invoke(this, e);
}
[Obsolete("Use ItemsControl.ContainerFromIndex")]
public Control? ContainerFromIndex(int index) => _owner.ContainerFromIndex(index);
[Obsolete("Use ItemsControl.IndexFromContainer")]
public int IndexFromContainer(Control container) => _owner.IndexFromContainer(container);
}
}

102
src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs

@ -1,102 +0,0 @@
using System;
using Avalonia.Data;
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Creates containers for items and maintains a list of created containers.
/// </summary>
/// <typeparam name="T">The type of the container.</typeparam>
public class ItemContainerGenerator<T> : ItemContainerGenerator where T : Control, new()
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerGenerator{T}"/> class.
/// </summary>
/// <param name="owner">The owner control.</param>
/// <param name="contentProperty">The container's Content property.</param>
/// <param name="contentTemplateProperty">The container's ContentTemplate property.</param>
public ItemContainerGenerator(
Control owner,
AvaloniaProperty contentProperty,
AvaloniaProperty? contentTemplateProperty)
: base(owner)
{
ContentProperty = contentProperty ?? throw new ArgumentNullException(nameof(contentProperty));
ContentTemplateProperty = contentTemplateProperty;
}
/// <inheritdoc/>
public override Type ContainerType => typeof(T);
/// <summary>
/// Gets the container's Content property.
/// </summary>
protected AvaloniaProperty ContentProperty { get; }
/// <summary>
/// Gets the container's ContentTemplate property.
/// </summary>
protected AvaloniaProperty? ContentTemplateProperty { get; }
/// <inheritdoc/>
protected override Control? CreateContainer(object item)
{
var container = item as T;
if (container is null)
{
container = new T();
if (ContentTemplateProperty != null)
{
container.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style);
}
if (DisplayMemberBinding is not null)
{
container.SetValue(StyledElement.DataContextProperty, item, BindingPriority.Style);
container.Bind(ContentProperty, DisplayMemberBinding, BindingPriority.Style);
}
else
{
container.SetValue(ContentProperty, item, BindingPriority.Style);
}
if (!(item is Control))
{
container.DataContext = item;
}
}
if (ItemContainerTheme != null)
{
container.SetValue(StyledElement.ThemeProperty, ItemContainerTheme, BindingPriority.Style);
}
return container;
}
/// <inheritdoc/>
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 Control))
{
container.DataContext = item;
}
var info = MoveContainer(oldIndex, newIndex, item);
RaiseRecycled(new ItemContainerEventArgs(info));
return true;
}
}
}

42
src/Avalonia.Controls/Generators/ItemContainerInfo.cs

@ -1,42 +0,0 @@
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Holds information about an item container generated by an
/// <see cref="IItemContainerGenerator"/>.
/// </summary>
public class ItemContainerInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerInfo"/> class.
/// </summary>
/// <param name="container">The container control.</param>
/// <param name="item">The item that the container represents.</param>
/// <param name="index">
/// The index of the item in the <see cref="ItemsControl.Items"/> collection.
/// </param>
public ItemContainerInfo(Control container, object item, int index)
{
ContainerControl = container;
Item = item;
Index = index;
}
/// <summary>
/// Gets the container control.
/// </summary>
/// <remarks>
/// This will be null if <see cref="Item"/> is null.
/// </remarks>
public Control ContainerControl { get; }
/// <summary>
/// Gets the item that the container represents.
/// </summary>
public object Item { get; internal set; }
/// <summary>
/// Gets the index of the item in the <see cref="ItemsControl.Items"/> collection.
/// </summary>
public int Index { get; set; }
}
}

21
src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs

@ -1,21 +0,0 @@
namespace Avalonia.Controls.Generators
{
public class MenuItemContainerGenerator : ItemContainerGenerator<MenuItem>
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerGenerator{T}"/> class.
/// </summary>
/// <param name="owner">The owner control.</param>
public MenuItemContainerGenerator(Control owner)
: base(owner, MenuItem.HeaderProperty, null)
{
}
/// <inheritdoc/>
protected override Control? CreateContainer(object item)
{
var separator = item as Separator;
return separator != null ? separator : base.CreateContainer(item);
}
}
}

105
src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs

@ -1,105 +0,0 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.LogicalTree;
using Avalonia.Reactive;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Generators
{
public class TabItemContainerGenerator : ItemContainerGenerator<TabItem>
{
public TabItemContainerGenerator(TabControl owner)
: base(owner, ContentControl.ContentProperty, ContentControl.ContentTemplateProperty)
{
Owner = owner;
}
public new TabControl Owner { get; }
protected override Control CreateContainer(object item)
{
var tabItem = (TabItem)base.CreateContainer(item)!;
tabItem.Bind(TabItem.TabStripPlacementProperty, new OwnerBinding<Dock>(
tabItem,
TabControl.TabStripPlacementProperty));
if (tabItem.HeaderTemplate == null)
{
tabItem.Bind(TabItem.HeaderTemplateProperty, new OwnerBinding<IDataTemplate?>(
tabItem,
TabControl.ItemTemplateProperty));
}
if (Owner.HeaderDisplayMemberBinding is not null)
{
tabItem.Bind(HeaderedContentControl.HeaderProperty, Owner.HeaderDisplayMemberBinding,
BindingPriority.Style);
}
if (tabItem.Header == null)
{
if (item is IHeadered headered)
{
tabItem.Header = headered.Header;
}
else
{
if (!(tabItem.DataContext is Control))
{
tabItem.Header = tabItem.DataContext;
}
}
}
if (!(tabItem.Content is Control))
{
tabItem.Bind(TabItem.ContentTemplateProperty, new OwnerBinding<IDataTemplate?>(
tabItem,
TabControl.ContentTemplateProperty));
}
return tabItem;
}
private class OwnerBinding<T> : SingleSubscriberObservableBase<T>
{
private readonly TabItem _item;
private readonly StyledProperty<T> _ownerProperty;
private IDisposable? _ownerSubscription;
private IDisposable? _propertySubscription;
public OwnerBinding(TabItem item, StyledProperty<T> ownerProperty)
{
_item = item;
_ownerProperty = ownerProperty;
}
protected override void Subscribed()
{
_ownerSubscription = ControlLocator.Track(_item, 0, typeof(TabControl)).Subscribe(OwnerChanged);
}
protected override void Unsubscribed()
{
_ownerSubscription?.Dispose();
_ownerSubscription = null;
}
private void OwnerChanged(ILogical? c)
{
_propertySubscription?.Dispose();
_propertySubscription = null;
if (c is TabControl tabControl)
{
_propertySubscription = tabControl.GetObservable(_ownerProperty)
.Subscribe(x => PublishNext(x));
}
}
}
}
}

166
src/Avalonia.Controls/Generators/TreeContainerIndex.cs

@ -1,166 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Maintains an index of all item containers currently materialized by a <see cref="TreeView"/>.
/// </summary>
/// <remarks>
/// Each <see cref="TreeViewItem"/> has its own <see cref="TreeItemContainerGenerator{T}"/>
/// that maintains the list of its direct children, but they also share an instance of this
/// class in their <see cref="TreeItemContainerGenerator{T}.Index"/> property which tracks
/// the containers materialized for the entire tree.
/// </remarks>
public class TreeContainerIndex
{
private readonly Dictionary<object, HashSet<Control>> _itemToContainerSet = new Dictionary<object, HashSet<Control>>();
private readonly Dictionary<object, Control> _itemToContainer = new Dictionary<object, Control>();
private readonly Dictionary<Control, object> _containerToItem = new Dictionary<Control, object>();
/// <summary>
/// Signaled whenever new containers are materialized.
/// </summary>
public event EventHandler<ItemContainerEventArgs>? Materialized;
/// <summary>
/// Event raised whenever containers are dematerialized.
/// </summary>
public event EventHandler<ItemContainerEventArgs>? Dematerialized;
/// <summary>
/// Gets the currently materialized containers.
/// </summary>
public IEnumerable<Control> Containers => _containerToItem.Keys;
/// <summary>
/// Gets the items of currently materialized containers.
/// </summary>
public IEnumerable<object> Items => _containerToItem.Values;
/// <summary>
/// Adds an entry to the index.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="container">The item container.</param>
public void Add(object item, Control container)
{
_itemToContainer[item] = container;
if (_itemToContainerSet.TryGetValue(item, out var set))
{
set.Add(container);
}
else
{
_itemToContainerSet.Add(item, new HashSet<Control> { container });
}
_containerToItem.Add(container, item);
Materialized?.Invoke(
this,
new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0)));
}
/// <summary>
/// Removes a container from private collections.
/// </summary>
/// <param name="container">The item container.</param>
/// <param name="item">The DataContext object</param>
private void RemoveContainer(Control container, object item)
{
if (_itemToContainerSet.TryGetValue(item, out var set))
{
set.Remove(container);
if (set.Count == 0)
{
_itemToContainerSet.Remove(item);
_itemToContainer.Remove(item);
}
else
{
_itemToContainer[item] = set.First();
}
}
}
/// <summary>
/// Removes a container from the index.
/// </summary>
/// <param name="container">The item container.</param>
public void Remove(Control container)
{
var item = _containerToItem[container];
_containerToItem.Remove(container);
RemoveContainer(container, item);
Dematerialized?.Invoke(
this,
new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0)));
}
/// <summary>
/// Removes a set of containers from the index.
/// </summary>
/// <param name="startingIndex">The index of the first item.</param>
/// <param name="containers">The item containers.</param>
public void Remove(int startingIndex, IEnumerable<ItemContainerInfo> containers)
{
foreach (var container in containers)
{
var item = _containerToItem[container.ContainerControl];
_containerToItem.Remove(container.ContainerControl);
RemoveContainer(container.ContainerControl, item);
}
Dematerialized?.Invoke(
this,
new ItemContainerEventArgs(startingIndex, containers.ToList()));
}
/// <summary>
/// Gets the container for an item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>The container, or null of not found.</returns>
public Control? ContainerFromItem(object item)
{
if (item != null)
{
_itemToContainer.TryGetValue(item, out var result);
if (result == null)
{
_itemToContainerSet.TryGetValue(item, out var set);
if (set?.Count > 0)
{
return set.FirstOrDefault();
}
}
return result;
}
return null;
}
/// <summary>
/// Gets the item for a container.
/// </summary>
/// <param name="container">The container.</param>
/// <returns>The item, or null of not found.</returns>
public object? ItemFromContainer(Control? container)
{
if (container != null)
{
_containerToItem.TryGetValue(container, out var result);
if (result != null)
{
_itemToContainer[result] = container;
}
return result;
}
return null;
}
}
}

166
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

@ -1,164 +1,32 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.LogicalTree;
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Creates containers for tree items and maintains a list of created containers.
/// </summary>
/// <typeparam name="T">The type of the container.</typeparam>
public class TreeItemContainerGenerator<T> : ItemContainerGenerator<T>, ITreeItemContainerGenerator
where T : Control, new()
public class TreeItemContainerGenerator : ItemContainerGenerator
{
private TreeView? _treeView;
/// <summary>
/// Initializes a new instance of the <see cref="TreeItemContainerGenerator{T}"/> class.
/// </summary>
/// <param name="owner">The owner control.</param>
/// <param name="contentProperty">The container's Content property.</param>
/// <param name="contentTemplateProperty">The container's ContentTemplate property.</param>
/// <param name="itemsProperty">The container's Items property.</param>
/// <param name="isExpandedProperty">The container's IsExpanded property.</param>
public TreeItemContainerGenerator(
Control owner,
AvaloniaProperty contentProperty,
AvaloniaProperty contentTemplateProperty,
AvaloniaProperty itemsProperty,
AvaloniaProperty isExpandedProperty)
: base(owner, contentProperty, contentTemplateProperty)
{
ItemsProperty = itemsProperty ?? throw new ArgumentNullException(nameof(itemsProperty));
IsExpandedProperty = isExpandedProperty ?? throw new ArgumentNullException(nameof(isExpandedProperty));
UpdateIndex();
}
/// <summary>
/// Gets the container index for the tree.
/// </summary>
public TreeContainerIndex? Index { get; private set; }
/// <summary>
/// Gets the item container's Items property.
/// </summary>
protected AvaloniaProperty ItemsProperty { get; }
/// <summary>
/// Gets the item container's IsExpanded property.
/// </summary>
protected AvaloniaProperty IsExpandedProperty { get; }
/// <inheritdoc/>
protected override Control? 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();
if (ItemContainerTheme != null)
{
result.SetValue(Control.ThemeProperty, ItemContainerTheme, BindingPriority.Style);
}
if (DisplayMemberBinding is not null)
{
result.SetValue(StyledElement.DataContextProperty, item, BindingPriority.Style);
result.Bind(ContentProperty, DisplayMemberBinding, BindingPriority.Style);
}
else
{
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 Control))
{
result.DataContext = item;
}
Index?.Add(item, result);
return result;
}
}
public override IEnumerable<ItemContainerInfo> Clear()
internal TreeItemContainerGenerator(TreeView owner)
: base(owner)
{
var items = base.Clear();
Index?.Remove(0, items);
return items;
Index = new TreeContainerIndex(owner);
}
public override IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count)
{
Index?.Remove(startingIndex, GetContainerRange(startingIndex, count));
return base.Dematerialize(startingIndex, count);
}
public TreeContainerIndex Index { get; }
}
public override IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count)
{
Index?.Remove(startingIndex, GetContainerRange(startingIndex, count));
return base.RemoveRange(startingIndex, count);
}
public class TreeContainerIndex
{
private readonly TreeView _owner;
public override bool TryRecycle(int oldIndex, int newIndex, object item) => false;
internal TreeContainerIndex(TreeView owner) => _owner = owner;
public void UpdateIndex()
{
if (Owner is TreeView treeViewOwner && Index == null)
{
Index = new TreeContainerIndex();
_treeView = treeViewOwner;
}
else
{
var treeView = Owner.GetSelfAndLogicalAncestors().OfType<TreeView>().FirstOrDefault();
if (treeView != _treeView)
{
Clear();
Index = treeView?.ItemContainerGenerator?.Index;
_treeView = treeView;
}
}
}
[Obsolete("Use TreeView.GetRealizedTreeContainers")]
public IEnumerable<Control> Containers => _owner.GetRealizedTreeContainers();
class WrapperTreeDataTemplate : ITreeDataTemplate
{
private readonly IDataTemplate _inner;
public WrapperTreeDataTemplate(IDataTemplate inner) => _inner = inner;
public Control? Build(object? param) => _inner.Build(param);
public bool Match(object? data) => _inner.Match(data);
public InstancedBinding? ItemsSelector(object item) => null;
}
[Obsolete("Use TreeView.TreeContainerFromItem")]
public Control? ContainerFromItem(object item) => _owner.TreeContainerFromItem(item);
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;
}
[Obsolete("Use TreeView.TreeItemFromContainer")]
public object? ItemFromContainer(Control container) => _owner.TreeItemFromContainer(container);
}
}

19
src/Avalonia.Controls/IVirtualizingController.cs

@ -1,19 +0,0 @@
namespace Avalonia.Controls
{
/// <summary>
/// Interface implemented by controls that act as controllers for an
/// <see cref="IVirtualizingPanel"/>.
/// </summary>
public interface IVirtualizingController
{
/// <summary>
/// Called when the <see cref="IVirtualizingPanel"/>'s controls should be updated.
/// </summary>
/// <remarks>
/// The controller should respond to this method being called by either adding
/// children up until <see cref="IVirtualizingPanel.IsFull"/> becomes true or
/// removing <see cref="IVirtualizingPanel.OverflowCount"/> controls.
/// </remarks>
void UpdateControls();
}
}

89
src/Avalonia.Controls/IVirtualizingPanel.cs

@ -1,89 +0,0 @@
using Avalonia.Layout;
namespace Avalonia.Controls
{
/// <summary>
/// A panel that can be used to virtualize items.
/// </summary>
public interface IVirtualizingPanel
{
/// <summary>
/// Gets the children of the panel.
/// </summary>
Controls Children { get; }
/// <summary>
/// Gets or sets the controller for the virtualizing panel.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
IVirtualizingController? Controller { get; set; }
/// <summary>
/// Gets a value indicating whether the panel is full.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
bool IsFull { get; }
/// <summary>
/// Gets the number of items that can be removed while keeping the panel full.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
int OverflowCount { get; }
/// <summary>
/// Gets the direction of scroll.
/// </summary>
Orientation ScrollDirection { get; }
/// <summary>
/// Gets the average size of the materialized items in the direction of scroll.
/// </summary>
double AverageItemSize { get; }
/// <summary>
/// Gets or sets a size in pixels by which the content is overflowing the panel, in the
/// direction of scroll.
/// </summary>
/// <remarks>
/// This may be non-zero even when <see cref="OverflowCount"/> is zero if the last item
/// overflows the panel bounds.
/// </remarks>
double PixelOverflow { get; }
/// <summary>
/// Gets or sets the current pixel offset of the items in the direction of scroll.
/// </summary>
double PixelOffset { get; set; }
/// <summary>
/// Gets or sets the current scroll offset in the cross axis.
/// </summary>
double CrossAxisOffset { get; set; }
/// <summary>
/// Invalidates the measure of the control and forces a call to
/// <see cref="IVirtualizingController.UpdateControls"/> on the next measure.
/// </summary>
/// <remarks>
/// The implementation for this method should call
/// <see cref="Layoutable.InvalidateMeasure"/> and also ensure that the next call to
/// <see cref="Layoutable.Measure(Size)"/> calls
/// <see cref="IVirtualizingController.UpdateControls"/> on the next measure even if
/// the available size hasn't changed.
/// </remarks>
void ForceInvalidateMeasure();
}
}

18
src/Avalonia.Controls/ItemVirtualizationMode.cs

@ -1,18 +0,0 @@
namespace Avalonia.Controls
{
/// <summary>
/// Describes the item virtualization method to use for a list.
/// </summary>
public enum ItemVirtualizationMode
{
/// <summary>
/// Do not virtualize items.
/// </summary>
None,
/// <summary>
/// Virtualize items without smooth scrolling.
/// </summary>
Simple,
}
}

532
src/Avalonia.Controls/ItemsControl.cs

@ -2,21 +2,20 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Collections;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Automation.Peers;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.VisualTree;
using Avalonia.Styling;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@ -24,7 +23,7 @@ namespace Avalonia.Controls
/// Displays a collection of items.
/// </summary>
[PseudoClasses(":empty", ":singleitem")]
public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener, IChildIndexProvider
public class ItemsControl : TemplatedControl, IChildIndexProvider
{
/// <summary>
/// The default value for the <see cref="ItemsPanel"/> property.
@ -62,6 +61,11 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
AvaloniaProperty.Register<ItemsControl, IDataTemplate?>(nameof(ItemTemplate));
/// <summary>
/// Defines the <see cref="ItemsView"/> property.
/// </summary>
public static readonly DirectProperty<ItemsControl, ItemsSourceView> ItemsViewProperty =
AvaloniaProperty.RegisterDirect<ItemsControl, ItemsSourceView>(nameof(ItemsView), o => o.ItemsView);
/// <summary>
/// Defines the <see cref="DisplayMemberBinding" /> property
@ -80,49 +84,32 @@ namespace Avalonia.Controls
}
private IEnumerable? _items = new AvaloniaList<object>();
private ItemsSourceView _itemsView;
private int _itemCount;
private IItemContainerGenerator? _itemContainerGenerator;
private ItemContainerGenerator? _itemContainerGenerator;
private EventHandler<ChildIndexChangedEventArgs>? _childIndexChanged;
/// <summary>
/// Initializes static members of the <see cref="ItemsControl"/> class.
/// </summary>
static ItemsControl()
{
ItemsProperty.Changed.AddClassHandler<ItemsControl>((x, e) => x.ItemsChanged(e));
ItemTemplateProperty.Changed.AddClassHandler<ItemsControl>((x, e) => x.ItemTemplateChanged(e));
}
private IDataTemplate? _displayMemberItemTemplate;
private Tuple<int, Control>? _containerBeingPrepared;
private ScrollViewer? _scrollViewer;
/// <summary>
/// Initializes a new instance of the <see cref="ItemsControl"/> class.
/// </summary>
public ItemsControl()
{
_itemsView = ItemsSourceView.GetOrCreate(_items);
_itemsView.PostCollectionChanged += ItemsCollectionChanged;
UpdatePseudoClasses(0);
SubscribeToItems(_items);
}
/// <summary>
/// Gets the <see cref="IItemContainerGenerator"/> for the control.
/// Gets the <see cref="ItemContainerGenerator"/> for the control.
/// </summary>
public IItemContainerGenerator ItemContainerGenerator
public ItemContainerGenerator ItemContainerGenerator
{
get
{
if (_itemContainerGenerator == null)
{
_itemContainerGenerator = CreateItemContainerGenerator();
_itemContainerGenerator.ItemContainerTheme = ItemContainerTheme;
_itemContainerGenerator.ItemTemplate = ItemTemplate;
_itemContainerGenerator.DisplayMemberBinding = DisplayMemberBinding;
_itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e);
_itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e);
_itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e);
}
return _itemContainerGenerator;
}
#pragma warning disable CS0612 // Type or member is obsolete
get => _itemContainerGenerator ??= CreateItemContainerGenerator();
#pragma warning restore CS0612 // Type or member is obsolete
}
/// <summary>
@ -174,10 +161,38 @@ namespace Avalonia.Controls
/// <summary>
/// Gets the items presenter control.
/// </summary>
public IItemsPresenter? Presenter
{
get;
protected set;
public ItemsPresenter? Presenter { get; private set; }
/// <summary>
/// Gets the <see cref="Panel"/> specified by <see cref="ItemsPanel"/>.
/// </summary>
public Panel? ItemsPanelRoot => Presenter?.Panel;
/// <summary>
/// Gets a standardized view over <see cref="Items"/>.
/// </summary>
/// <remarks>
/// The <see cref="Items"/> property may be an enumerable which does not implement
/// <see cref="IList"/> or may be null. This view can be used to provide a standardized
/// view of the current items regardless of the type of the concrete collection, and
/// without having to deal with null values.
/// </remarks>
public ItemsSourceView ItemsView
{
get => _itemsView;
private set
{
if (ReferenceEquals(_itemsView, value))
return;
var oldValue = _itemsView;
RemoveControlItemsFromLogicalChildren(_itemsView);
_itemsView.PostCollectionChanged -= ItemsCollectionChanged;
_itemsView = value;
_itemsView.PostCollectionChanged += ItemsCollectionChanged;
AddControlItemsToLogicalChildren(_itemsView);
RaisePropertyChanged(ItemsViewProperty, oldValue, _itemsView);
}
}
private protected bool WrapFocus { get; set; }
@ -188,144 +203,158 @@ namespace Avalonia.Controls
remove => _childIndexChanged -= value;
}
/// <inheritdoc/>
void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter)
{
if (Presenter is IChildIndexProvider oldInnerProvider)
{
oldInnerProvider.ChildIndexChanged -= PresenterChildIndexChanged;
}
Presenter = presenter;
ItemContainerGenerator?.Clear();
if (Presenter is IChildIndexProvider innerProvider)
{
innerProvider.ChildIndexChanged += PresenterChildIndexChanged;
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty);
}
}
/// <summary>
/// Returns the container for the item at the specified index.
/// </summary>
/// <param name="index">The index of the item to retrieve.</param>
/// <returns>
/// The container for the item at the specified index within the item collection, if the
/// item has a container; otherwise, null.
/// </returns>
public Control? ContainerFromIndex(int index) => Presenter?.ContainerFromIndex(index);
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
/// <summary>
/// Returns the container corresponding to the specified item.
/// </summary>
/// <param name="item">The item to retrieve the container for.</param>
/// <returns>
/// A container that corresponds to the specified item, if the item has a container and
/// exists in the collection; otherwise, null.
/// </returns>
public Control? ContainerFromItem(object item)
{
var index = ItemsView.IndexOf(item);
return index >= 0 ? ContainerFromIndex(index) : null;
}
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
}
/// <summary>
/// Returns the index to the item that has the specified, generated container.
/// </summary>
/// <param name="container">The generated container to retrieve the item index for.</param>
/// <returns>
/// The index to the item that corresponds to the specified generated container, or -1 if
/// <paramref name="container"/> is not found.
/// </returns>
public int IndexFromContainer(Control container) => Presenter?.IndexFromContainer(container) ?? -1;
void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
/// <summary>
/// Returns the item that corresponds to the specified, generated container.
/// </summary>
/// <param name="container">The control that corresponds to the item to be returned.</param>
/// <returns>
/// The contained item, or the container if it does not contain an item.
/// </returns>
public object? ItemFromContainer(Control container)
{
ItemsCollectionChanged(sender, e);
var index = IndexFromContainer(container);
return index >= 0 && index < ItemsView.Count ? ItemsView[index] : null;
}
/// <summary>
/// Gets the item at the specified index in a collection.
/// Gets the currently realized containers.
/// </summary>
public IEnumerable<Control> GetRealizedContainers() => Presenter?.GetRealizedContainers() ?? Array.Empty<Control>();
/// <summary>
/// Creates or a container that can be used to display an item.
/// </summary>
protected internal virtual Control CreateContainerForItemOverride() => new ContentPresenter();
/// <summary>
/// Prepares the specified element to display the specified item.
/// </summary>
/// <param name="items">The collection.</param>
/// <param name="index">The index.</param>
/// <returns>The item at the given index or null if the index is out of bounds.</returns>
protected static object? ElementAt(IEnumerable? items, int index)
/// <param name="container">The element that's used to display the specified item.</param>
/// <param name="item">The item to display.</param>
/// <param name="index">The index of the item to display.</param>
protected internal virtual void PrepareContainerForItemOverride(Control container, object? item, int index)
{
if (index != -1 && index < items.Count())
if (container == item)
return;
var itemTemplate = GetEffectiveItemTemplate();
if (container is HeaderedContentControl hcc)
{
return items!.ElementAt(index) ?? null;
hcc.Content = item;
if (item is IHeadered headered)
hcc.Header = headered.Header;
else if (item is not Visual)
hcc.Header = item;
if (itemTemplate is not null)
hcc.HeaderTemplate = itemTemplate;
}
else
else if (container is ContentControl cc)
{
return null;
cc.Content = item;
if (itemTemplate is not null)
cc.ContentTemplate = itemTemplate;
}
}
/// <summary>
/// Gets the index of an item in a collection.
/// </summary>
/// <param name="items">The collection.</param>
/// <param name="item">The item.</param>
/// <returns>The index of the item or -1 if the item was not found.</returns>
protected static int IndexOf(IEnumerable? items, object item)
{
if (items != null && item != null)
else if (container is ContentPresenter p)
{
p.Content = item;
if (itemTemplate is not null)
p.ContentTemplate = itemTemplate;
}
else if (container is ItemsControl ic)
{
var list = items as IList;
if (itemTemplate is not null)
ic.ItemTemplate = itemTemplate;
if (ItemContainerTheme is { } ict && !ict.IsSet(ItemContainerThemeProperty))
ic.ItemContainerTheme = ict;
}
if (list != null)
{
return list.IndexOf(item);
}
else
{
int index = 0;
// This condition is separate because HeaderedItemsControl needs to also run the
// ItemsControl preparation.
if (container is HeaderedItemsControl hic)
{
hic.Header = item;
hic.HeaderTemplate = itemTemplate;
foreach (var i in items)
{
if (Equals(i, item))
{
return index;
}
itemTemplate ??= hic.FindDataTemplate(item) ?? this.FindDataTemplate(item);
++index;
}
if (itemTemplate is ITreeDataTemplate treeTemplate)
{
if (item is not null && treeTemplate.ItemsSelector(item) is { } itemsBinding)
BindingOperations.Apply(hic, ItemsProperty, itemsBinding, null);
}
}
return -1;
}
/// <summary>
/// Creates the <see cref="ItemContainerGenerator"/> for the control.
/// Called when the index for a container changes due to an insertion or removal in the
/// items collection.
/// </summary>
/// <returns>
/// An <see cref="IItemContainerGenerator"/>.
/// </returns>
protected virtual IItemContainerGenerator CreateItemContainerGenerator()
/// <param name="container">The container whose index changed.</param>
/// <param name="oldIndex">The old index.</param>
/// <param name="newIndex">The new index.</param>
protected virtual void ContainerIndexChangedOverride(Control container, int oldIndex, int newIndex)
{
return new ItemContainerGenerator(this);
}
/// <summary>
/// Called when new containers are materialized for the <see cref="ItemsControl"/> by its
/// <see cref="ItemContainerGenerator"/>.
/// Undoes the effects of the <see cref="PrepareContainerForItemOverride(Control, object?, int)"/> method.
/// </summary>
/// <param name="e">The details of the containers.</param>
protected virtual void OnContainersMaterialized(ItemContainerEventArgs e)
/// <param name="container">The container element.</param>
protected internal virtual void ClearContainerForItemOverride(Control container)
{
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);
}
}
// Feels like we should be clearing the HeaderedItemsControl.Items binding here, but looking at
// the WPF source it seems that this isn't done there.
}
/// <summary>
/// Called when containers are dematerialized for the <see cref="ItemsControl"/> by its
/// <see cref="ItemContainerGenerator"/>.
/// Determines whether the specified item is (or is eligible to be) its own container.
/// </summary>
/// <param name="e">The details of the containers.</param>
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);
}
}
}
/// <param name="item">The item to check.</param>
/// <returns>true if the item is (or is eligible to be) its own container; otherwise, false.</returns>
protected internal virtual bool IsItemItsOwnContainerOverride(Control item) => true;
/// <summary>
/// Called when containers are recycled for the <see cref="ItemsControl"/> by its
/// <see cref="ItemContainerGenerator"/>.
/// </summary>
/// <param name="e">The details of the containers.</param>
protected virtual void OnContainersRecycled(ItemContainerEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_scrollViewer = e.NameScope.Find<ScrollViewer>("PART_ScrollViewer");
}
/// <summary>
@ -381,51 +410,51 @@ namespace Avalonia.Controls
{
base.OnPropertyChanged(change);
if (change.Property == ItemCountProperty)
if (change.Property == ItemsProperty)
{
ItemsView = ItemsSourceView.GetOrCreate(change.GetNewValue<IEnumerable?>());
ItemCount = ItemsView.Count;
}
else if (change.Property == ItemCountProperty)
{
UpdatePseudoClasses(change.GetNewValue<int>());
}
else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null)
{
_itemContainerGenerator.ItemContainerTheme = change.GetNewValue<ControlTheme?>();
RefreshContainers();
}
}
/// <summary>
/// Called when the <see cref="Items"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldValue = e.OldValue as IEnumerable;
var newValue = e.NewValue as IEnumerable;
if (oldValue is INotifyCollectionChanged incc)
else if (change.Property == ItemTemplateProperty)
{
CollectionChangedEventManager.Instance.RemoveListener(incc, this);
if (change.NewValue is not null && DisplayMemberBinding is not null)
throw new InvalidOperationException("Cannot set both DisplayMemberBinding and ItemTemplate.");
RefreshContainers();
}
UpdateItemCount();
RemoveControlItemsFromLogicalChildren(oldValue);
AddControlItemsToLogicalChildren(newValue);
if (Presenter != null)
else if (change.Property == DisplayMemberBindingProperty)
{
Presenter.Items = newValue;
if (change.NewValue is not null && ItemTemplate is not null)
throw new InvalidOperationException("Cannot set both DisplayMemberBinding and ItemTemplate.");
_displayMemberItemTemplate = null;
RefreshContainers();
}
SubscribeToItems(newValue);
}
/// <summary>
/// Refreshes the containers displayed by the control.
/// </summary>
/// <remarks>
/// Causes all containers to be unrealized and re-realized.
/// </remarks>
protected void RefreshContainers() => Presenter?.Refresh();
/// <summary>
/// Called when the <see cref="INotifyCollectionChanged.CollectionChanged"/> event is
/// raised on <see cref="Items"/>.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
protected virtual void ItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
UpdateItemCount();
ItemCount = _itemsView.Count;
switch (e.Action)
{
@ -437,8 +466,76 @@ namespace Avalonia.Controls
RemoveControlItemsFromLogicalChildren(e.OldItems);
break;
}
}
/// <summary>
/// Creates the <see cref="ItemContainerGenerator"/>
/// </summary>
/// <remarks>
/// This method is only present for backwards compatibility with 0.10.x in order for
/// TreeView to be able to create a <see cref="TreeItemContainerGenerator"/>. Can be
/// removed in 12.0.
/// </remarks>
[Obsolete]
private protected virtual ItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator(this);
}
internal void AddLogicalChild(Control c) => LogicalChildren.Add(c);
internal void RemoveLogicalChild(Control c) => LogicalChildren.Remove(c);
/// <summary>
/// Called by <see cref="ItemsPresenter"/> to register with the <see cref="ItemsControl"/>.
/// </summary>
/// <param name="presenter">The items presenter.</param>
/// <remarks>
/// ItemsPresenters can be within nested templates or in popups and so are not necessarily
/// created immediately when the ItemsControl control's template is instantiated. Instead
/// they register themselves using this method.
/// </remarks>
internal void RegisterItemsPresenter(ItemsPresenter presenter)
{
Presenter = presenter;
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty);
}
internal void PrepareItemContainer(Control container, object? item, int index)
{
var itemContainerTheme = ItemContainerTheme;
if (itemContainerTheme is not null &&
!container.IsSet(ThemeProperty) &&
((IStyleable)container).StyleKey == itemContainerTheme.TargetType)
{
container.Theme = itemContainerTheme;
}
Presenter?.ItemsChanged(e);
if (item is not Control)
container.DataContext = item;
PrepareContainerForItemOverride(container, item, index);
}
internal void ItemContainerPrepared(Control container, object? item, int index)
{
_containerBeingPrepared = new(index, container);
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container));
_containerBeingPrepared = null;
_scrollViewer?.RegisterAnchorCandidate(container);
}
internal void ItemContainerIndexChanged(Control container, int oldIndex, int newIndex)
{
ContainerIndexChangedOverride(container, oldIndex, newIndex);
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container));
}
internal void ClearItemContainer(Control container)
{
_scrollViewer?.UnregisterAnchorCandidate(container);
ClearContainerForItemOverride(container);
}
/// <summary>
@ -447,22 +544,22 @@ namespace Avalonia.Controls
/// <param name="items">The items.</param>
private void AddControlItemsToLogicalChildren(IEnumerable? items)
{
var toAdd = new List<ILogical>();
if (items is null)
return;
if (items != null)
List<ILogical>? toAdd = null;
foreach (var i in items)
{
foreach (var i in items)
if (i is Control control && !LogicalChildren.Contains(control))
{
var control = i as Control;
if (control != null && !LogicalChildren.Contains(control))
{
toAdd.Add(control);
}
toAdd ??= new();
toAdd.Add(control);
}
}
LogicalChildren.AddRange(toAdd);
if (toAdd is not null)
LogicalChildren.AddRange(toAdd);
}
/// <summary>
@ -471,63 +568,39 @@ namespace Avalonia.Controls
/// <param name="items">The items.</param>
private void RemoveControlItemsFromLogicalChildren(IEnumerable? items)
{
var toRemove = new List<ILogical>();
if (items is null)
return;
List<ILogical>? toRemove = null;
if (items != null)
foreach (var i in items)
{
foreach (var i in items)
if (i is Control control)
{
var control = i as Control;
if (control != null)
{
toRemove.Add(control);
}
toRemove ??= new();
toRemove.Add(control);
}
}
LogicalChildren.RemoveAll(toRemove);
if (toRemove is not null)
LogicalChildren.RemoveAll(toRemove);
}
/// <summary>
/// Subscribes to an <see cref="Items"/> collection.
/// </summary>
/// <param name="items">The items collection.</param>
private void SubscribeToItems(IEnumerable? items)
private IDataTemplate? GetEffectiveItemTemplate()
{
if (items is INotifyCollectionChanged incc)
{
CollectionChangedEventManager.Instance.AddListener(incc, this);
}
}
if (ItemTemplate is { } itemTemplate)
return itemTemplate;
/// <summary>
/// Called when the <see cref="ItemTemplate"/> changes.
/// </summary>
/// <param name="e">The event args.</param>
private void ItemTemplateChanged(AvaloniaPropertyChangedEventArgs e)
{
if (_itemContainerGenerator != null)
if (_displayMemberItemTemplate is null && DisplayMemberBinding is { } binding)
{
_itemContainerGenerator.ItemTemplate = (IDataTemplate?)e.NewValue;
// TODO: Rebuild the item containers.
_displayMemberItemTemplate = new FuncDataTemplate<object?>((_, _) =>
new TextBlock
{
[!TextBlock.TextProperty] = binding,
});
}
}
private void UpdateItemCount()
{
if (Items == null)
{
ItemCount = 0;
}
else if (Items is IList list)
{
ItemCount = list.Count;
}
else
{
ItemCount = Items.Count();
}
return _displayMemberItemTemplate;
}
private void UpdatePseudoClasses(int itemCount)
@ -585,26 +658,17 @@ namespace Avalonia.Controls
}
}
private void PresenterChildIndexChanged(object? sender, ChildIndexChangedEventArgs e)
{
_childIndexChanged?.Invoke(this, e);
}
int IChildIndexProvider.GetChildIndex(ILogical child)
{
return Presenter is IChildIndexProvider innerProvider
? innerProvider.GetChildIndex(child) : -1;
if (_containerBeingPrepared?.Item2 == child)
return _containerBeingPrepared.Item1;
return child is Control container ? IndexFromContainer(container) : -1;
}
bool IChildIndexProvider.TryGetTotalCount(out int count)
{
if (Presenter is IChildIndexProvider presenter
&& presenter.TryGetTotalCount(out count))
{
return true;
}
count = ItemCount;
count = ItemsView.Count;
return true;
}
}

249
src/Avalonia.Controls/ItemsSourceView.cs

@ -13,40 +13,38 @@ using Avalonia.Controls.Utils;
namespace Avalonia.Controls
{
/// <summary>
/// Represents a standardized view of the supported interactions between a given ItemsSource
/// object and an <see cref="ItemsRepeater"/> control.
/// Represents a standardized view of the supported interactions between an items collection
/// and an items control.
/// </summary>
/// <remarks>
/// Components written to work with ItemsRepeater should consume the
/// <see cref="ItemsRepeater.Items"/> via ItemsSourceView since this provides a normalized
/// view of the Items. That way, each component does not need to know if the source is an
/// IEnumerable, an IList, or something else.
/// </remarks>
public class ItemsSourceView : INotifyCollectionChanged, IDisposable
public class ItemsSourceView : IReadOnlyList<object?>,
INotifyCollectionChanged,
ICollectionChangedListener
{
/// <summary>
/// Gets an empty <see cref="ItemsSourceView"/>
/// </summary>
public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty<object>());
private IList? _inner;
private readonly IList _inner;
private NotifyCollectionChangedEventHandler? _collectionChanged;
private NotifyCollectionChangedEventHandler? _postCollectionChanged;
private bool _listening;
/// <summary>
/// Initializes a new instance of the ItemsSourceView class for the specified data source.
/// </summary>
/// <param name="source">The data source.</param>
public ItemsSourceView(IEnumerable source)
private protected ItemsSourceView(IEnumerable source)
{
source = source ?? throw new ArgumentNullException(nameof(source));
_inner = source switch
{
ItemsSourceView _ => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)),
ItemsSourceView => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)),
IList list => list,
INotifyCollectionChanged _ => throw new ArgumentException(
"Collection implements INotifyCollectionChanged by not IList.",
INotifyCollectionChanged => throw new ArgumentException(
"Collection implements INotifyCollectionChanged but not IList.",
nameof(source)),
IEnumerable<object> iObj => new List<object>(iObj),
null => throw new ArgumentNullException(nameof(source)),
_ => new List<object>(source.Cast<object>())
};
}
@ -56,26 +54,10 @@ namespace Avalonia.Controls
/// </summary>
public int Count => Inner.Count;
/// <summary>
/// Gets a value that indicates whether the items source can provide a unique key for each item.
/// </summary>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// </remarks>
public bool HasKeyIndexMapping => false;
/// <summary>
/// Gets the inner collection.
/// </summary>
public IList Inner
{
get
{
if (_inner is null)
ThrowDisposed();
return _inner!;
}
}
public IList Inner => _inner;
/// <summary>
/// Retrieves the item at the specified index.
@ -84,6 +66,14 @@ namespace Avalonia.Controls
/// <returns>The item.</returns>
public object? this[int index] => GetAt(index);
/// <summary>
/// Gets a value that indicates whether the items source can provide a unique key for each item.
/// </summary>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// </remarks>
internal bool HasKeyIndexMapping => false;
/// <summary>
/// Occurs when the collection has changed to indicate the reason for the change and which items changed.
/// </summary>
@ -91,34 +81,54 @@ namespace Avalonia.Controls
{
add
{
if (_collectionChanged is null && Inner is INotifyCollectionChanged incc)
{
incc.CollectionChanged += OnCollectionChanged;
}
AddListenerIfNecessary();
_collectionChanged += value;
}
remove
{
_collectionChanged -= value;
RemoveListenerIfNecessary();
}
}
if (_collectionChanged is null && Inner is INotifyCollectionChanged incc)
{
incc.CollectionChanged -= OnCollectionChanged;
}
/// <summary>
/// Occurs when a collection has finished changing and all <see cref="CollectionChanged"/>
/// event handlers have been notified.
/// </summary>
internal event NotifyCollectionChangedEventHandler? PostCollectionChanged
{
add
{
AddListenerIfNecessary();
_postCollectionChanged += value;
}
remove
{
_postCollectionChanged -= value;
RemoveListenerIfNecessary();
}
}
/// <inheritdoc/>
public void Dispose()
private void AddListenerIfNecessary()
{
if (_inner is INotifyCollectionChanged incc)
if (!_listening)
{
incc.CollectionChanged -= OnCollectionChanged;
if (_inner is INotifyCollectionChanged incc)
CollectionChangedEventManager.Instance.AddListener(incc, this);
_listening = true;
}
}
_inner = null;
private void RemoveListenerIfNecessary()
{
if (_listening && _collectionChanged is null && _postCollectionChanged is null)
{
if (_inner is INotifyCollectionChanged incc)
CollectionChangedEventManager.Instance.RemoveListener(incc, this);
_listening = false;
}
}
/// <summary>
@ -128,80 +138,121 @@ namespace Avalonia.Controls
/// <returns>The item.</returns>
public object? GetAt(int index) => Inner[index];
/// <summary>
/// Determines the index of a specific item in the collection.
/// </summary>
/// <param name="item">The object to locate in the collection.</param>
/// <returns>The index of value if found in the list; otherwise, -1.</returns>
public int IndexOf(object? item) => Inner.IndexOf(item);
/// <summary>
/// Gets or creates an <see cref="ItemsSourceView"/> for the specified enumerable.
/// </summary>
/// <param name="items">The enumerable.</param>
/// <remarks>
/// This method handles the following three cases:
/// - If <paramref name="items"/> is null, returns <see cref="Empty"/>
/// - If <paramref name="items"/> is an <see cref="ItemsSourceView"/> returns the existing
/// <see cref="ItemsSourceView"/>
/// - Otherwise creates a new <see cref="ItemsSourceView"/>
/// </remarks>
public static ItemsSourceView GetOrCreate(IEnumerable? items)
{
if (items is ItemsSourceView isv)
return items switch
{
return isv;
}
else if (items is null)
{
return Empty;
}
else
{
return new ItemsSourceView(items);
}
ItemsSourceView isv => isv,
null => Empty,
_ => new ItemsSourceView(items)
};
}
/// <summary>
/// Retrieves the index of the item that has the specified unique identifier (key).
/// Gets or creates an <see cref="ItemsSourceView{T}"/> for the specified enumerable.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The key</returns>
/// <param name="items">The enumerable.</param>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// This method handles the following three cases:
/// - If <paramref name="items"/> is null, returns <see cref="Empty"/>
/// - If <paramref name="items"/> is an <see cref="ItemsSourceView"/> returns the existing
/// <see cref="ItemsSourceView"/>
/// - Otherwise creates a new <see cref="ItemsSourceView"/>
/// </remarks>
public string KeyFromIndex(int index)
public static ItemsSourceView<T> GetOrCreate<T>(IEnumerable? items)
{
throw new NotImplementedException();
return items switch
{
ItemsSourceView<T> isv => isv,
null => ItemsSourceView<T>.Empty,
_ => new ItemsSourceView<T>(items)
};
}
/// <summary>
/// Retrieves the unique identifier (key) for the item at the specified index.
/// Gets or creates an <see cref="ItemsSourceView{T}"/> for the specified enumerable.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>The index.</returns>
/// <param name="items">The enumerable.</param>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// This method handles the following three cases:
/// - If <paramref name="items"/> is null, returns <see cref="Empty"/>
/// - If <paramref name="items"/> is an <see cref="ItemsSourceView"/> returns the existing
/// <see cref="ItemsSourceView"/>
/// - Otherwise creates a new <see cref="ItemsSourceView"/>
/// </remarks>
public int IndexFromKey(string key)
public static ItemsSourceView<T> GetOrCreate<T>(IEnumerable<T>? items)
{
throw new NotImplementedException();
return items switch
{
ItemsSourceView<T> isv => isv,
null => ItemsSourceView<T>.Empty,
_ => new ItemsSourceView<T>(items)
};
}
internal void AddListener(ICollectionChangedListener listener)
public IEnumerator<object?> GetEnumerator()
{
if (Inner is INotifyCollectionChanged incc)
static IEnumerator<object> EnumerateItems(IList list)
{
CollectionChangedEventManager.Instance.AddListener(incc, listener);
foreach (var o in list)
yield return o;
}
var inner = Inner;
return inner switch
{
IEnumerable<object> e => e.GetEnumerator(),
_ => EnumerateItems(inner),
};
}
internal void RemoveListener(ICollectionChangedListener listener)
IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator();
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
if (Inner is INotifyCollectionChanged incc)
{
CollectionChangedEventManager.Instance.RemoveListener(incc, listener);
}
}
protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args)
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
_collectionChanged?.Invoke(this, args);
_collectionChanged?.Invoke(this, e);
}
private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
OnItemsSourceChanged(e);
_postCollectionChanged?.Invoke(this, e);
}
private void ThrowDisposed() => throw new ObjectDisposedException(nameof(ItemsSourceView));
/// <summary>
/// Retrieves the index of the item that has the specified unique identifier (key).
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The key</returns>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// </remarks>
internal string KeyFromIndex(int index) => throw new NotImplementedException();
}
public class ItemsSourceView<T> : ItemsSourceView, IReadOnlyList<T>
public sealed class ItemsSourceView<T> : ItemsSourceView, IReadOnlyList<T>
{
/// <summary>
/// Gets an empty <see cref="ItemsSourceView"/>
@ -212,12 +263,12 @@ namespace Avalonia.Controls
/// Initializes a new instance of the ItemsSourceView class for the specified data source.
/// </summary>
/// <param name="source">The data source.</param>
public ItemsSourceView(IEnumerable<T> source)
internal ItemsSourceView(IEnumerable<T> source)
: base(source)
{
}
private ItemsSourceView(IEnumerable source)
internal ItemsSourceView(IEnumerable source)
: base(source)
{
}
@ -227,9 +278,7 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item.</returns>
#pragma warning disable CS8603
public new T this[int index] => GetAt(index);
#pragma warning restore CS8603
/// <summary>
/// Retrieves the item at the specified index.
@ -238,23 +287,23 @@ namespace Avalonia.Controls
/// <returns>The item.</returns>
public new T GetAt(int index) => (T)Inner[index]!;
public IEnumerator<T> GetEnumerator() => Inner.Cast<T>().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator();
public static new ItemsSourceView<T> GetOrCreate(IEnumerable? items)
public new IEnumerator<T> GetEnumerator()
{
if (items is ItemsSourceView<T> isv)
static IEnumerator<T> EnumerateItems(IList list)
{
return isv;
foreach (var o in list)
yield return (T)o;
}
else if (items is null)
{
return Empty;
}
else
var inner = Inner;
return inner switch
{
return new ItemsSourceView<T>(items);
}
IEnumerable<T> e => e.GetEnumerator(),
_ => EnumerateItems(inner),
};
}
IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator();
}
}

25
src/Avalonia.Controls/ListBox.cs

@ -47,12 +47,6 @@ namespace Avalonia.Controls
public static readonly new StyledProperty<SelectionMode> SelectionModeProperty =
SelectingItemsControl.SelectionModeProperty;
/// <summary>
/// Defines the <see cref="VirtualizationMode"/> property.
/// </summary>
public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty =
ItemsPresenter.VirtualizationModeProperty.AddOwner<ListBox>();
private IScrollable? _scroll;
/// <summary>
@ -61,7 +55,6 @@ namespace Avalonia.Controls
static ListBox()
{
ItemsPanelProperty.OverrideDefaultValue<ListBox>(DefaultPanel);
VirtualizationModeProperty.OverrideDefaultValue<ListBox>(ItemVirtualizationMode.Simple);
}
/// <summary>
@ -100,15 +93,6 @@ namespace Avalonia.Controls
set { base.SelectionMode = value; }
}
/// <summary>
/// Gets or sets the virtualization mode for the items.
/// </summary>
public ItemVirtualizationMode VirtualizationMode
{
get { return GetValue(VirtualizationModeProperty); }
set { SetValue(VirtualizationModeProperty, value); }
}
/// <summary>
/// Selects all items in the <see cref="ListBox"/>.
/// </summary>
@ -119,14 +103,7 @@ namespace Avalonia.Controls
/// </summary>
public void UnselectAll() => Selection.Clear();
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<ListBoxItem>(
this,
ListBoxItem.ContentProperty,
ListBoxItem.ContentTemplateProperty);
}
protected internal override Control CreateContainerForItemOverride() => new ListBoxItem();
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)

10
src/Avalonia.Controls/Menu.cs

@ -93,5 +93,15 @@ namespace Avalonia.Controls
inputRoot.AccessKeyHandler.MainMenu = this;
}
}
protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index)
{
base.PrepareContainerForItemOverride(element, item, index);
// Child menu items should not inherit the menu's ItemContainerTheme as that is specific
// for top-level menu items.
if ((element as MenuItem)?.ItemContainerTheme == ItemContainerTheme)
element.ClearValue(ItemContainerThemeProperty);
}
}
}

17
src/Avalonia.Controls/MenuBase.cs

@ -95,15 +95,7 @@ namespace Avalonia.Controls
}
/// <inheritdoc/>
IEnumerable<IMenuItem> IMenuElement.SubItems
{
get
{
return ItemContainerGenerator.Containers
.Select(x => x.ContainerControl)
.OfType<IMenuItem>();
}
}
IEnumerable<IMenuItem> IMenuElement.SubItems => GetRealizedContainers().OfType<IMenuItem>();
/// <summary>
/// Gets the interaction handler for the menu.
@ -141,11 +133,8 @@ namespace Avalonia.Controls
/// <inheritdoc/>
bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<MenuItem>(this, MenuItem.HeaderProperty, null);
}
protected internal override Control CreateContainerForItemOverride() => new MenuItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is MenuItem or Separator;
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)

19
src/Avalonia.Controls/MenuItem.cs

@ -329,15 +329,7 @@ namespace Avalonia.Controls
}
/// <inheritdoc/>
IEnumerable<IMenuItem> IMenuElement.SubItems
{
get
{
return ItemContainerGenerator.Containers
.Select(x => x.ContainerControl)
.OfType<IMenuItem>();
}
}
IEnumerable<IMenuItem> IMenuElement.SubItems => GetRealizedContainers().OfType<IMenuItem>();
/// <summary>
/// Opens the submenu.
@ -358,11 +350,8 @@ namespace Avalonia.Controls
/// <inheritdoc/>
void IMenuItem.RaiseClick() => RaiseEvent(new RoutedEventArgs(ClickEvent));
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
}
protected internal override Control CreateContainerForItemOverride() => new MenuItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is MenuItem or Separator;
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
@ -674,7 +663,7 @@ namespace Avalonia.Controls
if (value)
{
foreach (var item in Items!.OfType<MenuItem>())
foreach (var item in ItemsView.OfType<MenuItem>())
{
item.TryUpdateCanExecute();
}

276
src/Avalonia.Controls/Presenters/CarouselPresenter.cs

@ -1,276 +0,0 @@
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Reactive;
using System.Threading.Tasks;
using Avalonia.Animation;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Data;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Displays pages inside an <see cref="ItemsControl"/>.
/// </summary>
public class CarouselPresenter : ItemsPresenterBase
{
/// <summary>
/// Defines the <see cref="IsVirtualized"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsVirtualizedProperty =
Carousel.IsVirtualizedProperty.AddOwner<CarouselPresenter>();
/// <summary>
/// Defines the <see cref="SelectedIndex"/> property.
/// </summary>
public static readonly DirectProperty<CarouselPresenter, int> SelectedIndexProperty =
SelectingItemsControl.SelectedIndexProperty.AddOwner<CarouselPresenter>(
o => o.SelectedIndex,
(o, v) => o.SelectedIndex = v);
/// <summary>
/// Defines the <see cref="PageTransition"/> property.
/// </summary>
public static readonly StyledProperty<IPageTransition?> PageTransitionProperty =
Carousel.PageTransitionProperty.AddOwner<CarouselPresenter>();
private int _selectedIndex = -1;
private Task? _currentTransition;
private int _queuedTransitionIndex = -1;
/// <summary>
/// Initializes static members of the <see cref="CarouselPresenter"/> class.
/// </summary>
static CarouselPresenter()
{
IsVirtualizedProperty.Changed.AddClassHandler<CarouselPresenter>((x, e) => x.IsVirtualizedChanged(e));
SelectedIndexProperty.Changed.AddClassHandler<CarouselPresenter>((x, e) => x.SelectedIndexChanged(e));
}
/// <summary>
/// Gets or sets a value indicating whether the items in the carousel are virtualized.
/// </summary>
/// <remarks>
/// When the carousel is virtualized, only the active page is held in memory.
/// </remarks>
public bool IsVirtualized
{
get { return GetValue(IsVirtualizedProperty); }
set { SetValue(IsVirtualizedProperty, value); }
}
/// <summary>
/// Gets or sets the index of the selected page.
/// </summary>
public int SelectedIndex
{
get
{
return _selectedIndex;
}
set
{
var old = SelectedIndex;
var effective = (value >= 0 && value < Items?.Cast<object>().Count()) ? value : -1;
if (old != effective)
{
_selectedIndex = effective;
RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue);
}
}
}
/// <summary>
/// Gets or sets a transition to use when switching pages.
/// </summary>
public IPageTransition? PageTransition
{
get { return GetValue(PageTransitionProperty); }
set { SetValue(PageTransitionProperty, value); }
}
/// <inheritdoc/>
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(Panel panel)
{
ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
/// <summary>
/// Moves to the selected page, animating if a <see cref="PageTransition"/> is set.
/// </summary>
/// <param name="fromIndex">The index of the old page.</param>
/// <param name="toIndex">The index of the new page.</param>
/// <returns>A task tracking the animation.</returns>
private async Task MoveToPage(int fromIndex, int toIndex)
{
if (fromIndex != toIndex)
{
var generator = ItemContainerGenerator;
Control? from = null;
Control? to = null;
if (fromIndex != -1)
{
from = generator.ContainerFromIndex(fromIndex);
}
if (toIndex != -1)
{
to = GetOrCreateContainer(toIndex);
}
if (PageTransition != null && (from != null || to != null))
{
await PageTransition.Start((Visual?)from, (Visual?)to, fromIndex < toIndex, default);
}
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 Control? GetOrCreateContainer(int index)
{
var container = ItemContainerGenerator.ContainerFromIndex(index);
if (container == null && IsVirtualized)
{
var item = Items!.Cast<object>().ElementAt(index);
var materialized = ItemContainerGenerator.Materialize(index, item);
Panel!.Children.Add(materialized.ContainerControl);
container = materialized.ContainerControl;
}
return container;
}
/// <summary>
/// Called when the <see cref="IsVirtualized"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private void IsVirtualizedChanged(AvaloniaPropertyChangedEventArgs e)
{
if (Panel != null)
{
ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
/// <summary>
/// Called when the <see cref="SelectedIndex"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
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!;
}
}
}
}
}

18
src/Avalonia.Controls/Presenters/IItemsPresenter.cs

@ -1,18 +0,0 @@
using System.Collections;
using System.Collections.Specialized;
using Avalonia.Metadata;
namespace Avalonia.Controls.Presenters
{
[NotClientImplementable]
public interface IItemsPresenter : IPresenter
{
IEnumerable? Items { get; set; }
Panel? Panel { get; }
void ItemsChanged(NotifyCollectionChangedEventArgs e);
void ScrollIntoView(int index);
}
}

26
src/Avalonia.Controls/Presenters/IItemsPresenterHost.cs

@ -1,26 +0,0 @@
using Avalonia.Metadata;
using Avalonia.Styling;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Represents a control which hosts an items presenter.
/// </summary>
/// <remarks>
/// This interface is implemented by <see cref="ItemsControl"/> which usually contains an
/// <see cref="ItemsPresenter"/> and exposes it through its
/// <see cref="ItemsControl.Presenter"/> property. ItemsPresenters can be within
/// nested templates or in popups and so are not necessarily created immediately when the
/// parent control's template is instantiated so they register themselves using this
/// interface.
/// </remarks>
[NotClientImplementable]
public interface IItemsPresenterHost
{
/// <summary>
/// Registers an <see cref="IItemsPresenter"/> with a host control.
/// </summary>
/// <param name="presenter">The items presenter.</param>
void RegisterItemsPresenter(IItemsPresenter presenter);
}
}

125
src/Avalonia.Controls/Presenters/ItemContainerSync.cs

@ -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<ItemContainerInfo> AddContainers(
ItemsPresenterBase owner,
int index,
IEnumerable items)
{
var generator = owner.ItemContainerGenerator;
var result = new List<ItemContainerInfo>();
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(
Panel panel,
IEnumerable<ItemContainerInfo> items)
{
foreach (var i in items)
{
if (i.ContainerControl != null)
{
panel.Children.Remove(i.ContainerControl);
}
}
}
}
}

303
src/Avalonia.Controls/Presenters/ItemVirtualizer.cs

@ -1,303 +0,0 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using Avalonia.Reactive;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Base class for classes which handle virtualization for an <see cref="ItemsPresenter"/>.
/// </summary>
internal abstract class ItemVirtualizer : IVirtualizingController, IDisposable
{
private double _crossAxisOffset;
private IDisposable? _subscriptions;
/// <summary>
/// Initializes a new instance of the <see cref="ItemVirtualizer"/> class.
/// </summary>
/// <param name="owner"></param>
public ItemVirtualizer(ItemsPresenter owner)
{
Owner = owner;
Items = owner.Items;
ItemCount = owner.Items.Count();
var panel = VirtualizingPanel;
if (panel != null)
{
_subscriptions = ((AvaloniaObject)panel).GetObservable(Panel.BoundsProperty)
.Skip(1)
.Subscribe(_ => InvalidateScroll());
}
}
/// <summary>
/// Gets the <see cref="ItemsPresenter"/> which owns the virtualizer.
/// </summary>
public ItemsPresenter Owner { get; }
/// <summary>
/// Gets the <see cref="IVirtualizingPanel"/> which will host the items.
/// </summary>
public IVirtualizingPanel? VirtualizingPanel => Owner.Panel as IVirtualizingPanel;
/// <summary>
/// Gets the items to display.
/// </summary>
public IEnumerable? Items { get; private set; }
/// <summary>
/// Gets the number of items in <see cref="Items"/>.
/// </summary>
public int ItemCount { get; private set; }
/// <summary>
/// Gets or sets the index of the first item displayed in the panel.
/// </summary>
public int FirstIndex { get; protected set; }
/// <summary>
/// Gets or sets the index of the first item beyond those displayed in the panel.
/// </summary>
public int NextIndex { get; protected set; }
/// <summary>
/// Gets a value indicating whether the items should be scroll horizontally or vertically.
/// </summary>
public bool Vertical => VirtualizingPanel?.ScrollDirection == Orientation.Vertical;
/// <summary>
/// Gets a value indicating whether logical scrolling is enabled.
/// </summary>
public abstract bool IsLogicalScrollEnabled { get; }
/// <summary>
/// Gets the value of the scroll extent.
/// </summary>
public abstract double ExtentValue { get; }
/// <summary>
/// Gets or sets the value of the current scroll offset.
/// </summary>
public abstract double OffsetValue { get; set; }
/// <summary>
/// Gets the value of the scrollable viewport.
/// </summary>
public abstract double ViewportValue { get; }
/// <summary>
/// Gets the <see cref="ExtentValue"/> as a <see cref="Size"/>.
/// </summary>
public Size Extent
{
get
{
if (IsLogicalScrollEnabled && Owner.Panel is Panel panel)
{
return Vertical ?
new Size(panel.DesiredSize.Width, ExtentValue) :
new Size(ExtentValue, panel.DesiredSize.Height);
}
return default;
}
}
/// <summary>
/// Gets the <see cref="ViewportValue"/> as a <see cref="Size"/>.
/// </summary>
public Size Viewport
{
get
{
if (IsLogicalScrollEnabled && Owner.Panel is Panel panel)
{
return Vertical ?
new Size(panel.Bounds.Width, ViewportValue) :
new Size(ViewportValue, panel.Bounds.Height);
}
return default;
}
}
/// <summary>
/// Gets or sets the <see cref="OffsetValue"/> as a <see cref="Vector"/>.
/// </summary>
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();
}
}
}
/// <summary>
/// Creates an <see cref="ItemVirtualizer"/> based on an item presenter's
/// <see cref="ItemVirtualizationMode"/>.
/// </summary>
/// <param name="owner">The items presenter.</param>
/// <returns>An <see cref="ItemVirtualizer"/>.</returns>
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;
}
/// <summary>
/// Carries out a measure for the related <see cref="ItemsPresenter"/>.
/// </summary>
/// <param name="availableSize">The size available to the control.</param>
/// <returns>The desired size for the control.</returns>
public virtual Size MeasureOverride(Size availableSize)
{
Owner.Panel!.Measure(availableSize);
return Owner.Panel.DesiredSize;
}
/// <summary>
/// Carries out an arrange for the related <see cref="ItemsPresenter"/>.
/// </summary>
/// <param name="finalSize">The size available to the control.</param>
/// <returns>The actual size used.</returns>
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;
}
/// <inheritdoc/>
public virtual void UpdateControls()
{
}
/// <summary>
/// Gets the next control in the specified direction.
/// </summary>
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <returns>The control.</returns>
public virtual Control? GetControlInDirection(NavigationDirection direction, Control? from)
{
return null;
}
/// <summary>
/// Called when the items for the presenter change, either because
/// <see cref="ItemsPresenterBase.Items"/> has been set, the items collection has been
/// modified, or the panel has been created.
/// </summary>
/// <param name="items">The items.</param>
/// <param name="e">A description of the change.</param>
public virtual void ItemsChanged(IEnumerable? items, NotifyCollectionChangedEventArgs e)
{
Items = items;
ItemCount = items?.Count() ?? 0;
}
/// <summary>
/// Scrolls the specified item into view.
/// </summary>
/// <param name="index">The index of the item.</param>
public virtual void ScrollIntoView(int index)
{
}
/// <inheritdoc/>
public virtual void Dispose()
{
_subscriptions?.Dispose();
_subscriptions = null;
if (VirtualizingPanel != null)
{
VirtualizingPanel.Controller = null;
VirtualizingPanel.Children.Clear();
}
Owner.ItemContainerGenerator.Clear();
}
/// <summary>
/// Invalidates the current scroll.
/// </summary>
protected void InvalidateScroll() => ((ILogicalScrollable)Owner).RaiseScrollInvalidated(EventArgs.Empty);
}
}

106
src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs

@ -1,106 +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
{
/// <summary>
/// Represents an item virtualizer for an <see cref="ItemsPresenter"/> that doesn't actually
/// virtualize items - it just creates a container for every item.
/// </summary>
internal class ItemVirtualizerNone : ItemVirtualizer
{
public ItemVirtualizerNone(ItemsPresenter owner)
: base(owner)
{
if (Items != null && owner.Panel != null)
{
AddContainers(0, Items);
}
}
/// <inheritdoc/>
public override bool IsLogicalScrollEnabled => false;
/// <summary>
/// This property should never be accessed because <see cref="IsLogicalScrollEnabled"/> is
/// false.
/// </summary>
public override double ExtentValue
{
get { throw new NotSupportedException(); }
}
/// <summary>
/// This property should never be accessed because <see cref="IsLogicalScrollEnabled"/> is
/// false.
/// </summary>
public override double OffsetValue
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
/// <summary>
/// This property should never be accessed because <see cref="IsLogicalScrollEnabled"/> is
/// false.
/// </summary>
public override double ViewportValue
{
get { throw new NotSupportedException(); }
}
/// <inheritdoc/>
public override void ItemsChanged(IEnumerable? items, NotifyCollectionChangedEventArgs e)
{
base.ItemsChanged(items, e);
ItemContainerSync.ItemsChanged(Owner, items, e);
Owner.InvalidateMeasure();
}
/// <summary>
/// Scrolls the specified item into view.
/// </summary>
/// <param name="index">The index of the item.</param>
public override void ScrollIntoView(int index)
{
if (index != -1)
{
var container = Owner.ItemContainerGenerator.ContainerFromIndex(index);
container?.BringIntoView();
}
}
private IList<ItemContainerInfo> AddContainers(int index, IEnumerable items)
{
var generator = Owner.ItemContainerGenerator;
var result = new List<ItemContainerInfo>();
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;
}
}
}

606
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@ -1,606 +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
{
/// <summary>
/// Handles virtualization in an <see cref="ItemsPresenter"/> for
/// <see cref="ItemVirtualizationMode.Simple"/>.
/// </summary>
internal class ItemVirtualizerSimple : ItemVirtualizer
{
private int _anchor;
/// <summary>
/// Initializes a new instance of the <see cref="ItemVirtualizerSimple"/> class.
/// </summary>
/// <param name="owner"></param>
public ItemVirtualizerSimple(ItemsPresenter owner)
: base(owner)
{
// Don't need to add children here as UpdateControls should be called by the panel
// measure/arrange.
}
/// <inheritdoc/>
public override bool IsLogicalScrollEnabled => true;
/// <inheritdoc/>
public override double ExtentValue => ItemCount;
/// <inheritdoc/>
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;
}
}
}
}
/// <inheritdoc/>
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 new IVirtualizingPanel VirtualizingPanel => base.VirtualizingPanel!;
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public override void UpdateControls()
{
CreateAndRemoveContainers();
InvalidateScroll();
}
/// <inheritdoc/>
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 < 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 Control? GetControlInDirection(NavigationDirection direction, Control? from)
{
var generator = Owner.ItemContainerGenerator;
var panel = VirtualizingPanel;
var itemIndex = generator.IndexFromContainer(from);
var vertical = VirtualizingPanel.ScrollDirection == Orientation.Vertical;
var newItemIndex = -1;
switch (direction)
{
case NavigationDirection.First:
newItemIndex = 0;
break;
case NavigationDirection.Last:
newItemIndex = ItemCount - 1;
break;
default:
if (itemIndex == -1)
{
return null;
}
break;
}
switch (direction)
{
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);
}
/// <inheritdoc/>
public override void ScrollIntoView(int index)
{
if (index != -1)
{
ScrollIntoViewCore(index);
}
}
/// <summary>
/// Creates and removes containers such that we have at most enough containers to fill
/// the panel.
/// </summary>
private void CreateAndRemoveContainers()
{
var generator = Owner.ItemContainerGenerator;
var panel = VirtualizingPanel;
var panelControl = (Control)panel;
if (!panel.IsFull && Items != null && panelControl.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);
}
}
}
/// <summary>
/// Updates the containers in the panel to make sure they are displaying the correct item
/// based on <see cref="ItemVirtualizer.FirstIndex"/>.
/// </summary>
/// <remarks>
/// This method requires that <see cref="ItemVirtualizer.FirstIndex"/> + the number of
/// materialized containers is not more than <see cref="ItemVirtualizer.ItemCount"/>.
/// </remarks>
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;
}
}
/// <summary>
/// Recycles containers when a move occurs.
/// </summary>
/// <param name="delta">The delta of the move.</param>
/// <remarks>
/// 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
/// <see cref="ItemVirtualizer.FirstIndex"/> and <see cref="ItemVirtualizer.NextIndex"/>
/// with their new values.
/// </remarks>
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;
}
/// <summary>
/// Recycles containers due to items being removed.
/// </summary>
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();
}
}
/// <summary>
/// Removes the specified number of containers from the end of the panel and updates
/// <see cref="ItemVirtualizer.NextIndex"/>.
/// </summary>
/// <param name="count">The number of containers to remove.</param>
private void RemoveContainers(int count)
{
var index = VirtualizingPanel.Children.Count - count;
VirtualizingPanel.Children.RemoveRange(index, count);
Owner.ItemContainerGenerator.Dematerialize(FirstIndex + index, count);
NextIndex -= count;
}
/// <summary>
/// Scrolls the item with the specified index into view.
/// </summary>
/// <param name="index">The item index.</param>
/// <returns>The container that was brought into view.</returns>
private Control? ScrollIntoViewCore(int index)
{
var panel = VirtualizingPanel;
var panelControl = (Control)panel;
var generator = Owner.ItemContainerGenerator;
var newOffset = -1.0;
//better not trigger any container generation/recycle while or layout stuff
//before panel is attached/visible
if (!panelControl.IsAttachedToVisualTree)
{
return null;
}
if (!panelControl.IsMeasureValid && panelControl.PreviousMeasure.HasValue)
{
//before any kind of scrolling we need to make sure panel measure is valid
//or we risk get panel into not valid state
//we make a preemptive quick measure so scrolling is valid
panelControl.Measure(panelControl.PreviousMeasure.Value);
}
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 < panelControl.Bounds.Y || container.Bounds.Bottom > panelControl.Bounds.Bottom)
{
OffsetValue += 1;
}
}
else
{
if (container.Bounds.X < panelControl.Bounds.X || container.Bounds.Right > panelControl.Bounds.Right)
{
OffsetValue += 1;
}
}
}
return container;
}
return null;
}
/// <summary>
/// Ensures an offset value is within the value range.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>The coerced value.</returns>
private double CoerceOffset(double value)
{
var max = Math.Max(ExtentValue - ViewportValue, 0);
return MathUtilities.Clamp(value, 0, max);
}
}
}

223
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@ -1,179 +1,208 @@
using System;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using static Avalonia.Utilities.MathUtilities;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Displays items inside an <see cref="ItemsControl"/>.
/// Presents items inside an <see cref="Avalonia.Controls.ItemsControl"/>.
/// </summary>
public class ItemsPresenter : ItemsPresenterBase, ILogicalScrollable
public class ItemsPresenter : Control, ILogicalScrollable
{
/// <summary>
/// Defines the <see cref="VirtualizationMode"/> property.
/// Defines the <see cref="ItemsPanel"/> property.
/// </summary>
public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty =
AvaloniaProperty.Register<ItemsPresenter, ItemVirtualizationMode>(
nameof(VirtualizationMode),
defaultValue: ItemVirtualizationMode.None);
public static readonly StyledProperty<ITemplate<Panel>> ItemsPanelProperty =
ItemsControl.ItemsPanelProperty.AddOwner<ItemsPresenter>();
private bool _canHorizontallyScroll;
private bool _canVerticallyScroll;
private PanelContainerGenerator? _generator;
private ILogicalScrollable? _logicalScrollable;
private EventHandler? _scrollInvalidated;
/// <summary>
/// Initializes static members of the <see cref="ItemsPresenter"/> class.
/// </summary>
static ItemsPresenter()
{
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(
typeof(ItemsPresenter),
KeyboardNavigationMode.Once);
}
VirtualizationModeProperty.Changed
.AddClassHandler<ItemsPresenter>((x, e) => x.VirtualizationModeChanged(e));
event EventHandler? ILogicalScrollable.ScrollInvalidated
{
add => _scrollInvalidated += value;
remove => _scrollInvalidated -= value;
}
/// <summary>
/// Gets or sets the virtualization mode for the items.
/// Gets or sets a template which creates the <see cref="Panel"/> used to display the items.
/// </summary>
public ItemVirtualizationMode VirtualizationMode
public ITemplate<Panel> ItemsPanel
{
get { return GetValue(VirtualizationModeProperty); }
set { SetValue(VirtualizationModeProperty, value); }
get => GetValue(ItemsPanelProperty);
set => SetValue(ItemsPanelProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// Gets the panel used to display the items.
/// </summary>
bool ILogicalScrollable.CanHorizontallyScroll
public Panel? Panel { get; private set; }
/// <summary>
/// Gets the owner <see cref="ItemsControl"/>.
/// </summary>
internal ItemsControl? ItemsControl { get; private set; }
bool ILogicalScrollable.CanHorizontallyScroll
{
get { return _canHorizontallyScroll; }
get => _logicalScrollable?.CanHorizontallyScroll ?? false;
set
{
_canHorizontallyScroll = value;
InvalidateMeasure();
if (_logicalScrollable is not null)
_logicalScrollable.CanHorizontallyScroll = value;
}
}
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
bool ILogicalScrollable.CanVerticallyScroll
bool ILogicalScrollable.CanVerticallyScroll
{
get { return _canVerticallyScroll; }
get => _logicalScrollable?.CanVerticallyScroll ?? false;
set
{
_canVerticallyScroll = value;
InvalidateMeasure();
if (_logicalScrollable is not null)
_logicalScrollable.CanVerticallyScroll = value;
}
}
/// <inheritdoc/>
bool ILogicalScrollable.IsLogicalScrollEnabled
{
get { return Virtualizer?.IsLogicalScrollEnabled ?? false; }
}
/// <inheritdoc/>
Size IScrollable.Extent => Virtualizer?.Extent ?? default;
/// <inheritdoc/>
Vector IScrollable.Offset
Vector IScrollable.Offset
{
get { return Virtualizer?.Offset ?? new Vector(); }
get => _logicalScrollable?.Offset ?? default;
set
{
if (Virtualizer != null)
{
Virtualizer.Offset = CoerceOffset(value);
}
if (_logicalScrollable is not null)
_logicalScrollable.Offset = value;
}
}
/// <inheritdoc/>
Size IScrollable.Viewport => Virtualizer?.Viewport ?? Bounds.Size;
bool ILogicalScrollable.IsLogicalScrollEnabled => _logicalScrollable?.IsLogicalScrollEnabled ?? false;
Size ILogicalScrollable.ScrollSize => _logicalScrollable?.ScrollSize ?? default;
Size ILogicalScrollable.PageScrollSize => _logicalScrollable?.PageScrollSize ?? default;
Size IScrollable.Extent => _logicalScrollable?.Extent ?? default;
Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default;
/// <inheritdoc/>
event EventHandler? ILogicalScrollable.ScrollInvalidated
public override sealed void ApplyTemplate()
{
add => _scrollInvalidated += value;
remove => _scrollInvalidated -= value;
}
if (Panel is null && ItemsControl is not null)
{
if (_logicalScrollable is not null)
{
_logicalScrollable.ScrollInvalidated -= OnLogicalScrollInvalidated;
}
/// <inheritdoc/>
Size ILogicalScrollable.ScrollSize => new Size(ScrollViewer.DefaultSmallChange, 1);
Panel = ItemsPanel.Build();
Panel.SetValue(TemplatedParentProperty, TemplatedParent);
LogicalChildren.Add(Panel);
VisualChildren.Add(Panel);
/// <inheritdoc/>
Size ILogicalScrollable.PageScrollSize => Virtualizer?.Viewport ?? new Size(16, 16);
if (Panel is VirtualizingPanel v)
v.Attach(ItemsControl);
else
CreateSimplePanelGenerator();
internal ItemVirtualizer? Virtualizer { get; private set; }
_logicalScrollable = Panel as ILogicalScrollable;
/// <inheritdoc/>
bool ILogicalScrollable.BringIntoView(Control target, Rect targetRect)
{
return false;
if (_logicalScrollable is not null)
{
_logicalScrollable.ScrollInvalidated += OnLogicalScrollInvalidated;
}
}
}
/// <inheritdoc/>
Control? ILogicalScrollable.GetControlInDirection(NavigationDirection direction, Control? from)
{
return Virtualizer?.GetControlInDirection(direction, from);
}
bool ILogicalScrollable.BringIntoView(Control target, Rect targetRect) =>
_logicalScrollable?.BringIntoView(target, targetRect) ?? false;
Control? ILogicalScrollable.GetControlInDirection(NavigationDirection direction, Control? from) =>
_logicalScrollable?.GetControlInDirection(direction, from);
void ILogicalScrollable.RaiseScrollInvalidated(EventArgs e) => _scrollInvalidated?.Invoke(this, e);
/// <inheritdoc/>
void ILogicalScrollable.RaiseScrollInvalidated(EventArgs e)
internal void ScrollIntoView(int index)
{
_scrollInvalidated?.Invoke(this, e);
if (Panel is VirtualizingPanel v)
v.ScrollIntoView(index);
else if (index >= 0 && index < Panel?.Children.Count)
Panel.Children[index].BringIntoView();
}
public override void ScrollIntoView(int index)
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
Virtualizer?.ScrollIntoView(index);
base.OnPropertyChanged(change);
if (change.Property == TemplatedParentProperty)
{
ResetState();
ItemsControl = null;
if (change.NewValue is ItemsControl itemsControl)
{
ItemsControl = itemsControl;
ItemsControl.RegisterItemsPresenter(this);
}
}
else if (change.Property == ItemsPanelProperty)
{
ResetState();
InvalidateMeasure();
}
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
internal void Refresh()
{
return Virtualizer?.MeasureOverride(availableSize) ?? default;
if (Panel is VirtualizingPanel v)
v.Refresh();
else
_generator?.Refresh();
}
protected override Size ArrangeOverride(Size finalSize)
private void ResetState()
{
return Virtualizer?.ArrangeOverride(finalSize) ?? default;
_generator?.Dispose();
_generator = null;
LogicalChildren.Clear();
VisualChildren.Clear();
(Panel as VirtualizingPanel)?.Detach();
Panel = null;
}
/// <inheritdoc/>
protected override void PanelCreated(Panel panel)
private void CreateSimplePanelGenerator()
{
Virtualizer?.Dispose();
Virtualizer = ItemVirtualizer.Create(this);
_scrollInvalidated?.Invoke(this, EventArgs.Empty);
Debug.Assert(Panel is not VirtualizingPanel);
KeyboardNavigation.SetTabNavigation(
(InputElement)panel,
KeyboardNavigation.GetTabNavigation(this));
if (ItemsControl is null || Panel is null)
return;
_generator?.Dispose();
_generator = new(this);
}
protected override void ItemsChanged(NotifyCollectionChangedEventArgs e)
internal Control? ContainerFromIndex(int index)
{
Virtualizer?.ItemsChanged(Items, e);
if (Panel is VirtualizingPanel v)
return v.ContainerFromIndex(index);
return index >= 0 && index < Panel?.Children.Count ? Panel.Children[index] : null;
}
private Vector CoerceOffset(Vector value)
internal IEnumerable<Control>? GetRealizedContainers()
{
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 (Panel is VirtualizingPanel v)
return v.GetRealizedContainers();
return Panel?.Children;
}
private void VirtualizationModeChanged(AvaloniaPropertyChangedEventArgs e)
internal int IndexFromContainer(Control container)
{
Virtualizer?.Dispose();
Virtualizer = ItemVirtualizer.Create(this);
_scrollInvalidated?.Invoke(this, EventArgs.Empty);
if (Panel is VirtualizingPanel v)
return v.IndexFromContainer(container);
return Panel?.Children.IndexOf(container) ?? -1;
}
private void OnLogicalScrollInvalidated(object? sender, EventArgs e) => _scrollInvalidated?.Invoke(this, e);
}
}

308
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@ -1,308 +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.Data;
using Avalonia.LogicalTree;
using Avalonia.Styling;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Base class for controls that present items inside an <see cref="ItemsControl"/>.
/// </summary>
public abstract class ItemsPresenterBase : Control, IItemsPresenter, IChildIndexProvider
{
/// <summary>
/// Defines the <see cref="Items"/> property.
/// </summary>
public static readonly DirectProperty<ItemsPresenterBase, IEnumerable?> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<ItemsPresenterBase>(o => o.Items, (o, v) => o.Items = v);
/// <summary>
/// Defines the <see cref="ItemsPanel"/> property.
/// </summary>
public static readonly StyledProperty<ITemplate<Panel>> ItemsPanelProperty =
ItemsControl.ItemsPanelProperty.AddOwner<ItemsPresenterBase>();
/// <summary>
/// Defines the <see cref="ItemTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
ItemsControl.ItemTemplateProperty.AddOwner<ItemsPresenterBase>();
/// <summary>
/// Defines the <see cref="DisplayMemberBinding" /> property
/// </summary>
public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty =
ItemsControl.DisplayMemberBindingProperty.AddOwner<ItemsPresenterBase>();
private IEnumerable? _items;
private IDisposable? _itemsSubscription;
private bool _createdPanel;
private IItemContainerGenerator? _generator;
private EventHandler<ChildIndexChangedEventArgs>? _childIndexChanged;
/// <summary>
/// Initializes static members of the <see cref="ItemsPresenter"/> class.
/// </summary>
static ItemsPresenterBase()
{
TemplatedParentProperty.Changed.AddClassHandler<ItemsPresenterBase>((x,e) => x.TemplatedParentChanged(e));
}
/// <summary>
/// Gets or sets the items to be displayed.
/// </summary>
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));
}
}
}
/// <summary>
/// Gets the item container generator.
/// </summary>
public IItemContainerGenerator ItemContainerGenerator
{
get
{
if (_generator == null)
{
_generator = CreateItemContainerGenerator();
}
return _generator;
}
internal set
{
if (_generator != null)
{
throw new InvalidOperationException("ItemContainerGenerator already created.");
}
_generator = value;
}
}
/// <summary>
/// Gets or sets a template which creates the <see cref="Panel"/> used to display the items.
/// </summary>
public ITemplate<Panel> ItemsPanel
{
get { return GetValue(ItemsPanelProperty); }
set { SetValue(ItemsPanelProperty, value); }
}
/// <summary>
/// Gets or sets the data template used to display the items in the control.
/// </summary>
public IDataTemplate? ItemTemplate
{
get { return GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
/// <summary>
/// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item.
/// </summary>
public IBinding? DisplayMemberBinding
{
get { return GetValue(DisplayMemberBindingProperty); }
set { SetValue(DisplayMemberBindingProperty, value); }
}
/// <summary>
/// Gets the panel used to display the items.
/// </summary>
public Panel? Panel
{
get;
private set;
}
protected bool IsHosted => TemplatedParent is IItemsPresenterHost;
event EventHandler<ChildIndexChangedEventArgs>? IChildIndexProvider.ChildIndexChanged
{
add => _childIndexChanged += value;
remove => _childIndexChanged -= value;
}
/// <inheritdoc/>
public override sealed void ApplyTemplate()
{
if (!_createdPanel)
{
CreatePanel();
}
}
/// <inheritdoc/>
public virtual void ScrollIntoView(int index)
{
}
/// <inheritdoc/>
void IItemsPresenter.ItemsChanged(NotifyCollectionChangedEventArgs e)
{
if (Panel != null)
{
ItemsChanged(e);
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty);
}
}
/// <summary>
/// Creates the <see cref="ItemContainerGenerator"/> for the control.
/// </summary>
/// <returns>
/// An <see cref="IItemContainerGenerator"/> or null.
/// </returns>
protected virtual IItemContainerGenerator CreateItemContainerGenerator()
{
var i = TemplatedParent as ItemsControl;
var result = i?.ItemContainerGenerator;
if (result == null)
{
result = new ItemContainerGenerator(this);
result.ItemTemplate = ItemTemplate;
result.DisplayMemberBinding = DisplayMemberBinding;
}
result.Materialized += ContainerActionHandler;
result.Dematerialized += ContainerActionHandler;
result.Recycled += ContainerActionHandler;
return result;
}
private void ContainerActionHandler(object? sender, ItemContainerEventArgs e)
{
for (var i = 0; i < e.Containers.Count; i++)
{
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(e.Containers[i].ContainerControl));
}
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
Panel!.Measure(availableSize);
return Panel.DesiredSize;
}
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
Panel!.Arrange(new Rect(finalSize));
return finalSize;
}
/// <summary>
/// Called when the <see cref="Panel"/> is created.
/// </summary>
/// <param name="panel">The panel.</param>
protected virtual void PanelCreated(Panel panel)
{
}
/// <summary>
/// Called when the items for the presenter change, either because <see cref="Items"/>
/// has been set, the items collection has been modified, or the panel has been created.
/// </summary>
/// <param name="e">A description of the change.</param>
/// <remarks>
/// The panel is guaranteed to be created when this method is called.
/// </remarks>
protected virtual void ItemsChanged(NotifyCollectionChangedEventArgs e)
{
ItemContainerSync.ItemsChanged(this, Items, e);
}
/// <summary>
/// Creates the <see cref="Panel"/> when <see cref="ApplyTemplate"/> is called for the first
/// time.
/// </summary>
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);
}
/// <summary>
/// Called when the <see cref="Items"/> collection changes.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
private void ItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (_createdPanel)
{
ItemsChanged(e);
}
}
private void TemplatedParentChanged(AvaloniaPropertyChangedEventArgs e)
{
(e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this);
}
int IChildIndexProvider.GetChildIndex(ILogical child)
{
if (child is Control control && ItemContainerGenerator is { } generator)
{
var index = ItemContainerGenerator.IndexFromContainer(control);
return index;
}
return -1;
}
bool IChildIndexProvider.TryGetTotalCount(out int count)
{
return Items.TryGetCountFast(out count);
}
}
}

160
src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs

@ -0,0 +1,160 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;
using Avalonia.Controls.Utils;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Generates containers for <see cref="ItemsPresenter"/>s that have non-virtualizing panels.
/// </summary>
internal class PanelContainerGenerator : IDisposable
{
private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty =
AvaloniaProperty.RegisterAttached<PanelContainerGenerator, Control, bool>("ItemIsOwnContainer");
private readonly ItemsPresenter _presenter;
public PanelContainerGenerator(ItemsPresenter presenter)
{
Debug.Assert(presenter.ItemsControl is not null);
Debug.Assert(presenter.Panel is not null or VirtualizingPanel);
_presenter = presenter;
_presenter.ItemsControl.PropertyChanged += OnItemsControlPropertyChanged;
_presenter.ItemsControl.ItemsView.PostCollectionChanged += OnItemsChanged;
OnItemsChanged(null, CollectionUtils.ResetEventArgs);
}
public void Dispose()
{
if (_presenter.ItemsControl is { } itemsControl)
{
itemsControl.PropertyChanged -= OnItemsControlPropertyChanged;
itemsControl.ItemsView.PostCollectionChanged -= OnItemsChanged;
ClearItemsControlLogicalChildren();
}
_presenter.Panel?.Children.Clear();
}
internal void Refresh() => OnItemsChanged(null, CollectionUtils.ResetEventArgs);
private void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == ItemsControl.ItemsProperty)
{
if (e.OldValue is INotifyCollectionChanged inccOld)
inccOld.CollectionChanged -= OnItemsChanged;
OnItemsChanged(null, CollectionUtils.ResetEventArgs);
if (e.NewValue is INotifyCollectionChanged inccNew)
inccNew.CollectionChanged += OnItemsChanged;
}
}
private void OnItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (_presenter.Panel is null || _presenter.ItemsControl is null)
return;
var itemsControl = _presenter.ItemsControl;
var generator = itemsControl.ItemContainerGenerator;
var children = _presenter.Panel.Children;
void Add(int index, IEnumerable items)
{
var i = index;
foreach (var item in items)
InsertContainer(itemsControl, children, item, i++);
var childCount = children.Count;
var delta = i - index;
for (; i < childCount; ++i)
generator.ItemContainerIndexChanged(children[i], i - delta, i);
}
void Remove(int index, int count)
{
for (var i = 0; i < count; ++i)
{
var c = children[index + i];
if (!c.IsSet(ItemIsOwnContainerProperty))
itemsControl.RemoveLogicalChild(children[i + index]);
else
generator.ClearItemContainer(c);
}
children.RemoveRange(index, count);
var childCount = children.Count;
for (var i = index; i < childCount; ++i)
generator.ItemContainerIndexChanged(children[i], i + count, i);
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Add(e.NewStartingIndex, e.NewItems!);
break;
case NotifyCollectionChangedAction.Remove:
Remove(e.OldStartingIndex, e.OldItems!.Count);
break;
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
Remove(e.OldStartingIndex, e.OldItems!.Count);
Add(e.NewStartingIndex, e.NewItems!);
break;
case NotifyCollectionChangedAction.Reset:
ClearItemsControlLogicalChildren();
children.Clear();
Add(0, _presenter.ItemsControl.ItemsView);
break;
}
}
private static void InsertContainer(
ItemsControl itemsControl,
Controls children,
object? item,
int index)
{
var generator = itemsControl.ItemContainerGenerator;
Control container;
if (item is Control c && generator.IsItemItsOwnContainer(c))
{
container = c;
container.SetValue(ItemIsOwnContainerProperty, true);
}
else
{
container = generator.CreateContainer();
}
generator.PrepareItemContainer(container, item, index);
itemsControl.AddLogicalChild(container);
children.Insert(index, container);
generator.ItemContainerPrepared(container, item, index);
}
private void ClearItemsControlLogicalChildren()
{
if (_presenter.Panel is null || _presenter.ItemsControl is null)
return;
var itemsControl = _presenter.ItemsControl;
var panel = _presenter.Panel;
foreach (var c in panel.Children)
{
if (!c.IsSet(ItemIsOwnContainerProperty))
itemsControl.RemoveLogicalChild(c);
}
}
}
}

16
src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs

@ -1,5 +1,6 @@
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.LogicalTree;
namespace Avalonia.Controls.Primitives
@ -15,6 +16,12 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<object?> HeaderProperty =
HeaderedContentControl.HeaderProperty.AddOwner<HeaderedItemsControl>();
/// <summary>
/// Defines the <see cref="HeaderTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate?> HeaderTemplateProperty =
AvaloniaProperty.Register<HeaderedItemsControl, IDataTemplate?>(nameof(HeaderTemplate));
/// <summary>
/// Initializes static members of the <see cref="ContentControl"/> class.
/// </summary>
@ -32,6 +39,15 @@ namespace Avalonia.Controls.Primitives
set { SetValue(HeaderProperty, value); }
}
/// <summary>
/// Gets or sets the data template used to display the header content of the control.
/// </summary>
public IDataTemplate? HeaderTemplate
{
get => GetValue(HeaderTemplateProperty);
set => SetValue(HeaderTemplateProperty, value);
}
/// <summary>
/// Gets the header presenter from the control's template.
/// </summary>

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

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Xml.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Selection;
using Avalonia.Data;
@ -377,7 +378,7 @@ namespace Avalonia.Controls.Primitives
/// Scrolls the specified item into view.
/// </summary>
/// <param name="item">The item.</param>
public void ScrollIntoView(object item) => ScrollIntoView(IndexOf(Items, item));
public void ScrollIntoView(object item) => ScrollIntoView(ItemsView.IndexOf(item));
/// <summary>
/// Tries to get the container that was the source of an event.
@ -389,7 +390,7 @@ namespace Avalonia.Controls.Primitives
for (var current = eventSource as Visual; current != null; current = current.VisualParent)
{
if (current is Control control && control.Parent == this &&
ItemContainerGenerator?.IndexFromContainer(control) != -1)
IndexFromContainer(control) != -1)
{
return control;
}
@ -430,54 +431,40 @@ namespace Avalonia.Controls.Primitives
}
}
/// <inheritdoc/>
protected override void OnContainersMaterialized(ItemContainerEventArgs e)
protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index)
{
base.OnContainersMaterialized(e);
base.PrepareContainerForItemOverride(element, item, index);
foreach (var container in e.Containers)
if ((element as ISelectable)?.IsSelected == true)
{
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);
}
Selection.Select(index);
MarkContainerSelected(element, true);
}
else
{
var selected = Selection.IsSelected(index);
MarkContainerSelected(element, selected);
}
}
/// <inheritdoc/>
protected override void OnContainersDematerialized(ItemContainerEventArgs e)
protected override void ContainerIndexChangedOverride(Control container, int oldIndex, int newIndex)
{
base.OnContainersDematerialized(e);
if (Presenter?.Panel is InputElement panel)
{
foreach (var container in e.Containers)
{
if (KeyboardNavigation.GetTabOnceActiveElement(panel) == container.ContainerControl)
{
KeyboardNavigation.SetTabOnceActiveElement(panel, null);
break;
}
}
}
base.ContainerIndexChangedOverride(container, oldIndex, newIndex);
MarkContainerSelected(container, Selection.IsSelected(newIndex));
}
protected override void OnContainersRecycled(ItemContainerEventArgs e)
protected internal override void ClearContainerForItemOverride(Control element)
{
foreach (var i in e.Containers)
base.ClearContainerForItemOverride(element);
if (Presenter?.Panel is InputElement panel &&
KeyboardNavigation.GetTabOnceActiveElement(panel) == element)
{
if (i.ContainerControl != null && i.Item != null)
{
bool selected = Selection.IsSelected(i.Index);
MarkContainerSelected(i.ContainerControl, selected);
}
KeyboardNavigation.SetTabOnceActiveElement(panel, null);
}
if (element is ISelectable selectable)
MarkContainerSelected(element, false);
}
/// <inheritdoc/>
@ -533,9 +520,9 @@ namespace Avalonia.Controls.Primitives
_textSearchTerm += e.Text;
bool Match(ItemContainerInfo info)
bool Match(Control container)
{
if (info.ContainerControl is AvaloniaObject ao && ao.IsSet(TextSearch.TextProperty))
if (container is AvaloniaObject ao && ao.IsSet(TextSearch.TextProperty))
{
var searchText = ao.GetValue(TextSearch.TextProperty);
@ -545,15 +532,15 @@ namespace Avalonia.Controls.Primitives
}
}
return info.ContainerControl is IContentControl control &&
return container is IContentControl control &&
control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true;
}
var info = ItemContainerGenerator?.Containers.FirstOrDefault(Match);
if (info != null)
var container = GetRealizedContainers().FirstOrDefault(Match);
if (container != null)
{
SelectedIndex = info.Index;
SelectedIndex = IndexFromContainer(container);
}
StartTextSearchTimer();
@ -632,7 +619,7 @@ namespace Avalonia.Controls.Primitives
/// <returns>True if the selection was moved; otherwise false.</returns>
protected bool MoveSelection(NavigationDirection direction, bool wrap)
{
var from = SelectedIndex != -1 ? ItemContainerGenerator?.ContainerFromIndex(SelectedIndex) : null;
var from = SelectedIndex != -1 ? ContainerFromIndex(SelectedIndex) : null;
return MoveSelection(from, direction, wrap);
}
@ -648,7 +635,7 @@ namespace Avalonia.Controls.Primitives
if (Presenter?.Panel is INavigableContainer container &&
GetNextControl(container, direction, from, wrap) is Control next)
{
var index = ItemContainerGenerator?.IndexFromContainer(next) ?? -1;
var index = IndexFromContainer(next);
if (index != -1)
{
@ -731,7 +718,7 @@ namespace Avalonia.Controls.Primitives
if (Presenter?.Panel != null)
{
var container = ItemContainerGenerator?.ContainerFromIndex(index);
var container = ContainerFromIndex(index);
KeyboardNavigation.SetTabOnceActiveElement(
(InputElement)Presenter.Panel,
container);
@ -755,7 +742,7 @@ namespace Avalonia.Controls.Primitives
bool rightButton = false,
bool fromFocus = false)
{
var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1;
var index = IndexFromContainer(container);
if (index != -1)
{
@ -840,7 +827,7 @@ namespace Avalonia.Controls.Primitives
{
void Mark(int index, bool selected)
{
var container = ItemContainerGenerator?.ContainerFromIndex(index);
var container = ContainerFromIndex(index);
if (container != null)
{
@ -907,7 +894,7 @@ namespace Avalonia.Controls.Primitives
e.Source is Control control &&
e.Source is ISelectable selectable &&
control.Parent == this &&
ItemContainerGenerator?.IndexFromContainer(control) != -1)
IndexFromContainer(control) != -1)
{
UpdateSelection(control, selectable.IsSelected);
}
@ -959,7 +946,7 @@ namespace Avalonia.Controls.Primitives
{
MarkContainerSelected(
container,
Selection.IsSelected(ItemContainerGenerator.IndexFromContainer(container)));
Selection.IsSelected(IndexFromContainer(container)));
}
}
}

9
src/Avalonia.Controls/Primitives/TabStrip.cs

@ -18,13 +18,8 @@ namespace Avalonia.Controls.Primitives
ItemsPanelProperty.OverrideDefaultValue<TabStrip>(DefaultPanel);
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<TabStripItem>(
this,
ContentControl.ContentProperty,
ContentControl.ContentTemplateProperty);
}
protected internal override Control CreateContainerForItemOverride() => new TabStripItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TabStripItem;
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)

6
src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs

@ -151,7 +151,11 @@ namespace Avalonia.Controls
_content.Loaded += (s, e) =>
{
var composition = ElementComposition.GetElementVisual(_content);
var compositor = composition!.Compositor;
if(composition == null)
return;
var compositor = composition.Compositor;
composition.Opacity = 0;
var smoothRotationAnimation

3
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -442,7 +442,7 @@ namespace Avalonia.Controls
var newDataSource = newEnumerable as ItemsSourceView;
if (newEnumerable != null && newDataSource == null)
{
newDataSource = new ItemsSourceView(newEnumerable);
newDataSource = ItemsSourceView.GetOrCreate(newEnumerable);
}
OnDataSourcePropertyChanged(ItemsSourceView, newDataSource);
@ -628,7 +628,6 @@ namespace Avalonia.Controls
oldValue.CollectionChanged -= OnItemsSourceViewChanged;
}
ItemsSourceView?.Dispose();
ItemsSourceView = newValue;
if (newValue != null)

1
src/Avalonia.Controls/Selection/SelectionModel.cs

@ -439,6 +439,7 @@ namespace Avalonia.Controls.Selection
if ((e.Action == NotifyCollectionChangedAction.Remove && e.OldStartingIndex <= oldSelectedIndex) ||
(e.Action == NotifyCollectionChangedAction.Replace && e.OldStartingIndex == oldSelectedIndex) ||
(e.Action == NotifyCollectionChangedAction.Move && e.OldStartingIndex == oldSelectedIndex) ||
e.Action == NotifyCollectionChangedAction.Reset)
{
RaisePropertyChanged(nameof(SelectedItem));

9
src/Avalonia.Controls/Selection/SelectionNodeBase.cs

@ -21,10 +21,12 @@ namespace Avalonia.Controls.Selection
{
if (_source != value)
{
ItemsView?.RemoveListener(this);
if (ItemsView?.Inner is INotifyCollectionChanged inccOld)
CollectionChangedEventManager.Instance.RemoveListener(inccOld, this);
_source = value;
ItemsView = value is object ? ItemsSourceView<T>.GetOrCreate(value) : null;
ItemsView?.AddListener(this);
ItemsView = value is object ? ItemsSourceView.GetOrCreate<T>(value) : null;
if (ItemsView?.Inner is INotifyCollectionChanged inccNew)
CollectionChangedEventManager.Instance.AddListener(inccNew, this);
}
}
}
@ -254,6 +256,7 @@ namespace Avalonia.Controls.Selection
break;
}
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
{
var removeChange = OnItemsRemoved(e.OldStartingIndex, e.OldItems!);
var addChange = OnItemsAdded(e.NewStartingIndex, e.NewItems!);

64
src/Avalonia.Controls/TabControl.cs

@ -57,12 +57,6 @@ namespace Avalonia.Controls
/// </summary>
public static readonly StyledProperty<IDataTemplate?> SelectedContentTemplateProperty =
AvaloniaProperty.Register<TabControl, IDataTemplate?>(nameof(SelectedContentTemplate));
/// <summary>
/// Defines the <see cref="HeaderDisplayMemberBinding" /> property
/// </summary>
public static readonly StyledProperty<IBinding?> HeaderDisplayMemberBindingProperty =
AvaloniaProperty.Register<HeaderedItemsControl, IBinding?>(nameof(HeaderDisplayMemberBinding));
/// <summary>
/// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
@ -141,16 +135,6 @@ namespace Avalonia.Controls
get { return GetValue(SelectedContentTemplateProperty); }
internal set { SetValue(SelectedContentTemplateProperty, value); }
}
/// <summary>
/// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each tab-items header.
/// </summary>
[AssignBinding]
public IBinding? HeaderDisplayMemberBinding
{
get { return GetValue(HeaderDisplayMemberBindingProperty); }
set { SetValue(HeaderDisplayMemberBindingProperty, value); }
}
internal ItemsPresenter? ItemsPresenterPart { get; private set; }
@ -165,15 +149,40 @@ namespace Avalonia.Controls
return RegisterContentPresenter(presenter);
}
protected override void OnContainersMaterialized(ItemContainerEventArgs e)
protected internal override Control CreateContainerForItemOverride() => new TabItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TabItem;
protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index)
{
base.OnContainersMaterialized(e);
UpdateSelectedContent();
base.PrepareContainerForItemOverride(element, item, index);
if (element is TabItem tabItem)
{
if (ContentTemplate is { } ct)
tabItem.ContentTemplate = ct;
tabItem.SetValue(TabStripPlacementProperty, TabStripPlacement);
}
if (index == SelectedIndex && element is ContentControl container)
{
SelectedContentTemplate = container.ContentTemplate;
SelectedContent = container.Content;
}
}
protected override void OnContainersRecycled(ItemContainerEventArgs e)
protected override void ContainerIndexChangedOverride(Control container, int oldIndex, int newIndex)
{
base.OnContainersRecycled(e);
base.ContainerIndexChangedOverride(container, oldIndex, newIndex);
var selectedIndex = SelectedIndex;
if (selectedIndex == oldIndex || selectedIndex == newIndex)
UpdateSelectedContent();
}
protected internal override void ClearContainerForItemOverride(Control element)
{
base.ClearContainerForItemOverride(element);
UpdateSelectedContent();
}
@ -207,11 +216,6 @@ namespace Avalonia.Controls
return false;
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new TabItemContainerGenerator(this);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
ItemsPresenterPart = e.NameScope.Get<ItemsPresenter>("PART_ItemsPresenter");
@ -252,5 +256,13 @@ namespace Avalonia.Controls
}
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == TabStripPlacementProperty)
RefreshContainers();
}
}
}

2
src/Avalonia.Controls/TopLevel.cs

@ -318,7 +318,7 @@ namespace Avalonia.Controls
/// <summary>
/// Occurs when physical Back Button is pressed or a back navigation has been requested.
/// </summary>
public event EventHandler<RoutedEvent> BackRequested
public event EventHandler<RoutedEventArgs> BackRequested
{
add { AddHandler(BackRequestedEvent, value); }
remove { RemoveHandler(BackRequestedEvent, value); }

217
src/Avalonia.Controls/TreeView.cs

@ -76,10 +76,10 @@ namespace Avalonia.Controls
}
/// <summary>
/// Gets the <see cref="ITreeItemContainerGenerator"/> for the tree view.
/// Gets the <see cref="TreeItemContainerGenerator"/> for the tree view.
/// </summary>
public new ITreeItemContainerGenerator ItemContainerGenerator =>
(ITreeItemContainerGenerator)base.ItemContainerGenerator;
public new TreeItemContainerGenerator ItemContainerGenerator =>
(TreeItemContainerGenerator)base.ItemContainerGenerator;
/// <summary>
/// Gets or sets a value indicating whether to automatically scroll to newly selected items.
@ -208,7 +208,22 @@ namespace Avalonia.Controls
/// </remarks>
public void SelectAll()
{
SynchronizeItems(SelectedItems, ItemContainerGenerator.Index!.Items);
var allItems = new List<object>();
void AddItems(ItemsControl itemsControl)
{
foreach (var item in itemsControl.ItemsView)
allItems.Add(item!);
foreach (var child in itemsControl.GetRealizedContainers())
{
if (child is ItemsControl childItemsControl)
AddItems(childItemsControl);
}
}
AddItems(this);
SynchronizeItems(SelectedItems, allItems);
}
/// <summary>
@ -219,6 +234,62 @@ namespace Avalonia.Controls
SelectedItems.Clear();
}
public IEnumerable<Control> GetRealizedTreeContainers()
{
static IEnumerable<Control> GetRealizedContainers(ItemsControl itemsControl)
{
foreach (var container in itemsControl.GetRealizedContainers())
{
yield return container;
if (container is ItemsControl itemsControlContainer)
foreach (var child in GetRealizedContainers(itemsControlContainer))
yield return child;
}
}
return GetRealizedContainers(this);
}
public Control? TreeContainerFromItem(object item)
{
static Control? TreeContainerFromItem(ItemsControl itemsControl, object item)
{
if (itemsControl.ContainerFromItem(item) is { } container)
return container;
foreach (var child in itemsControl.GetRealizedContainers())
{
if (child is ItemsControl childItemsControl &&
TreeContainerFromItem(childItemsControl, item) is { } childContainer)
return childContainer;
}
return null;
}
return TreeContainerFromItem(this, item);
}
public object? TreeItemFromContainer(Control container)
{
static object? TreeItemFromContainer(ItemsControl itemsControl, Control container)
{
if (itemsControl.ItemFromContainer(container) is { } item)
return item;
foreach (var child in itemsControl.GetRealizedContainers())
{
if (child is ItemsControl childItemsControl &&
TreeItemFromContainer(childItemsControl, container) is { } childContainer)
return childContainer;
}
return null;
}
return TreeItemFromContainer(this, container);
}
/// <summary>
/// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
/// </summary>
@ -262,7 +333,7 @@ namespace Avalonia.Controls
if (AutoScrollToSelectedItem)
{
var container = ItemContainerGenerator.Index!.ContainerFromItem(e.NewItems![0]!);
var container = ContainerFromItem(e.NewItems![0]!);
container?.BringIntoView();
}
@ -302,7 +373,7 @@ namespace Avalonia.Controls
break;
case NotifyCollectionChangedAction.Reset:
foreach (Control container in ItemContainerGenerator.Index!.Containers)
foreach (var container in GetRealizedTreeContainers())
{
MarkContainerSelected(container, false);
}
@ -357,7 +428,7 @@ namespace Avalonia.Controls
private void MarkItemSelected(object item, bool selected)
{
var container = ItemContainerGenerator.Index!.ContainerFromItem(item)!;
var container = TreeContainerFromItem(item)!;
MarkContainerSelected(container, selected);
}
@ -390,6 +461,7 @@ namespace Avalonia.Controls
incc.CollectionChanged -= SelectedItemsCollectionChanged;
}
}
(bool handled, IInputElement? next) ICustomKeyboardNavigation.GetNext(IInputElement element,
NavigationDirection direction)
{
@ -398,8 +470,8 @@ namespace Avalonia.Controls
if (!this.IsVisualAncestorOf((Visual)element))
{
var result = _selectedItem != null ?
ItemContainerGenerator.Index!.ContainerFromItem(_selectedItem) :
ItemContainerGenerator.ContainerFromIndex(0);
TreeContainerFromItem(_selectedItem) :
ContainerFromIndex(0);
return (result != null, result); // SelectedItem may not be in the treeview.
}
@ -410,25 +482,19 @@ namespace Avalonia.Controls
return (false, null);
}
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
var result = CreateTreeItemContainerGenerator();
result.Index!.Materialized += ContainerMaterialized;
return result;
}
protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator() =>
CreateTreeItemContainerGenerator<TreeViewItem>();
protected internal override Control CreateContainerForItemOverride() => new TreeViewItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TreeViewItem;
protected ITreeItemContainerGenerator CreateTreeItemContainerGenerator<TVItem>() where TVItem: TreeViewItem, new()
protected internal override void PrepareContainerForItemOverride(Control container, object? item, int index)
{
return new TreeItemContainerGenerator<TVItem>(
this,
TreeViewItem.HeaderProperty,
TreeViewItem.ItemTemplateProperty,
TreeViewItem.ItemsProperty,
TreeViewItem.IsExpandedProperty);
base.PrepareContainerForItemOverride(container, item, index);
if (item == SelectedItem)
{
MarkContainerSelected(container, true);
if (AutoScrollToSelectedItem)
Dispatcher.UIThread.Post(container.BringIntoView);
}
}
/// <inheritdoc/>
@ -464,7 +530,7 @@ namespace Avalonia.Controls
}
else
{
SelectedItem = ElementAt(Items, 0);
SelectedItem = ItemsView[0];
}
}
@ -486,14 +552,19 @@ namespace Avalonia.Controls
NavigationDirection direction,
bool intoChildren)
{
IItemContainerGenerator? parentGenerator = GetParentContainerGenerator(from);
var parentItemsControl = from?.Parent switch
{
TreeView tv => (ItemsControl)tv,
TreeViewItem i => i,
_ => null
};
if (parentGenerator == null)
if (parentItemsControl == null)
{
return null;
}
var index = from is not null ? parentGenerator.IndexFromContainer(from) : -1;
var index = from is not null ? parentItemsControl.IndexFromContainer(from) : -1;
var parent = from?.Parent as ItemsControl;
TreeViewItem? result = null;
@ -502,7 +573,7 @@ namespace Avalonia.Controls
case NavigationDirection.Up:
if (index > 0)
{
var previous = (TreeViewItem)parentGenerator.ContainerFromIndex(index - 1)!;
var previous = (TreeViewItem)parentItemsControl.ContainerFromIndex(index - 1)!;
result = previous.IsExpanded && previous.ItemCount > 0 ?
(TreeViewItem)previous.ItemContainerGenerator.ContainerFromIndex(previous.ItemCount - 1)! :
previous;
@ -522,7 +593,7 @@ namespace Avalonia.Controls
}
else if (index < parent?.ItemCount - 1)
{
result = (TreeViewItem)parentGenerator.ContainerFromIndex(index + 1)!;
result = (TreeViewItem)parentItemsControl.ContainerFromIndex(index + 1)!;
}
else if (parent is TreeViewItem parentItem)
{
@ -571,7 +642,7 @@ namespace Avalonia.Controls
bool toggleModifier = false,
bool rightButton = false)
{
var item = ItemContainerGenerator.Index!.ItemFromContainer(container);
var item = TreeItemFromContainer(container);
if (item == null)
{
@ -582,7 +653,7 @@ namespace Avalonia.Controls
if (SelectedItem != null)
{
selectedContainer = ItemContainerGenerator.Index!.ContainerFromItem(SelectedItem);
selectedContainer = TreeContainerFromItem(SelectedItem);
}
var mode = SelectionMode;
@ -629,22 +700,10 @@ namespace Avalonia.Controls
}
}
private static IItemContainerGenerator? GetParentContainerGenerator(TreeViewItem? item)
[Obsolete]
private protected override ItemContainerGenerator CreateItemContainerGenerator()
{
if (item == null)
{
return null;
}
switch (item.Parent)
{
case TreeView treeView:
return treeView.ItemContainerGenerator;
case TreeViewItem treeViewItem:
return treeViewItem.ItemContainerGenerator;
default:
return null;
}
return new TreeItemContainerGenerator(this);
}
/// <summary>
@ -656,18 +715,16 @@ namespace Avalonia.Controls
/// <returns>Found first node.</returns>
private static TreeViewItem? FindFirstNode(TreeView treeView, TreeViewItem nodeA, TreeViewItem nodeB)
{
return FindInContainers(treeView.ItemContainerGenerator, nodeA, nodeB);
return FindInContainers(treeView, nodeA, nodeB);
}
private static TreeViewItem? FindInContainers(ITreeItemContainerGenerator containerGenerator,
private static TreeViewItem? FindInContainers(ItemsControl itemsControl,
TreeViewItem nodeA,
TreeViewItem nodeB)
{
IEnumerable<ItemContainerInfo> containers = containerGenerator.Containers;
foreach (ItemContainerInfo container in containers)
foreach (var container in itemsControl.GetRealizedContainers())
{
TreeViewItem? node = FindFirstNode(container.ContainerControl as TreeViewItem, nodeA, nodeB);
TreeViewItem? node = FindFirstNode(container as TreeViewItem, nodeA, nodeB);
if (node != null)
{
@ -692,7 +749,7 @@ namespace Avalonia.Controls
return match;
}
return FindInContainers(node.ItemContainerGenerator, nodeA, nodeB);
return FindInContainers(node, nodeA, nodeB);
}
/// <summary>
@ -731,9 +788,9 @@ namespace Avalonia.Controls
TreeViewItem? node = from;
while (node != to)
while (node is not null && node != to)
{
var item = ItemContainerGenerator.Index!.ItemFromContainer(node);
var item = TreeItemFromContainer(node);
if (item != null)
{
@ -743,7 +800,7 @@ namespace Avalonia.Controls
node = GetContainerInDirection(node, NavigationDirection.Down, true);
}
var toItem = ItemContainerGenerator.Index!.ItemFromContainer(to);
var toItem = TreeItemFromContainer(to);
if (toItem != null)
{
@ -800,45 +857,7 @@ namespace Avalonia.Controls
.OfType<TreeViewItem>()
.FirstOrDefault();
if (item != null)
{
if (item.ItemContainerGenerator.Index == ItemContainerGenerator.Index)
{
return item;
}
}
return null;
}
/// <summary>
/// Called when a new item container is materialized, to set its selected state.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
private void ContainerMaterialized(object? sender, ItemContainerEventArgs e)
{
var selectedItem = SelectedItem;
if (selectedItem == null)
{
return;
}
foreach (var container in e.Containers)
{
if (container.Item == selectedItem)
{
((TreeViewItem)container.ContainerControl).IsSelected = true;
if (AutoScrollToSelectedItem)
{
Dispatcher.UIThread.Post(container.ContainerControl.BringIntoView);
}
break;
}
}
return item?.TreeViewOwner == this ? item : null;
}
/// <summary>
@ -859,7 +878,7 @@ namespace Avalonia.Controls
}
else
{
container.Classes.Set(":selected", selected);
((IPseudoClasses)container.Classes).Set(":selected", selected);
}
}

36
src/Avalonia.Controls/TreeViewItem.cs

@ -60,7 +60,6 @@ namespace Avalonia.Controls
PressedMixin.Attach<TreeViewItem>();
FocusableProperty.OverrideDefaultValue<TreeViewItem>(true);
ItemsPanelProperty.OverrideDefaultValue<TreeViewItem>(DefaultPanel);
ParentProperty.Changed.AddClassHandler<TreeViewItem>((o, e) => o.OnParentChanged(e));
RequestBringIntoViewEvent.AddClassHandler<TreeViewItem>((x, e) => x.OnRequestBringIntoView(e));
}
@ -91,26 +90,10 @@ namespace Avalonia.Controls
private set { SetAndRaise(LevelProperty, ref _level, value); }
}
/// <summary>
/// Gets the <see cref="ITreeItemContainerGenerator"/> for the tree view.
/// </summary>
public new ITreeItemContainerGenerator ItemContainerGenerator =>
(ITreeItemContainerGenerator)base.ItemContainerGenerator;
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator() => CreateTreeItemContainerGenerator<TreeViewItem>();
internal TreeView? TreeViewOwner => _treeView;
/// <inheritdoc/>
protected ITreeItemContainerGenerator CreateTreeItemContainerGenerator<TVItem>()
where TVItem: TreeViewItem, new()
{
return new TreeItemContainerGenerator<TVItem>(
this,
TreeViewItem.HeaderProperty,
TreeViewItem.ItemTemplateProperty,
TreeViewItem.ItemsProperty,
TreeViewItem.IsExpandedProperty);
}
protected internal override Control CreateContainerForItemOverride() => new TreeViewItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TreeViewItem;
/// <inheritdoc/>
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
@ -120,7 +103,6 @@ namespace Avalonia.Controls
_treeView = this.GetLogicalAncestors().OfType<TreeView>().FirstOrDefault();
Level = CalculateDistanceFromLogicalParent<TreeView>(this) - 1;
ItemContainerGenerator.UpdateIndex();
if (ItemTemplate == null && _treeView?.ItemTemplate != null)
{
@ -136,7 +118,6 @@ namespace Avalonia.Controls
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnDetachedFromLogicalTree(e);
ItemContainerGenerator.UpdateIndex();
}
protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e)
@ -305,16 +286,5 @@ namespace Avalonia.Controls
e.Handled = true;
}
}
private void OnParentChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!((ILogical)this).IsAttachedToLogicalTree && e.NewValue is null)
{
// 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();
}
}
}
}

49
src/Avalonia.Controls/Utils/CollectionUtils.cs

@ -0,0 +1,49 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Avalonia.Controls.Utils
{
internal static class CollectionUtils
{
public static NotifyCollectionChangedEventArgs ResetEventArgs { get; } = new(NotifyCollectionChangedAction.Reset);
public static void InsertMany<T>(this List<T> list, int index, T item, int count)
{
var repeat = FastRepeat<T>.Instance;
repeat.Count = count;
repeat.Item = item;
list.InsertRange(index, FastRepeat<T>.Instance);
repeat.Item = default;
}
private class FastRepeat<T> : ICollection<T>
{
public static readonly FastRepeat<T> Instance = new();
public int Count { get; set; }
public bool IsReadOnly => true;
[AllowNull] public T Item { get; set; }
public void Add(T item) => throw new NotImplementedException();
public void Clear() => throw new NotImplementedException();
public bool Contains(T item) => throw new NotImplementedException();
public bool Remove(T item) => throw new NotImplementedException();
IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
public IEnumerator<T> GetEnumerator() => throw new NotImplementedException();
public void CopyTo(T[] array, int arrayIndex)
{
var end = arrayIndex + Count;
for (var i = arrayIndex; i < end; ++i)
{
array[i] = Item;
}
}
}
}
}

356
src/Avalonia.Controls/VirtualizingCarouselPanel.cs

@ -0,0 +1,356 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
namespace Avalonia.Controls
{
/// <summary>
/// A panel used by <see cref="Carousel"/> to display the current item.
/// </summary>
public class VirtualizingCarouselPanel : VirtualizingPanel, ILogicalScrollable
{
private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty =
AvaloniaProperty.RegisterAttached<VirtualizingCarouselPanel, Control, bool>("ItemIsOwnContainer");
private Size _extent;
private Vector _offset;
private Size _viewport;
private Stack<Control>? _recyclePool;
private Control? _realized;
private int _realizedIndex = -1;
private Control? _transitionFrom;
private int _transitionFromIndex = -1;
private CancellationTokenSource? _transition;
private EventHandler? _scrollInvalidated;
bool ILogicalScrollable.CanHorizontallyScroll { get; set; }
bool ILogicalScrollable.CanVerticallyScroll { get; set; }
bool ILogicalScrollable.IsLogicalScrollEnabled => true;
Size ILogicalScrollable.ScrollSize => new(1, 1);
Size ILogicalScrollable.PageScrollSize => new(1, 1);
Size IScrollable.Extent => Extent;
Size IScrollable.Viewport => Viewport;
Vector IScrollable.Offset
{
get => _offset;
set
{
if ((int)_offset.X != value.X)
InvalidateMeasure();
_offset = value;
}
}
private Size Extent
{
get => _extent;
set
{
if (_extent != value)
{
_extent = value;
_scrollInvalidated?.Invoke(this, EventArgs.Empty);
}
}
}
private Size Viewport
{
get => _viewport;
set
{
if (_viewport != value)
{
_viewport = value;
_scrollInvalidated?.Invoke(this, EventArgs.Empty);
}
}
}
event EventHandler? ILogicalScrollable.ScrollInvalidated
{
add => _scrollInvalidated += value;
remove => _scrollInvalidated -= value;
}
bool ILogicalScrollable.BringIntoView(Control target, Rect targetRect)
{
throw new NotImplementedException();
}
Control? ILogicalScrollable.GetControlInDirection(NavigationDirection direction, Control? from)
{
throw new NotImplementedException();
}
void ILogicalScrollable.RaiseScrollInvalidated(EventArgs e) => _scrollInvalidated?.Invoke(this, e);
protected override Size MeasureOverride(Size availableSize)
{
var items = Items;
var index = (int)_offset.X;
if (index != _realizedIndex)
{
if (_realized is not null)
{
var cancelTransition = _transition is not null;
// Cancel any already running transition, and recycle the element we're transitioning from.
if (cancelTransition)
{
_transition!.Cancel();
_transition = null;
if (_transitionFrom is not null)
RecycleElement(_transitionFrom);
_transitionFrom = null;
_transitionFromIndex = -1;
}
if (cancelTransition || GetTransition() is null)
{
// If don't have a transition or we've just canceled a transition then recycle the element
// we're moving from.
RecycleElement(_realized);
}
else
{
// We have a transition to do: record the current element as the element we're transitioning
// from and we'll start the transition in the arrange pass.
_transitionFrom = _realized;
_transitionFromIndex = _realizedIndex;
}
_realized = null;
_realizedIndex = -1;
}
// Get or create an element for the new item.
if (index >= 0 && index < items.Count)
{
_realized = GetOrCreateElement(items, index);
_realizedIndex = index;
}
}
if (_realized is null)
{
Extent = Viewport = new(0, 0);
_transitionFrom = null;
_transitionFromIndex = -1;
return default;
}
_realized.Measure(availableSize);
Extent = new(items.Count, 1);
Viewport = new(1, 1);
return _realized.DesiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
var result = base.ArrangeOverride(finalSize);
if (_transition is null &&
_transitionFrom is not null &&
_realized is { } to &&
GetTransition() is { } transition)
{
_transition = new CancellationTokenSource();
transition.Start(_transitionFrom, to, _realizedIndex > _transitionFromIndex, _transition.Token)
.ContinueWith(TransitionFinished, TaskScheduler.FromCurrentSynchronizationContext());
}
return result;
}
protected override IInputElement? GetControl(NavigationDirection direction, IInputElement? from, bool wrap) => null;
protected internal override Control? ContainerFromIndex(int index)
{
return index == _realizedIndex ? _realized : null;
}
protected internal override IEnumerable<Control>? GetRealizedContainers()
{
return _realized is not null ? new[] { _realized } : null;
}
protected internal override int IndexFromContainer(Control container)
{
return container == _realized ? _realizedIndex : -1;
}
protected internal override Control? ScrollIntoView(int index)
{
return null;
}
protected override void OnItemsChanged(IReadOnlyList<object?> items, NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(items, e);
void Add(int index, int count)
{
if (index <= _realizedIndex)
_realizedIndex += count;
}
void Remove(int index, int count)
{
var end = index + (count - 1);
if (_realized is not null && index <= _realizedIndex && end >= _realizedIndex)
{
RecycleElement(_realized);
_realized = null;
_realizedIndex = -1;
}
else if (index < _realizedIndex)
{
_realizedIndex -= count;
}
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Add(e.NewStartingIndex, e.NewItems!.Count);
break;
case NotifyCollectionChangedAction.Remove:
Remove(e.OldStartingIndex, e.OldItems!.Count);
break;
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
Remove(e.OldStartingIndex, e.OldItems!.Count);
Add(e.NewStartingIndex, e.NewItems!.Count);
break;
case NotifyCollectionChangedAction.Reset:
if (_realized is not null)
{
RecycleElement(_realized);
_realized = null;
_realizedIndex = -1;
}
break;
}
InvalidateMeasure();
}
private Control GetOrCreateElement(IReadOnlyList<object?> items, int index)
{
return GetRealizedElement(index) ??
GetItemIsOwnContainer(items, index) ??
GetRecycledElement(items, index) ??
CreateElement(items, index);
}
private Control? GetRealizedElement(int index)
{
return _realizedIndex == index ? _realized : null;
}
private Control? GetItemIsOwnContainer(IReadOnlyList<object?> items, int index)
{
Debug.Assert(ItemContainerGenerator is not null);
var item = items[index];
if (item is Control controlItem)
{
var generator = ItemContainerGenerator;
if (controlItem.IsSet(ItemIsOwnContainerProperty))
{
controlItem.IsVisible = true;
generator.ItemContainerPrepared(controlItem, item, index);
return controlItem;
}
else if (generator.IsItemItsOwnContainer(controlItem))
{
generator.PrepareItemContainer(controlItem, controlItem, index);
AddInternalChild(controlItem);
controlItem.SetValue(ItemIsOwnContainerProperty, true);
generator.ItemContainerPrepared(controlItem, item, index);
return controlItem;
}
}
return null;
}
private Control? GetRecycledElement(IReadOnlyList<object?> items, int index)
{
Debug.Assert(ItemContainerGenerator is not null);
var generator = ItemContainerGenerator;
var item = items[index];
if (_recyclePool?.Count > 0)
{
var recycled = _recyclePool.Pop();
recycled.IsVisible = true;
generator.PrepareItemContainer(recycled, item, index);
generator.ItemContainerPrepared(recycled, item, index);
return recycled;
}
return null;
}
private Control CreateElement(IReadOnlyList<object?> items, int index)
{
Debug.Assert(ItemContainerGenerator is not null);
var generator = ItemContainerGenerator;
var item = items[index];
var container = generator.CreateContainer();
generator.PrepareItemContainer(container, item, index);
AddInternalChild(container);
generator.ItemContainerPrepared(container, item, index);
return container;
}
private void RecycleElement(Control element)
{
Debug.Assert(ItemContainerGenerator is not null);
if (element.IsSet(ItemIsOwnContainerProperty))
{
element.IsVisible = false;
}
else
{
ItemContainerGenerator.ClearItemContainer(element);
_recyclePool ??= new();
_recyclePool.Push(element);
element.IsVisible = false;
}
}
private IPageTransition? GetTransition() => (ItemsControl as Carousel)?.PageTransition;
private void TransitionFinished(Task task)
{
if (task.IsCanceled)
return;
if (_transitionFrom is not null)
RecycleElement(_transitionFrom);
_transition = null;
_transitionFrom = null;
_transitionFromIndex = -1;
}
}
}

241
src/Avalonia.Controls/VirtualizingPanel.cs

@ -0,0 +1,241 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Layout;
namespace Avalonia.Controls
{
/// <summary>
/// Base class for panels that can be used to virtualize items for an <see cref="ItemsControl"/>.
/// </summary>
/// <remarks>
/// Panels should implement the abstract members of this class to provide virtualization of
/// items in a <see cref="ItemsControl"/>. Derived panels can manage scrolling by implementing
/// <see cref="ILogicalScrollable"/> or by listening to the
/// <see cref="Layoutable.EffectiveViewportChanged"/> event.
///
/// The methods on the <see cref="ItemContainerGenerator"/> should be used to create, prepare and
/// clear containers for items.
/// </remarks>
public abstract class VirtualizingPanel : Panel, INavigableContainer
{
private ItemsControl? _itemsControl;
/// <summary>
/// Gets the <see cref="ItemContainerGenerator"/> for this <see cref="VirtualizingPanel"/>.
/// </summary>
public ItemContainerGenerator? ItemContainerGenerator => _itemsControl?.ItemContainerGenerator;
/// <summary>
/// Gets the items to display.
/// </summary>
protected IReadOnlyList<object?> Items => ItemsControl?.ItemsView ?? ItemsSourceView.Empty;
/// <summary>
/// Gets the <see cref="ItemsControl"/> that the panel is displaying items for.
/// </summary>
protected ItemsControl? ItemsControl
{
get => _itemsControl;
private set
{
if (_itemsControl != value)
{
var oldValue = _itemsControl;
_itemsControl= value;
OnItemsControlChanged(oldValue);
}
}
}
IInputElement? INavigableContainer.GetControl(NavigationDirection direction, IInputElement? from, bool wrap)
{
return GetControl(direction, from, wrap);
}
/// <summary>
/// Scrolls the specified item into view.
/// </summary>
/// <param name="index">The index of the item.</param>
/// <returns>
/// The element with the specified index, or null if the element could not be brought into view.
/// </returns>
protected internal abstract Control? ScrollIntoView(int index);
/// <summary>
/// Returns the container for the item at the specified index.
/// </summary>
/// <param name="index">The index of the item to retrieve.</param>
/// <returns>
/// The container for the item at the specified index within the item collection, if the
/// item is realized; otherwise, null.
/// </returns>
protected internal abstract Control? ContainerFromIndex(int index);
/// <summary>
/// Returns the index to the item that has the specified realized container.
/// </summary>
/// <param name="container">The generated container to retrieve the item index for.</param>
/// <returns>
/// The index to the item that corresponds to the specified realized container, or -1 if
/// <paramref name="container"/> is not found.
/// </returns>
protected internal abstract int IndexFromContainer(Control container);
/// <summary>
/// Gets the currently realized containers.
/// </summary>
protected internal abstract IEnumerable<Control>? GetRealizedContainers();
/// <summary>
/// Gets the next control in the specified direction.
/// </summary>
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <param name="wrap">Whether to wrap around when the first or last item is reached.</param>
/// <returns>The control.</returns>
protected abstract IInputElement? GetControl(NavigationDirection direction, IInputElement? from, bool wrap);
/// <summary>
/// Called when the <see cref="ItemsControl"/> that owns the panel changes.
/// </summary>
/// <param name="oldValue">
/// The old value of the <see cref="ItemsControl"/> property.
/// </param>
protected virtual void OnItemsControlChanged(ItemsControl? oldValue)
{
}
/// <summary>
/// Called when the <see cref="ItemsControl.Items"/> collection of the owner
/// <see cref="ItemsControl"/> changes.
/// </summary>
/// <param name="items">The items.</param>
/// <param name="e">The event args.</param>
/// <remarks>
/// This method is called a <see cref="INotifyCollectionChanged"/> event is raised by
/// the items, or when the <see cref="ItemsControl.Items"/> property is assigned a
/// new collection, in which case the <see cref="NotifyCollectionChangedAction"/> will
/// be <see cref="NotifyCollectionChangedAction.Reset"/>.
/// </remarks>
protected virtual void OnItemsChanged(IReadOnlyList<object?> items, NotifyCollectionChangedEventArgs e)
{
}
/// <summary>
/// Adds the specified <see cref="Control"/> to the <see cref="Panel.Children"/> collection
/// of a <see cref="VirtualizingPanel"/> element.
/// </summary>
/// <param name="control">The control to add to the collection.</param>
protected void AddInternalChild(Control control)
{
var itemsControl = EnsureItemsControl();
itemsControl.AddLogicalChild(control);
Children.Add(control);
}
/// <summary>
/// Adds the specified <see cref="Control"/> to the <see cref="Panel.Children"/> collection
/// of a <see cref="VirtualizingPanel"/> element at the specified index position.
/// </summary>
/// <param name="index">
/// The index position within the collection at which the child element is inserted.
/// </param>
/// <param name="control">The control to add to the collection.</param>
protected void InsertInternalChild(int index, Control control)
{
var itemsControl = EnsureItemsControl();
itemsControl.AddLogicalChild(control);
Children.Insert(index, control);
}
/// <summary>
/// Removes a child element from the <see cref="Panel.Children"/> collection.
/// </summary>
/// <param name="child">The child to remove/</param>
protected void RemoveInternalChild(Control child)
{
var itemsControl = EnsureItemsControl();
itemsControl.RemoveLogicalChild(child);
Children.Remove(child);
}
/// <summary>
/// Removes child elements from the <see cref="Panel.Children"/> collection.
/// </summary>
/// <param name="index">
/// The beginning index position within the collection at which the first child element is
/// removed.
/// </param>
/// <param name="count">The number of child elements to remove.</param>
protected void RemoveInternalChildRange(int index, int count)
{
var itemsControl = EnsureItemsControl();
for (var i = 0; i < count; ++i)
{
var c = Children[i];
itemsControl.RemoveLogicalChild(c);
}
Children.RemoveRange(index, count);
}
internal void Attach(ItemsControl itemsControl)
{
if (ItemsControl is not null)
throw new InvalidOperationException("The VirtualizingPanel is already attached to an ItemsControl");
ItemsControl = itemsControl;
ItemsControl.PropertyChanged += OnItemsControlPropertyChanged;
ItemsControl.ItemsView.PostCollectionChanged += OnItemsControlItemsChanged;
}
internal void Detach()
{
var itemsControl = EnsureItemsControl();
itemsControl.PropertyChanged -= OnItemsControlPropertyChanged;
itemsControl.ItemsView.PostCollectionChanged -= OnItemsControlItemsChanged;
ItemsControl = null;
Children.Clear();
}
internal void Refresh() => OnItemsControlItemsChanged(null, CollectionUtils.ResetEventArgs);
private ItemsControl EnsureItemsControl()
{
if (ItemsControl is null)
ThrowNotAttached();
return ItemsControl;
}
private protected virtual void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == ItemsControl.ItemsViewProperty)
{
var (oldValue, newValue) = e.GetOldAndNewValue<ItemsSourceView>();
oldValue.PostCollectionChanged -= OnItemsControlItemsChanged;
Refresh();
newValue.PostCollectionChanged += OnItemsControlItemsChanged;
}
}
private void OnItemsControlItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
OnItemsChanged(_itemsControl?.ItemsView ?? ItemsSourceView.Empty, e);
}
[DoesNotReturn]
private static void ThrowNotAttached()
{
throw new InvalidOperationException("The VirtualizingPanel does not belong to an ItemsControl.");
}
}
}

1111
src/Avalonia.Controls/VirtualizingStackPanel.cs

File diff suppressed because it is too large

21
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs

@ -8,7 +8,7 @@ using Avalonia.Collections;
namespace Avalonia.Diagnostics.ViewModels
{
internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList<TreeNode>, IDisposable
internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList<TreeNode>, IList, IDisposable
{
private class EmptyTreeNodeCollection : TreeNodeCollection
{
@ -33,6 +33,16 @@ namespace Avalonia.Diagnostics.ViewModels
public int Count => EnsureInitialized().Count;
protected TreeNode Owner { get; }
bool IList.IsFixedSize => false;
bool IList.IsReadOnly => true;
bool ICollection.IsSynchronized => false;
object ICollection.SyncRoot => this;
object? IList.this[int index]
{
get => this[index];
set => throw new NotImplementedException();
}
public event NotifyCollectionChangedEventHandler? CollectionChanged
{
@ -75,5 +85,14 @@ namespace Avalonia.Diagnostics.ViewModels
}
return _inner;
}
int IList.Add(object? value) => throw new NotImplementedException();
void IList.Clear() => throw new NotImplementedException();
bool IList.Contains(object? value) => EnsureInitialized().Contains((TreeNode)value!);
int IList.IndexOf(object? value) => EnsureInitialized().IndexOf((TreeNode)value!);
void IList.Insert(int index, object? value) => throw new NotImplementedException();
void IList.Remove(object? value) => throw new NotImplementedException();
void IList.RemoveAt(int index) => throw new NotImplementedException();
void ICollection.CopyTo(Array array, int index) => throw new NotImplementedException();
}
}

3
src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml

@ -40,8 +40,7 @@
BorderBrush="{DynamicResource ThemeControlMidBrush}"
BorderThickness="0,0,0,1"
FontFamily="/Assets/Fonts/SourceSansPro-Regular.ttf"
Items="{Binding History}"
VirtualizationMode="None">
Items="{Binding History}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">

4
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml

@ -11,7 +11,9 @@
<TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:TreeNode"
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Spacing="8">
<StackPanel Orientation="Horizontal" Spacing="8"
PointerEntered="AddAdorner"
PointerExited="RemoveAdorner">
<TextBlock Text="{Binding Type}" FontWeight="{Binding FontWeight}"/>
<TextBlock Text="{Binding Classes}"/>
<TextBlock Foreground="Gray" Text="{Binding ElementName}"/>

26
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs

@ -21,7 +21,6 @@ namespace Avalonia.Diagnostics.Views
{
InitializeComponent();
_tree = this.GetControl<TreeView>("tree");
_tree.ItemContainerGenerator.Index!.Materialized += TreeViewItemMaterialized;
_adorner = new Panel
{
@ -102,30 +101,5 @@ namespace Avalonia.Diagnostics.Views
{
AvaloniaXamlLoader.Load(this);
}
private void TreeViewItemMaterialized(object? sender, ItemContainerEventArgs e)
{
var item = (TreeViewItem)e.Containers[0].ContainerControl;
item.TemplateApplied += TreeViewItemTemplateApplied;
}
private void TreeViewItemTemplateApplied(object? sender, TemplateAppliedEventArgs e)
{
var item = (TreeViewItem)sender!;
// This depends on the default tree item template.
// We want to handle events in the item header but exclude events coming from children.
var header = item.FindDescendantOfType<Border>();
Debug.Assert(header != null);
if (header != null)
{
header.PointerEntered += AddAdorner;
header.PointerExited += RemoveAdorner;
}
item.TemplateApplied -= TreeViewItemTemplateApplied;
}
}
}

21
src/Avalonia.Themes.Fluent/Controls/Carousel.xaml

@ -3,17 +3,16 @@
<ControlTheme x:Key="{x:Type Carousel}" TargetType="Carousel">
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<CarouselPresenter IsVirtualized="{TemplateBinding IsVirtualized}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
Margin="{TemplateBinding Padding}"
SelectedIndex="{TemplateBinding SelectedIndex}"
PageTransition="{TemplateBinding PageTransition}"/>
</Border>
<ScrollViewer Name="PART_ScrollViewer"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden">
<ItemsPresenter Name="PART_ItemsPresenter"
ItemsPanel="{TemplateBinding ItemsPanel}"
Margin="{TemplateBinding Padding}"/>
</ScrollViewer>
</ControlTemplate>
</Setter>
</ControlTheme>

5
src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml

@ -128,11 +128,8 @@
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
Margin="{DynamicResource ComboBoxDropdownContentMargin}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
VirtualizationMode="{TemplateBinding VirtualizationMode}" />
ItemsPanel="{TemplateBinding ItemsPanel}" />
</ScrollViewer>
</Border>
</Popup>

2
src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml

@ -56,9 +56,7 @@
CornerRadius="{TemplateBinding CornerRadius}">
<ScrollViewer Theme="{StaticResource FluentMenuScrollViewer}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{DynamicResource MenuFlyoutScrollerMargin}"
KeyboardNavigation.TabNavigation="Continue"
Grid.IsSharedSizeScope="True" />

4
src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml

@ -9,9 +9,7 @@
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"/>
ItemsPanel="{TemplateBinding ItemsPanel}"/>
</Border>
</ControlTemplate>
</Setter>

5
src/Avalonia.Themes.Fluent/Controls/ListBox.xaml

@ -34,11 +34,8 @@
IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{TemplateBinding Padding}"
VirtualizationMode="{TemplateBinding VirtualizationMode}" />
Margin="{TemplateBinding Padding}"/>
</ScrollViewer>
</Border>
</ControlTemplate>

6
src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml

@ -220,7 +220,6 @@
<TextBlock Grid.Column="8" Text="Size" />
</Grid>
<ListBox x:Name="PART_Files"
VirtualizationMode="Simple"
Items="{Binding Items}"
Margin="0 5"
SelectionMode="{Binding SelectionMode}"
@ -277,11 +276,8 @@
<Border Name="border" BoxShadow="inset -6 0 3 -3 #20000000" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer Name="PART_ScrollViewer" HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}" VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{TemplateBinding Padding}"
VirtualizationMode="{TemplateBinding VirtualizationMode}"/>
Margin="{TemplateBinding Padding}"/>
</ScrollViewer>
</Border>
</ControlTemplate>

4
src/Avalonia.Themes.Fluent/Controls/Menu.xaml

@ -48,9 +48,7 @@
CornerRadius="{DynamicResource OverlayCornerRadius}">
<ScrollViewer Theme="{StaticResource FluentMenuScrollViewer}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{DynamicResource MenuFlyoutScrollerMargin}"
Grid.IsSharedSizeScope="True" />
</ScrollViewer>
@ -101,9 +99,7 @@
HorizontalAlignment="Stretch"
Padding="{TemplateBinding Padding}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
VerticalAlignment="Stretch"
KeyboardNavigation.TabNavigation="Continue" />
</Border>

2
src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml

@ -22,9 +22,7 @@
Theme="{StaticResource FluentMenuScrollViewer}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{DynamicResource MenuFlyoutScrollerMargin}"
KeyboardNavigation.TabNavigation="Continue"
Grid.IsSharedSizeScope="True" />

2
src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml

@ -131,9 +131,7 @@
CornerRadius="{DynamicResource OverlayCornerRadius}">
<ScrollViewer Theme="{StaticResource FluentMenuScrollViewer}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{DynamicResource MenuFlyoutScrollerMargin}"
Grid.IsSharedSizeScope="True" />
</ScrollViewer>

2
src/Avalonia.Themes.Fluent/Controls/TabControl.xaml

@ -32,9 +32,7 @@
VerticalAlignment="{TemplateBinding VerticalAlignment}">
<DockPanel>
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
DockPanel.Dock="{TemplateBinding TabStripPlacement}" />
<ContentPresenter Name="PART_SelectedContentHost"
Margin="{TemplateBinding Padding}"

4
src/Avalonia.Themes.Fluent/Controls/TabStrip.xaml

@ -19,9 +19,7 @@
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}" />
ItemsPanel="{TemplateBinding ItemsPanel}" />
</Border>
</ControlTemplate>
</Setter>

1
src/Avalonia.Themes.Fluent/Controls/TreeView.xaml

@ -32,7 +32,6 @@
IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
Margin="{TemplateBinding Padding}" />
</ScrollViewer>

2
src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml

@ -89,6 +89,7 @@
Grid.Column="1"
Focusable="False"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Margin="{TemplateBinding Padding}" />
@ -96,7 +97,6 @@
</Border>
<ItemsPresenter Name="PART_ItemsPresenter"
IsVisible="{TemplateBinding IsExpanded}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}" />
</StackPanel>
</ControlTemplate>

24
src/Avalonia.Themes.Simple/Controls/Carousel.xaml

@ -1,20 +1,18 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ControlTheme x:Key="{x:Type Carousel}"
TargetType="Carousel">
<ControlTheme x:Key="{x:Type Carousel}" TargetType="Carousel">
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<CarouselPresenter Margin="{TemplateBinding Padding}"
IsVirtualized="{TemplateBinding IsVirtualized}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
PageTransition="{TemplateBinding PageTransition}"
SelectedIndex="{TemplateBinding SelectedIndex}" />
</Border>
<ScrollViewer Name="PART_ScrollViewer"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden">
<ItemsPresenter Name="PART_ItemsPresenter"
ItemsPanel="{TemplateBinding ItemsPanel}"
Margin="{TemplateBinding Padding}"/>
</ScrollViewer>
</ControlTemplate>
</Setter>
</ControlTheme>

5
src/Avalonia.Themes.Simple/Controls/ComboBox.xaml

@ -62,10 +62,7 @@
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
<ItemsPresenter Name="PART_ItemsPresenter"
ItemTemplate="{TemplateBinding ItemTemplate}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
VirtualizationMode="{TemplateBinding VirtualizationMode}" />
ItemsPanel="{TemplateBinding ItemsPanel}" />
</ScrollViewer>
</Border>
</Popup>

2
src/Avalonia.Themes.Simple/Controls/ContextMenu.xaml

@ -17,8 +17,6 @@
CornerRadius="{TemplateBinding CornerRadius}">
<ScrollViewer Theme="{StaticResource SimpleMenuScrollViewer}">
<ItemsPresenter Name="PART_ItemsPresenter"
ItemTemplate="{TemplateBinding ItemTemplate}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
KeyboardNavigation.TabNavigation="Continue" />
</ScrollViewer>

2
src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml

@ -10,8 +10,6 @@
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<ItemsPresenter Name="PART_ItemsPresenter"
ItemTemplate="{TemplateBinding ItemTemplate}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}" />
</Border>
</ControlTemplate>

5
src/Avalonia.Themes.Simple/Controls/ListBox.xaml

@ -23,10 +23,7 @@
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
<ItemsPresenter Name="PART_ItemsPresenter"
Margin="{TemplateBinding Padding}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
VirtualizationMode="{TemplateBinding VirtualizationMode}" />
ItemsPanel="{TemplateBinding ItemsPanel}" />
</ScrollViewer>
</Border>
</ControlTemplate>

3
src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml

@ -163,8 +163,7 @@
Items="{Binding Items}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
SelectedItems="{Binding SelectedItems}"
SelectionMode="{Binding SelectionMode}"
VirtualizationMode="Simple">
SelectionMode="{Binding SelectionMode}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Background="Transparent">

4
src/Avalonia.Themes.Simple/Controls/Menu.xaml

@ -36,8 +36,6 @@
<ItemsPresenter Name="PART_ItemsPresenter"
Margin="2"
Grid.IsSharedSizeScope="True"
ItemTemplate="{TemplateBinding ItemTemplate}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}" />
</ScrollViewer>
</Border>
@ -72,8 +70,6 @@
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<ItemsPresenter Name="PART_ItemsPresenter"
ItemTemplate="{TemplateBinding ItemTemplate}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
KeyboardNavigation.TabNavigation="Continue" />
</Border>

2
src/Avalonia.Themes.Simple/Controls/MenuFlyoutPresenter.xaml

@ -20,8 +20,6 @@
<ItemsPresenter Name="PART_ItemsPresenter"
Margin="{TemplateBinding Padding}"
Grid.IsSharedSizeScope="True"
ItemTemplate="{TemplateBinding ItemTemplate}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
KeyboardNavigation.TabNavigation="Continue" />
</ScrollViewer>

2
src/Avalonia.Themes.Simple/Controls/MenuItem.xaml

@ -72,8 +72,6 @@
<ItemsPresenter Name="PART_ItemsPresenter"
Margin="2"
Grid.IsSharedSizeScope="True"
ItemTemplate="{TemplateBinding ItemTemplate}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}" />
</ScrollViewer>
</Border>

2
src/Avalonia.Themes.Simple/Controls/TabControl.xaml

@ -14,8 +14,6 @@
CornerRadius="{TemplateBinding CornerRadius}">
<DockPanel>
<ItemsPresenter Name="PART_ItemsPresenter"
ItemTemplate="{TemplateBinding ItemTemplate}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}" />
<ContentPresenter Name="PART_SelectedContentHost"
Margin="{TemplateBinding Padding}"

2
src/Avalonia.Themes.Simple/Controls/TabStrip.xaml

@ -5,8 +5,6 @@
<Setter Property="Template">
<ControlTemplate>
<ItemsPresenter Name="PART_ItemsPresenter"
ItemTemplate="{TemplateBinding ItemTemplate}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}" />
</ControlTemplate>
</Setter>

1
src/Avalonia.Themes.Simple/Controls/TreeView.xaml

@ -21,7 +21,6 @@
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
<ItemsPresenter Name="PART_ItemsPresenter"
Margin="{TemplateBinding Padding}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}" />
</ScrollViewer>
</Border>

2
src/Avalonia.Themes.Simple/Controls/TreeViewItem.xaml

@ -59,12 +59,12 @@
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalAlignment}"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
Focusable="False" />
</Grid>
</Border>
<ItemsPresenter Name="PART_ItemsPresenter"
IsVisible="{TemplateBinding IsExpanded}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}" />
</StackPanel>
</ControlTemplate>

2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs

@ -79,7 +79,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
var parentType = parentObject.Type.GetClrType();
if (context.GetAvaloniaTypes().IItemsPresenterHost.IsDirectlyAssignableFrom(parentType)
if (context.GetAvaloniaTypes().ItemsControl.IsDirectlyAssignableFrom(parentType)
|| context.GetAvaloniaTypes().ItemsRepeater.IsDirectlyAssignableFrom(parentType))
{
inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject);

4
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -54,7 +54,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType DataTemplate { get; }
public IXamlType IDataTemplate { get; }
public IXamlType IItemsPresenterHost { get; }
public IXamlType ItemsControl { get; }
public IXamlType ItemsRepeater { get; }
public IXamlType ReflectionBindingExtension { get; }
@ -178,7 +178,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
ResolveByNameExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ResolveByNameExtension");
DataTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.DataTemplate");
IDataTemplate = cfg.TypeSystem.GetType("Avalonia.Controls.Templates.IDataTemplate");
IItemsPresenterHost = cfg.TypeSystem.GetType("Avalonia.Controls.Presenters.IItemsPresenterHost");
ItemsControl = cfg.TypeSystem.GetType("Avalonia.Controls.ItemsControl");
ItemsRepeater = cfg.TypeSystem.GetType("Avalonia.Controls.ItemsRepeater");
ReflectionBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension");
RelativeSource = cfg.TypeSystem.GetType("Avalonia.Data.RelativeSource");

2
src/iOS/Avalonia.iOS/TextInputResponder.cs

@ -445,7 +445,7 @@ partial class AvaloniaView
UITextPosition IUITextInput.BeginningOfDocument => _beginningOfDocument;
private int DocumentLength => _client.SurroundingText.Text.Length + (_markedText?.Length ?? 0);
private int DocumentLength => (_client.SurroundingText.Text?.Length ?? 0) + (_markedText?.Length ?? 0);
UITextPosition IUITextInput.EndOfDocument => new AvaloniaTextPosition(DocumentLength);
UITextRange IUITextInput.MarkedTextRange

233
tests/Avalonia.Controls.UnitTests/CarouselTests.cs

@ -1,9 +1,12 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
@ -16,9 +19,10 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void First_Item_Should_Be_Selected_By_Default()
{
using var app = Start();
var target = new Carousel
{
Template = new FuncControlTemplate<Carousel>(CreateTemplate),
Template = CarouselTemplate(),
Items = new[]
{
"Foo",
@ -26,7 +30,7 @@ namespace Avalonia.Controls.UnitTests
}
};
target.ApplyTemplate();
Prepare(target);
Assert.Equal(0, target.SelectedIndex);
Assert.Equal("Foo", target.SelectedItem);
@ -35,9 +39,10 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void LogicalChild_Should_Be_Selected_Item()
{
using var app = Start();
var target = new Carousel
{
Template = new FuncControlTemplate<Carousel>(CreateTemplate),
Template = CarouselTemplate(),
Items = new[]
{
"Foo",
@ -45,38 +50,19 @@ namespace Avalonia.Controls.UnitTests
}
};
target.ApplyTemplate();
((Control)target.Presenter).ApplyTemplate();
Prepare(target);
Assert.Single(target.GetLogicalChildren());
Assert.Single(target.GetRealizedContainers());
var child = GetContainerTextBlock(target.GetLogicalChildren().Single());
var child = GetContainerTextBlock(target.GetRealizedContainers().Single());
Assert.Equal("Foo", child.Text);
}
[Fact]
public void Should_Remove_NonCurrent_Page_When_IsVirtualized_True()
{
var target = new Carousel
{
Template = new FuncControlTemplate<Carousel>(CreateTemplate),
Items = new[] { "foo", "bar" },
IsVirtualized = true,
SelectedIndex = 0,
};
target.ApplyTemplate();
((Control)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()
{
using var app = Start();
var items = new ObservableCollection<string>
{
"Foo",
@ -86,62 +72,26 @@ namespace Avalonia.Controls.UnitTests
var target = new Carousel
{
Template = new FuncControlTemplate<Carousel>(CreateTemplate),
Items = items,
IsVirtualized = false
};
target.ApplyTemplate();
((Control)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<string>
{
"Foo",
"Bar",
"FooBar"
};
var target = new Carousel
{
Template = new FuncControlTemplate<Carousel>(CreateTemplate),
Template = CarouselTemplate(),
Items = items,
IsVirtualized = true,
};
target.ApplyTemplate();
((Control)target.Presenter).ApplyTemplate();
Prepare(target);
Assert.Single(target.GetLogicalChildren());
Assert.Single(target.GetRealizedContainers());
var child = GetContainerTextBlock(target.GetLogicalChildren().Single());
var child = GetContainerTextBlock(target.GetRealizedContainers().Single());
Assert.Equal("Foo", child.Text);
var newItems = items.ToList();
newItems.RemoveAt(0);
Layout(target);
target.Items = newItems;
Layout(target);
child = GetContainerTextBlock(target.GetLogicalChildren().Single());
child = GetContainerTextBlock(target.GetRealizedContainers().Single());
Assert.Equal("Bar", child.Text);
}
@ -149,29 +99,30 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Selected_Item_Changes_To_First_Item_When_Item_Added()
{
using var app = Start();
var items = new ObservableCollection<string>();
var target = new Carousel
{
Template = new FuncControlTemplate<Carousel>(CreateTemplate),
Template = CarouselTemplate(),
Items = items,
IsVirtualized = false
};
target.ApplyTemplate();
((Control)target.Presenter).ApplyTemplate();
Prepare(target);
Assert.Equal(-1, target.SelectedIndex);
Assert.Empty(target.GetLogicalChildren());
Assert.Empty(target.GetRealizedContainers());
items.Add("Foo");
Layout(target);
Assert.Equal(0, target.SelectedIndex);
Assert.Single(target.GetLogicalChildren());
Assert.Single(target.GetRealizedContainers());
}
[Fact]
public void Selected_Index_Changes_To_None_When_Items_Assigned_Null()
{
using var app = Start();
var items = new ObservableCollection<string>
{
"Foo",
@ -181,31 +132,30 @@ namespace Avalonia.Controls.UnitTests
var target = new Carousel
{
Template = new FuncControlTemplate<Carousel>(CreateTemplate),
Template = CarouselTemplate(),
Items = items,
IsVirtualized = false
};
target.ApplyTemplate();
((Control)target.Presenter).ApplyTemplate();
Prepare(target);
Assert.Equal(3, target.GetLogicalChildren().Count());
Assert.Equal(1, target.GetRealizedContainers().Count());
var child = GetContainerTextBlock(target.GetLogicalChildren().First());
var child = GetContainerTextBlock(target.GetRealizedContainers().First());
Assert.Equal("Foo", child.Text);
target.Items = null;
Layout(target);
var numChildren = target.GetLogicalChildren().Count();
var numChildren = target.GetRealizedContainers().Count();
Assert.Equal(0, numChildren);
Assert.Equal(-1, target.SelectedIndex);
}
[Fact]
public void Selected_Index_Is_Maintained_Carousel_Created_With_Non_Zero_SelectedIndex()
{
using var app = Start();
var items = new ObservableCollection<string>
{
"Foo",
@ -215,26 +165,24 @@ namespace Avalonia.Controls.UnitTests
var target = new Carousel
{
Template = new FuncControlTemplate<Carousel>(CreateTemplate),
Template = CarouselTemplate(),
Items = items,
IsVirtualized = false,
SelectedIndex = 2
};
target.ApplyTemplate();
((Control)target.Presenter).ApplyTemplate();
Prepare(target);
Assert.Equal("FooBar", target.SelectedItem);
var child = GetContainerTextBlock(target.GetVisualDescendants().LastOrDefault());
var child = GetContainerTextBlock(target.GetRealizedContainers().LastOrDefault());
Assert.IsType<TextBlock>(child);
Assert.Equal("FooBar", ((TextBlock)child).Text);
Assert.Equal("FooBar", child.Text);
}
[Fact]
public void Selected_Item_Changes_To_Next_First_Item_When_Item_Removed_From_Beggining_Of_List()
{
using var app = Start();
var items = new ObservableCollection<string>
{
"Foo",
@ -244,31 +192,29 @@ namespace Avalonia.Controls.UnitTests
var target = new Carousel
{
Template = new FuncControlTemplate<Carousel>(CreateTemplate),
Template = CarouselTemplate(),
Items = items,
IsVirtualized = false
};
target.ApplyTemplate();
((Control)target.Presenter).ApplyTemplate();
Prepare(target);
Assert.Equal(3, target.GetLogicalChildren().Count());
var child = GetContainerTextBlock(target.GetLogicalChildren().First());
var child = GetContainerTextBlock(target.GetRealizedContainers().First());
Assert.Equal("Foo", child.Text);
items.RemoveAt(0);
Layout(target);
child = GetContainerTextBlock(target.GetLogicalChildren().First());
child = GetContainerTextBlock(target.GetRealizedContainers().First());
Assert.IsType<TextBlock>(child);
Assert.Equal("Bar", ((TextBlock)child).Text);
Assert.Equal("Bar", child.Text);
}
[Fact]
public void Selected_Item_Changes_To_First_Item_If_SelectedItem_Is_Removed_From_Middle()
{
using var app = Start();
var items = new ObservableCollection<string>
{
"Foo",
@ -278,13 +224,11 @@ namespace Avalonia.Controls.UnitTests
var target = new Carousel
{
Template = new FuncControlTemplate<Carousel>(CreateTemplate),
Template = CarouselTemplate(),
Items = items,
IsVirtualized = false
};
target.ApplyTemplate();
((Control)target.Presenter).ApplyTemplate();
Prepare(target);
target.SelectedIndex = 1;
@ -294,26 +238,6 @@ namespace Avalonia.Controls.UnitTests
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<ContentPresenter>(control);
contentPresenter.UpdateChild();
return Assert.IsType<TextBlock>(contentPresenter.Child);
}
[Fact]
public void SelectedItem_Validation()
{
@ -321,12 +245,10 @@ namespace Avalonia.Controls.UnitTests
{
var target = new Carousel
{
Template = new FuncControlTemplate<Carousel>(CreateTemplate),
IsVirtualized = false
Template = CarouselTemplate(),
};
target.ApplyTemplate();
((Control)target.Presenter).ApplyTemplate();
Prepare(target);
var exception = new System.InvalidCastException("failed validation");
var textObservable =
@ -338,5 +260,62 @@ namespace Avalonia.Controls.UnitTests
Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
}
}
private static IDisposable Start() => UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
private static void Prepare(Carousel target)
{
var root = new TestRoot(target);
root.LayoutManager.ExecuteInitialLayoutPass();
}
private static void Layout(Carousel target)
{
((ILayoutRoot)target.GetVisualRoot()).LayoutManager.ExecuteLayoutPass();
}
private static IControlTemplate CarouselTemplate()
{
return new FuncControlTemplate((c, ns) =>
new ScrollViewer
{
Name = "PART_ScrollViewer",
Template = ScrollViewerTemplate(),
HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden,
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
Content = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsPanelProperty] = c[~ItemsControl.ItemsPanelProperty],
}.RegisterInNameScope(ns)
}.RegisterInNameScope(ns));
}
private static FuncControlTemplate ScrollViewerTemplate()
{
return new FuncControlTemplate<ScrollViewer>((parent, scope) =>
new Panel
{
Children =
{
new ScrollContentPresenter
{
Name = "PART_ContentPresenter",
[~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(),
[~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty],
[~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty],
[~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty],
[~ScrollContentPresenter.CanHorizontallyScrollProperty] = parent[~ScrollViewer.CanHorizontallyScrollProperty],
[~ScrollContentPresenter.CanVerticallyScrollProperty] = parent[~ScrollViewer.CanVerticallyScrollProperty],
}.RegisterInNameScope(scope),
}
});
}
private static TextBlock GetContainerTextBlock(object control)
{
var contentPresenter = Assert.IsType<ContentPresenter>(control);
return Assert.IsType<TextBlock>(contentPresenter.Child);
}
}
}

2
tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

@ -204,7 +204,6 @@ namespace Avalonia.Controls.UnitTests
Child = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
[!ItemsPresenter.ItemsProperty] = parent[!ComboBox.ItemsProperty],
}.RegisterInNameScope(scope)
}.RegisterInNameScope(scope)
}
@ -286,7 +285,6 @@ namespace Avalonia.Controls.UnitTests
var target = new ComboBox
{
Template = GetTemplate(),
VirtualizationMode = ItemVirtualizationMode.None
};
target.ApplyTemplate();

173
tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs

@ -1,173 +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<ContentPresenter>()
.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<object>().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<ListBoxItem>(owner, ListBoxItem.ContentProperty, null);
var container = (ListBoxItem)target.Materialize(0, "foo").ContainerControl;
Assert.Equal("foo", container.Content);
container.Bind(
ContentPresenter.ContentProperty,
Observable.Never<object>().StartWith("bar"),
BindingPriority.Style);
Assert.Equal("bar", container.Content);
}
[Fact]
public void Materialize_Should_Create_Containers_When_Item_Is_Null()
{
var owner = new Decorator();
var target = new ItemContainerGenerator<ListBoxItem>(owner, ListBoxItem.ContentProperty, null);
var container = (ListBoxItem)target.Materialize(0, null).ContainerControl;
Assert.True(container != null, "The containers is not materialized.");
}
private static IList<ItemContainerInfo> Materialize(
IItemContainerGenerator generator,
int index,
string[] items)
{
var result = new List<ItemContainerInfo>();
foreach (var item in items)
{
var container = generator.Materialize(index++, item);
result.Add(container);
}
return result;
}
}
}

42
tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs

@ -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<ListBoxItem>(owner, ListBoxItem.ContentProperty, null);
var containers = Materialize(target, 0, items);
var result = containers
.Select(x => x.ContainerControl)
.OfType<ListBoxItem>()
.Select(x => x.Content)
.ToList();
Assert.Equal(items, result);
}
private static IList<ItemContainerInfo> Materialize(
IItemContainerGenerator generator,
int index,
string[] items)
{
var result = new List<ItemContainerInfo>();
foreach (var item in items)
{
var container = generator.Materialize(index++, item);
result.Add(container);
}
return result;
}
}
}

152
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@ -1,3 +1,4 @@
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
@ -34,6 +35,31 @@ namespace Avalonia.Controls.UnitTests
Assert.IsType<Canvas>(container.Child);
}
[Fact]
public void ItemTemplate_Can_Be_Changed()
{
var target = new ItemsControl
{
Template = GetTemplate(),
ItemTemplate = new FuncDataTemplate<string>((_, __) => new Canvas()),
};
target.Items = new[] { "Foo" };
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var container = (ContentPresenter)target.Presenter.Panel.Children[0];
container.UpdateChild();
Assert.IsType<Canvas>(container.Child);
target.ItemTemplate = new FuncDataTemplate<string>((_, __) => new Border());
container = (ContentPresenter)target.Presenter.Panel.Children[0];
container.UpdateChild();
Assert.IsType<Border>(container.Child);
}
[Fact]
public void Panel_Should_Have_TemplatedParent_Set_To_ItemsControl()
{
@ -65,7 +91,7 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Container_Should_Have_Theme_Set_To_ItemContainerTheme()
{
var theme = new ControlTheme();
var theme = new ControlTheme { TargetType = typeof(ContentPresenter) };
var target = new ItemsControl
{
ItemContainerTheme = theme,
@ -367,12 +393,13 @@ namespace Avalonia.Controls.UnitTests
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Assert.Equal(2, target.ItemContainerGenerator.Containers.Count());
var panel = target.Presenter.Panel;
Assert.Equal(2, panel.Children.Count());
target.Template = GetTemplate();
target.ApplyTemplate();
Assert.Empty(target.ItemContainerGenerator.Containers);
Assert.Empty(panel.Children);
}
[Fact]
@ -535,27 +562,6 @@ namespace Avalonia.Controls.UnitTests
Assert.DoesNotContain(":singleitem", target.Classes);
}
[Fact]
public void Setting_Presenter_Explicitly_Should_Set_Item_Parent()
{
var target = new TestItemsControl();
var child = new Control();
var presenter = new ItemsPresenter
{
[StyledElement.TemplatedParentProperty] = target,
[~ItemsPresenter.ItemsProperty] = target[~ItemsControl.ItemsProperty],
};
presenter.ApplyTemplate();
target.Presenter = presenter;
target.Items = new[] { child };
target.ApplyTemplate();
Assert.Equal(target, child.Parent);
Assert.Equal(target, ((ILogical)child).LogicalParent);
}
[Fact]
public void DataContexts_Should_Be_Correctly_Set()
{
@ -684,36 +690,6 @@ namespace Avalonia.Controls.UnitTests
}
}
[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<Canvas>(e.Containers[0].Item);
};
target.Items = new[]
{
new Canvas()
};
}
[Fact]
public void Detaching_Then_Reattaching_To_Logical_Tree_Twice_Does_Not_Throw()
{
@ -757,6 +733,57 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(container.Child!.GetValue(TextBlock.TextProperty), "3");
}
[Fact]
public void DisplayMemberBinding_Can_Be_Changed()
{
var target = new ItemsControl
{
Template = GetTemplate(),
DisplayMemberBinding = new Binding("Value")
};
target.Items = new[] { new Item("Foo", "Bar") };
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var container = (ContentPresenter)target.Presenter.Panel.Children[0];
container.UpdateChild();
Assert.Equal(container.Child!.GetValue(TextBlock.TextProperty), "Bar");
target.DisplayMemberBinding = new Binding("Caption");
container = (ContentPresenter)target.Presenter.Panel.Children[0];
container.UpdateChild();
Assert.Equal(container.Child!.GetValue(TextBlock.TextProperty), "Foo");
}
[Fact]
public void Cannot_Set_Both_DisplayMemberBinding_And_ItemTemplate_1()
{
var target = new ItemsControl
{
Template = GetTemplate(),
DisplayMemberBinding = new Binding("Length")
};
Assert.Throws<InvalidOperationException>(() =>
target.ItemTemplate = new FuncDataTemplate<string>((_, _) => new TextBlock()));
}
[Fact]
public void Cannot_Set_Both_DisplayMemberBinding_And_ItemTemplate_2()
{
var target = new ItemsControl
{
Template = GetTemplate(),
ItemTemplate = new FuncDataTemplate<string>((_, _) => new TextBlock()),
};
Assert.Throws<InvalidOperationException>(() => target.DisplayMemberBinding = new Binding("Length"));
}
private class Item
{
public Item(string value)
@ -764,6 +791,13 @@ namespace Avalonia.Controls.UnitTests
Value = value;
}
public Item(string caption, string value)
{
Caption = caption;
Value = value;
}
public string Caption { get; }
public string Value { get; }
}
@ -777,19 +811,9 @@ namespace Avalonia.Controls.UnitTests
Child = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
}.RegisterInNameScope(scope)
};
});
}
private class TestItemsControl : ItemsControl
{
public new IItemsPresenter Presenter
{
get { return base.Presenter; }
set { base.Presenter = value; }
}
}
}
}

11
tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs

@ -15,7 +15,7 @@ namespace Avalonia.Controls.UnitTests
public void Only_Subscribes_To_Source_CollectionChanged_When_CollectionChanged_Subscribed()
{
var source = new AvaloniaList<string>();
var target = new ItemsSourceView<string>(source);
var target = ItemsSourceView.GetOrCreate(source);
var debug = (INotifyCollectionChangedDebug)source;
Assert.Null(debug.GetCollectionChangedSubscribers());
@ -31,18 +31,11 @@ namespace Avalonia.Controls.UnitTests
Assert.Null(debug.GetCollectionChangedSubscribers());
}
[Fact]
public void Cannot_Wrap_An_ItemsSourceView_In_Another()
{
var source = new ItemsSourceView<string>(new string[0]);
Assert.Throws<ArgumentException>(() => new ItemsSourceView<string>(source));
}
[Fact]
public void Cannot_Create_ItemsSourceView_With_Collection_That_Implements_INCC_But_Not_List()
{
var source = new InvalidCollection();
Assert.Throws<ArgumentException>(() => new ItemsSourceView<string>(source));
Assert.Throws<ArgumentException>(() => ItemsSourceView.GetOrCreate(source));
}
private class InvalidCollection : INotifyCollectionChanged, IEnumerable<string>

212
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Collections;
@ -7,6 +10,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Styling;
using Avalonia.UnitTests;
@ -217,6 +221,7 @@ namespace Avalonia.Controls.UnitTests
// Scroll down a page.
target.Scroll.Offset = new Vector(0, 10);
Layout(target);
// Make sure recycled item isn't now selected.
Assert.False(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected);
@ -238,8 +243,8 @@ namespace Avalonia.Controls.UnitTests
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);
}
}
@ -262,11 +267,13 @@ namespace Avalonia.Controls.UnitTests
items.Clear();
items.AddRange(Enumerable.Range(0, 11).Select(x => $"Item {x}"));
Layout(target);
items.Remove("Item 2");
Layout(target);
Assert.Equal(
items,
target.Presenter.Panel.Children.Cast<ListBoxItem>().Select(x => (string)x.Content));
var actual = target.Presenter.Panel.Children.Cast<ListBoxItem>().Select(x => (string)x.Content).ToList();
Assert.Equal(items.OrderBy(x => x), actual.OrderBy(x => x));
}
}
@ -327,47 +334,9 @@ namespace Avalonia.Controls.UnitTests
_mouse.Click(listBox, item, mouseButton);
}
[Fact]
public void ListBox_After_Scroll_IndexOutOfRangeException_Shouldnt_Be_Thrown()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var items = Enumerable.Range(0, 11).Select(x => $"{x}").ToArray();
var target = new ListBox
{
Template = ListBoxTemplate(),
Items = items,
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 11 })
};
Prepare(target);
var panel = target.Presenter.Panel as IVirtualizingPanel;
var listBoxItems = panel.Children.OfType<ListBoxItem>();
//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<string>(Enumerable.Range(1, 7).Select(v => v.ToString()));
@ -385,7 +354,6 @@ namespace Avalonia.Controls.UnitTests
target.Height = 110;
target.Width = 50;
target.DataContext = items;
target.VirtualizationMode = virtualizationMode;
target.ItemTemplate = new FuncDataTemplate<object>((c, _) =>
{
@ -446,7 +414,6 @@ namespace Avalonia.Controls.UnitTests
AutoScrollToSelectedItem = true,
Height = 100,
Width = 50,
VirtualizationMode = ItemVirtualizationMode.Simple,
ItemTemplate = new FuncDataTemplate<object>((c, _) => new Border() { Height = 10 }),
Items = items,
};
@ -465,11 +432,11 @@ namespace Avalonia.Controls.UnitTests
items.Remove("1");
lm.ExecuteLayoutPass();
Assert.Equal("30", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 1).DataContext);
Assert.Equal("29", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 2).DataContext);
Assert.Equal("28", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 3).DataContext);
Assert.Equal("27", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 4).DataContext);
Assert.Equal("26", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 5).DataContext);
Assert.Equal("30", target.ContainerFromIndex(items.Count - 1).DataContext);
Assert.Equal("29", target.ContainerFromIndex(items.Count - 2).DataContext);
Assert.Equal("28", target.ContainerFromIndex(items.Count - 3).DataContext);
Assert.Equal("27", target.ContainerFromIndex(items.Count - 4).DataContext);
Assert.Equal("26", target.ContainerFromIndex(items.Count - 5).DataContext);
}
}
@ -486,7 +453,6 @@ namespace Avalonia.Controls.UnitTests
Items = items,
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 }),
SelectionMode = SelectionMode.AlwaysSelected,
VirtualizationMode = ItemVirtualizationMode.None,
};
Prepare(target);
@ -527,10 +493,9 @@ namespace Avalonia.Controls.UnitTests
var target = new ListBox()
{
VerticalAlignment = Layout.VerticalAlignment.Top,
VerticalAlignment = VerticalAlignment.Top,
AutoScrollToSelectedItem = true,
Width = 50,
VirtualizationMode = ItemVirtualizationMode.Simple,
ItemTemplate = new FuncDataTemplate<object>((c, _) => new Border() { Height = 10 }),
Items = items,
};
@ -600,9 +565,7 @@ namespace Avalonia.Controls.UnitTests
Content = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(),
[~ItemsPresenter.ItemsPanelProperty] = parent.GetObservable(ItemsControl.ItemsPanelProperty).ToBinding(),
[~ItemsPresenter.VirtualizationModeProperty] = parent.GetObservable(ListBox.VirtualizationModeProperty).ToBinding(),
}.RegisterInNameScope(scope)
}.RegisterInNameScope(scope));
}
@ -632,6 +595,8 @@ namespace Avalonia.Controls.UnitTests
[~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty],
[~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty],
[~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty],
[~ScrollContentPresenter.CanHorizontallyScrollProperty] = parent[~ScrollViewer.CanHorizontallyScrollProperty],
[~ScrollContentPresenter.CanVerticallyScrollProperty] = parent[~ScrollViewer.CanVerticallyScrollProperty],
}.RegisterInNameScope(scope),
new ScrollBar
{
@ -645,45 +610,28 @@ namespace Avalonia.Controls.UnitTests
private static 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();
target.Width = target.Height = 100;
// 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)
var root = new TestRoot(target)
{
item.Template = ListBoxItemTemplate();
item.ApplyTemplate();
item.Presenter.ApplyTemplate();
((ContentPresenter)item.Presenter).UpdateChild();
}
Resources =
{
{
typeof(ListBoxItem),
new ControlTheme(typeof(ListBoxItem))
{
Setters = { new Setter(ListBoxItem.TemplateProperty, ListBoxItemTemplate()) }
}
}
}
};
// The items were created before the template was applied, so now we need to go back
// and re-arrange everything.
foreach (Control i in target.GetSelfAndVisualDescendants())
{
i.InvalidateMeasure();
}
root.LayoutManager.ExecuteInitialLayoutPass();
}
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
private static void Layout(Control c)
{
((ILayoutRoot)c.GetVisualRoot()).LayoutManager.ExecuteLayoutPass();
}
private class Item
@ -705,7 +653,6 @@ namespace Avalonia.Controls.UnitTests
Items = new[] { "Foo" },
ItemTemplate = new FuncDataTemplate<string>((_, __) => new Canvas()),
SelectionMode = SelectionMode.AlwaysSelected,
VirtualizationMode = ItemVirtualizationMode.None
};
Prepare(target);
@ -718,6 +665,71 @@ namespace Avalonia.Controls.UnitTests
Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
}
[Fact]
public void Handles_Resetting_Items()
{
var items = new ResettingCollection(100);
var target = new ListBox
{
Template = ListBoxTemplate(),
Items = items,
ItemTemplate = new FuncDataTemplate<string>((_, __) => new Canvas { Height = 10 }),
};
Prepare(target);
var realized = target.GetRealizedContainers()
.Cast<ListBoxItem>()
.Select(x => (string)x.DataContext)
.ToList();
Assert.Equal(Enumerable.Range(0, 10).Select(x => $"Item{x}"), realized);
items.Reverse();
Layout(target);
realized = target.GetRealizedContainers()
.Cast<ListBoxItem>()
.Select(x => (string)x.DataContext)
.ToList();
Assert.Equal(Enumerable.Range(0, 10).Select(x => $"Item{99 - x}"), realized);
}
[Fact]
public void Handles_Resetting_Items_With_Existing_Selection_And_AutoScrollToSelectedItem()
{
var items = new ResettingCollection(100);
var target = new ListBox
{
Template = ListBoxTemplate(),
Items = items,
ItemTemplate = new FuncDataTemplate<string>((_, __) => new Canvas { Height = 10 }),
AutoScrollToSelectedItem = true,
SelectedIndex = 1,
};
Prepare(target);
var realized = target.GetRealizedContainers()
.Cast<ListBoxItem>()
.Select(x => (string)x.DataContext)
.ToList();
Assert.Equal(Enumerable.Range(0, 10).Select(x => $"Item{x}"), realized);
items.Reverse();
Layout(target);
realized = target.GetRealizedContainers()
.Cast<ListBoxItem>()
.Select(x => (string)x.DataContext)
.ToList();
// "Item1" should remain selected, and now be at the bottom of the viewport.
Assert.Equal(Enumerable.Range(0, 10).Select(x => $"Item{10 - x}"), realized);
}
private static void RaiseKeyEvent(ListBox listBox, Key key, KeyModifiers inputModifiers = 0)
{
listBox.RaiseEvent(new KeyEventArgs
@ -766,5 +778,23 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(true, first.IsSelected);
}
}
private class ResettingCollection : List<string>, INotifyCollectionChanged
{
public ResettingCollection(int itemCount)
{
AddRange(Enumerable.Range(0, itemCount).Select(x => $"Item{x}"));
}
public new void Reverse()
{
base.Reverse();
CollectionChanged?.Invoke(
this,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
}
}
}

1
tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs

@ -93,7 +93,6 @@ namespace Avalonia.Controls.UnitTests
Content = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsProperty] = ((Control)parent).GetObservable(ItemsControl.ItemsProperty).ToBinding(),
}.RegisterInNameScope(scope)
};
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save