Browse Source

Begin refactoring ItemsPresenter.

A lot still broken, in particular virtualization is completely removed.`ItemsPresenter` now no longer has an `Items` or `ItemTemplate` property; it detects when it's hosted in an `ItemsControl`. `IItemsPresenter` interface removed.
pull/9677/head
Steven Kirk 4 years ago
parent
commit
692c0c155d
  1. 588
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  2. 4
      samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
  3. 5
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  4. 5
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml
  5. 2
      src/Avalonia.Controls/ComboBox.cs
  6. 78
      src/Avalonia.Controls/IVirtualizingPanel.cs
  7. 12
      src/Avalonia.Controls/ItemsControl.cs
  8. 2
      src/Avalonia.Controls/ListBox.cs
  9. 344
      src/Avalonia.Controls/Presenters/CarouselPresenter.cs
  10. 18
      src/Avalonia.Controls/Presenters/IItemsPresenter.cs
  11. 4
      src/Avalonia.Controls/Presenters/IItemsPresenterHost.cs
  12. 125
      src/Avalonia.Controls/Presenters/ItemContainerSync.cs
  13. 303
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  14. 106
      src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs
  15. 606
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  16. 187
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  17. 308
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  18. 108
      src/Avalonia.Controls/Presenters/ItemsPresenterContainerGenerator.cs
  19. 2
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  20. 252
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  21. 1
      src/Avalonia.Themes.Fluent/Controls/Carousel.xaml
  22. 5
      src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
  23. 2
      src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml
  24. 4
      src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml
  25. 5
      src/Avalonia.Themes.Fluent/Controls/ListBox.xaml
  26. 5
      src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml
  27. 4
      src/Avalonia.Themes.Fluent/Controls/Menu.xaml
  28. 2
      src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml
  29. 2
      src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
  30. 2
      src/Avalonia.Themes.Fluent/Controls/TabControl.xaml
  31. 4
      src/Avalonia.Themes.Fluent/Controls/TabStrip.xaml
  32. 1
      src/Avalonia.Themes.Fluent/Controls/TreeView.xaml
  33. 1
      src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
  34. 1
      src/Avalonia.Themes.Simple/Controls/Carousel.xaml
  35. 5
      src/Avalonia.Themes.Simple/Controls/ComboBox.xaml
  36. 2
      src/Avalonia.Themes.Simple/Controls/ContextMenu.xaml
  37. 2
      src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml
  38. 5
      src/Avalonia.Themes.Simple/Controls/ListBox.xaml
  39. 4
      src/Avalonia.Themes.Simple/Controls/Menu.xaml
  40. 2
      src/Avalonia.Themes.Simple/Controls/MenuFlyoutPresenter.xaml
  41. 2
      src/Avalonia.Themes.Simple/Controls/MenuItem.xaml
  42. 2
      src/Avalonia.Themes.Simple/Controls/TabControl.xaml
  43. 2
      src/Avalonia.Themes.Simple/Controls/TabStrip.xaml
  44. 1
      src/Avalonia.Themes.Simple/Controls/TreeView.xaml
  45. 1
      src/Avalonia.Themes.Simple/Controls/TreeViewItem.xaml
  46. 1
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  47. 1
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  48. 4
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  49. 52
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  50. 1
      tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs
  51. 1
      tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs
  52. 1464
      tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs
  53. 370
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs
  54. 748
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs
  55. 1822
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs
  56. 5
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  57. 2
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  58. 1
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs
  59. 1
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  60. 1
      tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs
  61. 2
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  62. 522
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
  63. 1
      tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs
  64. 1
      tests/Avalonia.UnitTests/TestExtensions.cs

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

@ -24,301 +24,301 @@ namespace ControlCatalog.Pages
{ {
this.InitializeComponent(); this.InitializeComponent();
var results = this.Get<ItemsPresenter>("PickerLastResults"); //// var results = this.Get<ItemsPresenter>("PickerLastResults");
var resultsVisible = this.Get<TextBlock>("PickerLastResultsVisible"); //// var resultsVisible = this.Get<TextBlock>("PickerLastResultsVisible");
var bookmarkContainer = this.Get<TextBox>("BookmarkContainer"); //// var bookmarkContainer = this.Get<TextBox>("BookmarkContainer");
var openedFileContent = this.Get<TextBox>("OpenedFileContent"); //// var openedFileContent = this.Get<TextBox>("OpenedFileContent");
var openMultiple = this.Get<CheckBox>("OpenMultiple"); //// var openMultiple = this.Get<CheckBox>("OpenMultiple");
IStorageFolder? lastSelectedDirectory = null; //// IStorageFolder? lastSelectedDirectory = null;
List<FileDialogFilter> GetFilters() //// List<FileDialogFilter> GetFilters()
{ //// {
if (this.Get<CheckBox>("UseFilters").IsChecked != true) //// if (this.Get<CheckBox>("UseFilters").IsChecked != true)
return new List<FileDialogFilter>(); //// return new List<FileDialogFilter>();
return new List<FileDialogFilter> //// return new List<FileDialogFilter>
{ //// {
new FileDialogFilter //// new FileDialogFilter
{ //// {
Name = "Text files (.txt)", Extensions = new List<string> {"txt"} //// Name = "Text files (.txt)", Extensions = new List<string> {"txt"}
}, //// },
new FileDialogFilter //// new FileDialogFilter
{ //// {
Name = "All files", //// Name = "All files",
Extensions = new List<string> {"*"} //// Extensions = new List<string> {"*"}
} //// }
}; //// };
} //// }
List<FilePickerFileType>? GetFileTypes() //// List<FilePickerFileType>? GetFileTypes()
{ //// {
if (this.Get<CheckBox>("UseFilters").IsChecked != true) //// if (this.Get<CheckBox>("UseFilters").IsChecked != true)
return null; //// return null;
return new List<FilePickerFileType> //// return new List<FilePickerFileType>
{ //// {
FilePickerFileTypes.All, //// FilePickerFileTypes.All,
FilePickerFileTypes.TextPlain //// FilePickerFileTypes.TextPlain
}; //// };
} //// }
this.Get<Button>("OpenFile").Click += async delegate //// this.Get<Button>("OpenFile").Click += async delegate
{ //// {
// Almost guaranteed to exist //// // Almost guaranteed to exist
var uri = Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName; //// var uri = Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName;
var initialFileName = uri == null ? null : System.IO.Path.GetFileName(uri); //// var initialFileName = uri == null ? null : System.IO.Path.GetFileName(uri);
var initialDirectory = uri == null ? null : System.IO.Path.GetDirectoryName(uri); //// var initialDirectory = uri == null ? null : System.IO.Path.GetDirectoryName(uri);
var result = await new OpenFileDialog() //// var result = await new OpenFileDialog()
{ //// {
Title = "Open file", //// Title = "Open file",
Filters = GetFilters(), //// Filters = GetFilters(),
Directory = initialDirectory, //// Directory = initialDirectory,
InitialFileName = initialFileName //// InitialFileName = initialFileName
}.ShowAsync(GetWindow()); //// }.ShowAsync(GetWindow());
results.Items = result; //// results.Items = result;
resultsVisible.IsVisible = result?.Any() == true; //// resultsVisible.IsVisible = result?.Any() == true;
}; //// };
this.Get<Button>("OpenMultipleFiles").Click += async delegate //// this.Get<Button>("OpenMultipleFiles").Click += async delegate
{ //// {
var result = await new OpenFileDialog() //// var result = await new OpenFileDialog()
{ //// {
Title = "Open multiple files", //// Title = "Open multiple files",
Filters = GetFilters(), //// Filters = GetFilters(),
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null, //// Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null,
AllowMultiple = true //// AllowMultiple = true
}.ShowAsync(GetWindow()); //// }.ShowAsync(GetWindow());
results.Items = result; //// results.Items = result;
resultsVisible.IsVisible = result?.Any() == true; //// resultsVisible.IsVisible = result?.Any() == true;
}; //// };
this.Get<Button>("SaveFile").Click += async delegate //// this.Get<Button>("SaveFile").Click += async delegate
{ //// {
var filters = GetFilters(); //// var filters = GetFilters();
var result = await new SaveFileDialog() //// var result = await new SaveFileDialog()
{ //// {
Title = "Save file", //// Title = "Save file",
Filters = filters, //// Filters = filters,
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null, //// Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null,
DefaultExtension = filters?.Any() == true ? "txt" : null, //// DefaultExtension = filters?.Any() == true ? "txt" : null,
InitialFileName = "test.txt" //// InitialFileName = "test.txt"
}.ShowAsync(GetWindow()); //// }.ShowAsync(GetWindow());
results.Items = new[] { result }; //// results.Items = new[] { result };
resultsVisible.IsVisible = result != null; //// resultsVisible.IsVisible = result != null;
}; //// };
this.Get<Button>("SelectFolder").Click += async delegate //// this.Get<Button>("SelectFolder").Click += async delegate
{ //// {
var result = await new OpenFolderDialog() //// var result = await new OpenFolderDialog()
{ //// {
Title = "Select folder", //// Title = "Select folder",
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null //// Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null
}.ShowAsync(GetWindow()); //// }.ShowAsync(GetWindow());
if (string.IsNullOrEmpty(result)) //// if (string.IsNullOrEmpty(result))
{ //// {
resultsVisible.IsVisible = false; //// resultsVisible.IsVisible = false;
} //// }
else //// else
{ //// {
lastSelectedDirectory = new BclStorageFolder(new System.IO.DirectoryInfo(result)); //// lastSelectedDirectory = new BclStorageFolder(new System.IO.DirectoryInfo(result));
results.Items = new[] { result }; //// results.Items = new[] { result };
resultsVisible.IsVisible = true; //// resultsVisible.IsVisible = true;
} //// }
}; //// };
this.Get<Button>("OpenBoth").Click += async delegate //// this.Get<Button>("OpenBoth").Click += async delegate
{ //// {
var result = await new OpenFileDialog() //// var result = await new OpenFileDialog()
{ //// {
Title = "Select both", //// Title = "Select both",
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null, //// Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null,
AllowMultiple = true //// AllowMultiple = true
}.ShowManagedAsync(GetWindow(), new ManagedFileDialogOptions //// }.ShowManagedAsync(GetWindow(), new ManagedFileDialogOptions
{ //// {
AllowDirectorySelection = true //// AllowDirectorySelection = true
}); //// });
results.Items = result; //// results.Items = result;
resultsVisible.IsVisible = result?.Any() == true; //// resultsVisible.IsVisible = result?.Any() == true;
}; //// };
this.Get<Button>("DecoratedWindow").Click += delegate //// this.Get<Button>("DecoratedWindow").Click += delegate
{ //// {
new DecoratedWindow().Show(); //// new DecoratedWindow().Show();
}; //// };
this.Get<Button>("DecoratedWindowDialog").Click += delegate //// this.Get<Button>("DecoratedWindowDialog").Click += delegate
{ //// {
_ = new DecoratedWindow().ShowDialog(GetWindow()); //// _ = new DecoratedWindow().ShowDialog(GetWindow());
}; //// };
this.Get<Button>("Dialog").Click += delegate //// this.Get<Button>("Dialog").Click += delegate
{ //// {
var window = CreateSampleWindow(); //// var window = CreateSampleWindow();
window.Height = 200; //// window.Height = 200;
_ = window.ShowDialog(GetWindow()); //// _ = window.ShowDialog(GetWindow());
}; //// };
this.Get<Button>("DialogNoTaskbar").Click += delegate //// this.Get<Button>("DialogNoTaskbar").Click += delegate
{ //// {
var window = CreateSampleWindow(); //// var window = CreateSampleWindow();
window.Height = 200; //// window.Height = 200;
window.ShowInTaskbar = false; //// window.ShowInTaskbar = false;
_ = window.ShowDialog(GetWindow()); //// _ = window.ShowDialog(GetWindow());
}; //// };
this.Get<Button>("OwnedWindow").Click += delegate //// this.Get<Button>("OwnedWindow").Click += delegate
{ //// {
var window = CreateSampleWindow(); //// var window = CreateSampleWindow();
window.Show(GetWindow()); //// window.Show(GetWindow());
}; //// };
this.Get<Button>("OwnedWindowNoTaskbar").Click += delegate //// this.Get<Button>("OwnedWindowNoTaskbar").Click += delegate
{ //// {
var window = CreateSampleWindow(); //// var window = CreateSampleWindow();
window.ShowInTaskbar = false; //// window.ShowInTaskbar = false;
window.Show(GetWindow()); //// window.Show(GetWindow());
}; //// };
this.Get<Button>("OpenFilePicker").Click += async delegate //// this.Get<Button>("OpenFilePicker").Click += async delegate
{ //// {
var result = await GetStorageProvider().OpenFilePickerAsync(new FilePickerOpenOptions() //// var result = await GetStorageProvider().OpenFilePickerAsync(new FilePickerOpenOptions()
{ //// {
Title = "Open file", //// Title = "Open file",
FileTypeFilter = GetFileTypes(), //// FileTypeFilter = GetFileTypes(),
SuggestedStartLocation = lastSelectedDirectory, //// SuggestedStartLocation = lastSelectedDirectory,
AllowMultiple = openMultiple.IsChecked == true //// AllowMultiple = openMultiple.IsChecked == true
}); //// });
await SetPickerResult(result); //// await SetPickerResult(result);
}; //// };
this.Get<Button>("SaveFilePicker").Click += async delegate //// this.Get<Button>("SaveFilePicker").Click += async delegate
{ //// {
var fileTypes = GetFileTypes(); //// var fileTypes = GetFileTypes();
var file = await GetStorageProvider().SaveFilePickerAsync(new FilePickerSaveOptions() //// var file = await GetStorageProvider().SaveFilePickerAsync(new FilePickerSaveOptions()
{ //// {
Title = "Save file", //// Title = "Save file",
FileTypeChoices = fileTypes, //// FileTypeChoices = fileTypes,
SuggestedStartLocation = lastSelectedDirectory, //// SuggestedStartLocation = lastSelectedDirectory,
SuggestedFileName = "FileName", //// SuggestedFileName = "FileName",
DefaultExtension = fileTypes?.Any() == true ? "txt" : null, //// DefaultExtension = fileTypes?.Any() == true ? "txt" : null,
ShowOverwritePrompt = false //// ShowOverwritePrompt = false
}); //// });
if (file is not null && file.CanOpenWrite) //// if (file is not null && file.CanOpenWrite)
{ //// {
// Sync disposal of StreamWriter is not supported on WASM //// // Sync disposal of StreamWriter is not supported on WASM
#if NET6_0_OR_GREATER ////#if NET6_0_OR_GREATER
await using var stream = await file.OpenWriteAsync(); //// await using var stream = await file.OpenWriteAsync();
await using var reader = new System.IO.StreamWriter(stream); //// await using var reader = new System.IO.StreamWriter(stream);
#else ////#else
using var stream = await file.OpenWriteAsync(); //// using var stream = await file.OpenWriteAsync();
using var reader = new System.IO.StreamWriter(stream); //// using var reader = new System.IO.StreamWriter(stream);
#endif ////#endif
await reader.WriteLineAsync(openedFileContent.Text); //// await reader.WriteLineAsync(openedFileContent.Text);
lastSelectedDirectory = await file.GetParentAsync(); //// 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 //// this.Get<Button>("OpenFolderPicker").Click += async delegate
{ //// {
var folders = await GetStorageProvider().OpenFolderPickerAsync(new FolderPickerOpenOptions() //// var folders = await GetStorageProvider().OpenFolderPickerAsync(new FolderPickerOpenOptions()
{ //// {
Title = "Folder file", //// Title = "Folder file",
SuggestedStartLocation = lastSelectedDirectory, //// SuggestedStartLocation = lastSelectedDirectory,
AllowMultiple = openMultiple.IsChecked == true //// AllowMultiple = openMultiple.IsChecked == true
}); //// });
await SetPickerResult(folders); //// await SetPickerResult(folders);
lastSelectedDirectory = folders.FirstOrDefault(); //// lastSelectedDirectory = folders.FirstOrDefault();
}; //// };
this.Get<Button>("OpenFileFromBookmark").Click += async delegate //// this.Get<Button>("OpenFileFromBookmark").Click += async delegate
{ //// {
var file = bookmarkContainer.Text is not null //// var file = bookmarkContainer.Text is not null
? await GetStorageProvider().OpenFileBookmarkAsync(bookmarkContainer.Text) //// ? await GetStorageProvider().OpenFileBookmarkAsync(bookmarkContainer.Text)
: null; //// : null;
await SetPickerResult(file is null ? null : new[] { file }); //// await SetPickerResult(file is null ? null : new[] { file });
}; //// };
this.Get<Button>("OpenFolderFromBookmark").Click += async delegate //// this.Get<Button>("OpenFolderFromBookmark").Click += async delegate
{ //// {
var folder = bookmarkContainer.Text is not null //// var folder = bookmarkContainer.Text is not null
? await GetStorageProvider().OpenFolderBookmarkAsync(bookmarkContainer.Text) //// ? await GetStorageProvider().OpenFolderBookmarkAsync(bookmarkContainer.Text)
: null; //// : null;
await SetPickerResult(folder is null ? null : new[] { folder }); //// await SetPickerResult(folder is null ? null : new[] { folder });
lastSelectedDirectory = folder; //// lastSelectedDirectory = folder;
}; //// };
async Task SetPickerResult(IReadOnlyCollection<IStorageItem>? items) //// async Task SetPickerResult(IReadOnlyCollection<IStorageItem>? items)
{ //// {
items ??= Array.Empty<IStorageItem>(); //// items ??= Array.Empty<IStorageItem>();
bookmarkContainer.Text = items.FirstOrDefault(f => f.CanBookmark) is { } f ? await f.SaveBookmarkAsync() : "Can't bookmark"; //// bookmarkContainer.Text = items.FirstOrDefault(f => f.CanBookmark) is { } f ? await f.SaveBookmarkAsync() : "Can't bookmark";
var mappedResults = new List<string>(); //// var mappedResults = new List<string>();
if (items.FirstOrDefault() is IStorageItem item) //// if (items.FirstOrDefault() is IStorageItem item)
{ //// {
var resultText = item is IStorageFile ? "File:" : "Folder:"; //// var resultText = item is IStorageFile ? "File:" : "Folder:";
resultText += Environment.NewLine; //// resultText += Environment.NewLine;
var props = await item.GetBasicPropertiesAsync(); //// var props = await item.GetBasicPropertiesAsync();
resultText += @$"Size: {props.Size} //// resultText += @$"Size: {props.Size}
DateCreated: {props.DateCreated} ////DateCreated: {props.DateCreated}
DateModified: {props.DateModified} ////DateModified: {props.DateModified}
CanBookmark: {item.CanBookmark} ////CanBookmark: {item.CanBookmark}
"; ////";
if (item is IStorageFile file) //// if (item is IStorageFile file)
{ //// {
resultText += @$" //// resultText += @$"
CanOpenRead: {file.CanOpenRead} ////CanOpenRead: {file.CanOpenRead}
CanOpenWrite: {file.CanOpenWrite} ////CanOpenWrite: {file.CanOpenWrite}
Content: ////Content:
"; ////";
if (file.CanOpenRead) //// if (file.CanOpenRead)
{ //// {
#if NET6_0_OR_GREATER ////#if NET6_0_OR_GREATER
await using var stream = await file.OpenReadAsync(); //// await using var stream = await file.OpenReadAsync();
#else ////#else
using var stream = await file.OpenReadAsync(); //// using var stream = await file.OpenReadAsync();
#endif ////#endif
using var reader = new System.IO.StreamReader(stream); //// using var reader = new System.IO.StreamReader(stream);
// 4GB file test, shouldn't load more than 10000 chars into a memory. //// // 4GB file test, shouldn't load more than 10000 chars into a memory.
const int length = 10000; //// const int length = 10000;
var buffer = ArrayPool<char>.Shared.Rent(length); //// var buffer = ArrayPool<char>.Shared.Rent(length);
try //// try
{ //// {
var charsRead = await reader.ReadAsync(buffer, 0, length); //// var charsRead = await reader.ReadAsync(buffer, 0, length);
resultText += new string(buffer, 0, charsRead); //// resultText += new string(buffer, 0, charsRead);
} //// }
finally //// finally
{ //// {
ArrayPool<char>.Shared.Return(buffer); //// ArrayPool<char>.Shared.Return(buffer);
} //// }
} //// }
} //// }
openedFileContent.Text = resultText; //// openedFileContent.Text = resultText;
lastSelectedDirectory = await item.GetParentAsync(); //// lastSelectedDirectory = await item.GetParentAsync();
if (lastSelectedDirectory is not null) //// if (lastSelectedDirectory is not null)
{ //// {
mappedResults.Add(FullPathOrName(lastSelectedDirectory)); //// mappedResults.Add(FullPathOrName(lastSelectedDirectory));
} //// }
foreach (var selectedItem in items) //// foreach (var selectedItem in items)
{ //// {
mappedResults.Add("+> " + FullPathOrName(selectedItem)); //// mappedResults.Add("+> " + FullPathOrName(selectedItem));
if (selectedItem is IStorageFolder folder) //// if (selectedItem is IStorageFolder folder)
{ //// {
foreach (var innerItems in await folder.GetItemsAsync()) //// foreach (var innerItems in await folder.GetItemsAsync())
{ //// {
mappedResults.Add("++> " + FullPathOrName(innerItems)); //// mappedResults.Add("++> " + FullPathOrName(innerItems));
} //// }
} //// }
} //// }
} //// }
results.Items = mappedResults; //// results.Items = mappedResults;
resultsVisible.IsVisible = mappedResults.Any(); //// resultsVisible.IsVisible = mappedResults.Any();
} //// }
} }
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)

4
samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml

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

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

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

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

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

2
src/Avalonia.Controls/ComboBox.cs

@ -57,7 +57,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="VirtualizationMode"/> property. /// Defines the <see cref="VirtualizationMode"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty = public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty =
ItemsPresenter.VirtualizationModeProperty.AddOwner<ComboBox>(); VirtualizingStackPanel.VirtualizationModeProperty.AddOwner<ComboBox>();
/// <summary> /// <summary>
/// Defines the <see cref="PlaceholderText"/> property. /// Defines the <see cref="PlaceholderText"/> property.

78
src/Avalonia.Controls/IVirtualizingPanel.cs

@ -7,83 +7,5 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public interface IVirtualizingPanel 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();
} }
} }

12
src/Avalonia.Controls/ItemsControl.cs

@ -174,7 +174,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Gets the items presenter control. /// Gets the items presenter control.
/// </summary> /// </summary>
public IItemsPresenter? Presenter public ItemsPresenter? Presenter
{ {
get; get;
protected set; protected set;
@ -189,7 +189,7 @@ namespace Avalonia.Controls
} }
/// <inheritdoc/> /// <inheritdoc/>
void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter) void IItemsPresenterHost.RegisterItemsPresenter(ItemsPresenter presenter)
{ {
if (Presenter is IChildIndexProvider oldInnerProvider) if (Presenter is IChildIndexProvider oldInnerProvider)
{ {
@ -408,12 +408,6 @@ namespace Avalonia.Controls
UpdateItemCount(); UpdateItemCount();
RemoveControlItemsFromLogicalChildren(oldValue); RemoveControlItemsFromLogicalChildren(oldValue);
AddControlItemsToLogicalChildren(newValue); AddControlItemsToLogicalChildren(newValue);
if (Presenter != null)
{
Presenter.Items = newValue;
}
SubscribeToItems(newValue); SubscribeToItems(newValue);
} }
@ -437,8 +431,6 @@ namespace Avalonia.Controls
RemoveControlItemsFromLogicalChildren(e.OldItems); RemoveControlItemsFromLogicalChildren(e.OldItems);
break; break;
} }
Presenter?.ItemsChanged(e);
} }
/// <summary> /// <summary>

2
src/Avalonia.Controls/ListBox.cs

@ -51,7 +51,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="VirtualizationMode"/> property. /// Defines the <see cref="VirtualizationMode"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty = public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty =
ItemsPresenter.VirtualizationModeProperty.AddOwner<ListBox>(); VirtualizingStackPanel.VirtualizationModeProperty.AddOwner<ListBox>();
private IScrollable? _scroll; private IScrollable? _scroll;

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

@ -12,7 +12,7 @@ namespace Avalonia.Controls.Presenters
/// <summary> /// <summary>
/// Displays pages inside an <see cref="ItemsControl"/>. /// Displays pages inside an <see cref="ItemsControl"/>.
/// </summary> /// </summary>
public class CarouselPresenter : ItemsPresenterBase public class CarouselPresenter : ItemsPresenter
{ {
/// <summary> /// <summary>
/// Defines the <see cref="IsVirtualized"/> property. /// Defines the <see cref="IsVirtualized"/> property.
@ -43,8 +43,8 @@ namespace Avalonia.Controls.Presenters
/// </summary> /// </summary>
static CarouselPresenter() static CarouselPresenter()
{ {
IsVirtualizedProperty.Changed.AddClassHandler<CarouselPresenter>((x, e) => x.IsVirtualizedChanged(e)); ////IsVirtualizedProperty.Changed.AddClassHandler<CarouselPresenter>((x, e) => x.IsVirtualizedChanged(e));
SelectedIndexProperty.Changed.AddClassHandler<CarouselPresenter>((x, e) => x.SelectedIndexChanged(e)); ////SelectedIndexProperty.Changed.AddClassHandler<CarouselPresenter>((x, e) => x.SelectedIndexChanged(e));
} }
/// <summary> /// <summary>
@ -71,14 +71,14 @@ namespace Avalonia.Controls.Presenters
set set
{ {
var old = SelectedIndex; ////var old = SelectedIndex;
var effective = (value >= 0 && value < Items?.Cast<object>().Count()) ? value : -1; ////var effective = (value >= 0 && value < Items?.Cast<object>().Count()) ? value : -1;
if (old != effective) ////if (old != effective)
{ ////{
_selectedIndex = effective; //// _selectedIndex = effective;
RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue); //// RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue);
} ////}
} }
} }
@ -92,185 +92,185 @@ namespace Avalonia.Controls.Presenters
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void ItemsChanged(NotifyCollectionChangedEventArgs e) ////protected override void ItemsChanged(NotifyCollectionChangedEventArgs e)
{ ////{
if (!IsVirtualized) //// if (!IsVirtualized)
{ //// {
base.ItemsChanged(e); //// base.ItemsChanged(e);
if (Items == null || SelectedIndex >= Items.Count()) //// if (Items == null || SelectedIndex >= Items.Count())
{ //// {
SelectedIndex = Items.Count() - 1; //// SelectedIndex = Items.Count() - 1;
} //// }
foreach (var c in ItemContainerGenerator.Containers) //// foreach (var c in ItemContainerGenerator.Containers)
{ //// {
c.ContainerControl.IsVisible = c.Index == SelectedIndex; //// c.ContainerControl.IsVisible = c.Index == SelectedIndex;
} //// }
} //// }
else if (SelectedIndex != -1 && Panel != null) //// else if (SelectedIndex != -1 && Panel != null)
{ //// {
switch (e.Action) //// switch (e.Action)
{ //// {
case NotifyCollectionChangedAction.Add: //// case NotifyCollectionChangedAction.Add:
if (e.NewStartingIndex > SelectedIndex) //// if (e.NewStartingIndex > SelectedIndex)
{ //// {
return; //// return;
} //// }
break; //// break;
case NotifyCollectionChangedAction.Remove: //// case NotifyCollectionChangedAction.Remove:
if (e.OldStartingIndex > SelectedIndex) //// if (e.OldStartingIndex > SelectedIndex)
{ //// {
return; //// return;
} //// }
break; //// break;
case NotifyCollectionChangedAction.Replace: //// case NotifyCollectionChangedAction.Replace:
if (e.OldStartingIndex > SelectedIndex || //// if (e.OldStartingIndex > SelectedIndex ||
e.OldStartingIndex + e.OldItems!.Count - 1 < SelectedIndex) //// e.OldStartingIndex + e.OldItems!.Count - 1 < SelectedIndex)
{ //// {
return; //// return;
} //// }
break; //// break;
case NotifyCollectionChangedAction.Move: //// case NotifyCollectionChangedAction.Move:
if (e.OldStartingIndex > SelectedIndex && //// if (e.OldStartingIndex > SelectedIndex &&
e.NewStartingIndex > SelectedIndex) //// e.NewStartingIndex > SelectedIndex)
{ //// {
return; //// return;
} //// }
break; //// break;
} //// }
if (Items == null || SelectedIndex >= Items.Count()) //// if (Items == null || SelectedIndex >= Items.Count())
{ //// {
SelectedIndex = Items.Count() - 1; //// SelectedIndex = Items.Count() - 1;
} //// }
Panel.Children.Clear(); //// Panel.Children.Clear();
ItemContainerGenerator.Clear(); //// ItemContainerGenerator.Clear();
if (SelectedIndex != -1) //// if (SelectedIndex != -1)
{ //// {
GetOrCreateContainer(SelectedIndex); //// GetOrCreateContainer(SelectedIndex);
} //// }
} //// }
} ////}
protected override void PanelCreated(Panel panel) ////protected override void PanelCreated(Panel panel)
{ ////{
ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); //// ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
} ////}
/// <summary> /////// <summary>
/// Moves to the selected page, animating if a <see cref="PageTransition"/> is set. /////// Moves to the selected page, animating if a <see cref="PageTransition"/> is set.
/// </summary> /////// </summary>
/// <param name="fromIndex">The index of the old page.</param> /////// <param name="fromIndex">The index of the old page.</param>
/// <param name="toIndex">The index of the new page.</param> /////// <param name="toIndex">The index of the new page.</param>
/// <returns>A task tracking the animation.</returns> /////// <returns>A task tracking the animation.</returns>
private async Task MoveToPage(int fromIndex, int toIndex) ////private async Task MoveToPage(int fromIndex, int toIndex)
{ ////{
if (fromIndex != toIndex) //// if (fromIndex != toIndex)
{ //// {
var generator = ItemContainerGenerator; //// var generator = ItemContainerGenerator;
Control? from = null; //// Control? from = null;
Control? to = null; //// Control? to = null;
if (fromIndex != -1) //// if (fromIndex != -1)
{ //// {
from = generator.ContainerFromIndex(fromIndex); //// from = generator.ContainerFromIndex(fromIndex);
} //// }
if (toIndex != -1) //// if (toIndex != -1)
{ //// {
to = GetOrCreateContainer(toIndex); //// to = GetOrCreateContainer(toIndex);
} //// }
if (PageTransition != null && (from != null || to != null)) //// if (PageTransition != null && (from != null || to != null))
{ //// {
await PageTransition.Start((Visual?)from, (Visual?)to, fromIndex < toIndex, default); //// await PageTransition.Start((Visual?)from, (Visual?)to, fromIndex < toIndex, default);
} //// }
else if (to != null) //// else if (to != null)
{ //// {
to.IsVisible = true; //// to.IsVisible = true;
} //// }
if (from != null) //// if (from != null)
{ //// {
if (IsVirtualized) //// if (IsVirtualized)
{ //// {
Panel!.Children.Remove(from); //// Panel!.Children.Remove(from);
generator.Dematerialize(fromIndex, 1); //// generator.Dematerialize(fromIndex, 1);
} //// }
else //// else
{ //// {
from.IsVisible = false; //// from.IsVisible = false;
} //// }
} //// }
} //// }
} ////}
private Control? GetOrCreateContainer(int index) ////private Control? GetOrCreateContainer(int index)
{ ////{
var container = ItemContainerGenerator.ContainerFromIndex(index); //// var container = ItemContainerGenerator.ContainerFromIndex(index);
if (container == null && IsVirtualized) //// if (container == null && IsVirtualized)
{ //// {
var item = Items!.Cast<object>().ElementAt(index); //// var item = Items!.Cast<object>().ElementAt(index);
var materialized = ItemContainerGenerator.Materialize(index, item); //// var materialized = ItemContainerGenerator.Materialize(index, item);
Panel!.Children.Add(materialized.ContainerControl); //// Panel!.Children.Add(materialized.ContainerControl);
container = materialized.ContainerControl; //// container = materialized.ContainerControl;
} //// }
return container; //// return container;
} ////}
/// <summary> /////// <summary>
/// Called when the <see cref="IsVirtualized"/> property changes. /////// Called when the <see cref="IsVirtualized"/> property changes.
/// </summary> /////// </summary>
/// <param name="e">The event args.</param> /////// <param name="e">The event args.</param>
private void IsVirtualizedChanged(AvaloniaPropertyChangedEventArgs e) ////private void IsVirtualizedChanged(AvaloniaPropertyChangedEventArgs e)
{ ////{
if (Panel != null) //// if (Panel != null)
{ //// {
ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); //// ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
} //// }
} ////}
/// <summary> /////// <summary>
/// Called when the <see cref="SelectedIndex"/> property changes. /////// Called when the <see cref="SelectedIndex"/> property changes.
/// </summary> /////// </summary>
/// <param name="e">The event args.</param> /////// <param name="e">The event args.</param>
private async void SelectedIndexChanged(AvaloniaPropertyChangedEventArgs e) ////private async void SelectedIndexChanged(AvaloniaPropertyChangedEventArgs e)
{ ////{
if (Panel != null) //// if (Panel != null)
{ //// {
if (_currentTransition == null) //// if (_currentTransition == null)
{ //// {
int fromIndex = (int)e.OldValue!; //// int fromIndex = (int)e.OldValue!;
int toIndex = (int)e.NewValue!; //// int toIndex = (int)e.NewValue!;
for (;;) //// for (;;)
{ //// {
_currentTransition = MoveToPage(fromIndex, toIndex); //// _currentTransition = MoveToPage(fromIndex, toIndex);
await _currentTransition; //// await _currentTransition;
if (_queuedTransitionIndex != -1) //// if (_queuedTransitionIndex != -1)
{ //// {
fromIndex = toIndex; //// fromIndex = toIndex;
toIndex = _queuedTransitionIndex; //// toIndex = _queuedTransitionIndex;
_queuedTransitionIndex = -1; //// _queuedTransitionIndex = -1;
} //// }
else //// else
{ //// {
_currentTransition = null; //// _currentTransition = null;
break; //// break;
} //// }
} //// }
} //// }
else //// else
{ //// {
_queuedTransitionIndex = (int)e.NewValue!; //// _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);
}
}

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

@ -18,9 +18,9 @@ namespace Avalonia.Controls.Presenters
public interface IItemsPresenterHost public interface IItemsPresenterHost
{ {
/// <summary> /// <summary>
/// Registers an <see cref="IItemsPresenter"/> with a host control. /// Registers an <see cref="ItemsPresenter"/> with a host control.
/// </summary> /// </summary>
/// <param name="presenter">The items presenter.</param> /// <param name="presenter">The items presenter.</param>
void RegisterItemsPresenter(IItemsPresenter presenter); void RegisterItemsPresenter(ItemsPresenter 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 System.Reactive.Linq;
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);
}
}
}

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

@ -1,179 +1,88 @@
using System; using System;
using System.Collections;
using System.Collections.Specialized; using System.Collections.Specialized;
using Avalonia.Controls.Primitives; using System.Diagnostics;
using Avalonia.Input;
using static Avalonia.Utilities.MathUtilities;
namespace Avalonia.Controls.Presenters namespace Avalonia.Controls.Presenters
{ {
/// <summary> /// <summary>
/// Displays items inside an <see cref="ItemsControl"/>. /// Presents items inside an <see cref="Avalonia.Controls.ItemsControl"/>.
/// </summary> /// </summary>
public class ItemsPresenter : ItemsPresenterBase, ILogicalScrollable public class ItemsPresenter : Control
{ {
/// <summary> /// <summary>
/// Defines the <see cref="VirtualizationMode"/> property. /// Defines the <see cref="ItemsPanel"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty = public static readonly StyledProperty<ITemplate<Panel>> ItemsPanelProperty =
AvaloniaProperty.Register<ItemsPresenter, ItemVirtualizationMode>( ItemsControl.ItemsPanelProperty.AddOwner<ItemsPresenter>();
nameof(VirtualizationMode),
defaultValue: ItemVirtualizationMode.None);
private bool _canHorizontallyScroll; private ItemsPresenterContainerGenerator? _generator;
private bool _canVerticallyScroll;
private EventHandler? _scrollInvalidated;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ItemsPresenter"/> class. /// Gets or sets a template which creates the <see cref="Panel"/> used to display the items.
/// </summary> /// </summary>
static ItemsPresenter() public ITemplate<Panel> ItemsPanel
{ {
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue( get => GetValue(ItemsPanelProperty);
typeof(ItemsPresenter), set => SetValue(ItemsPanelProperty, value);
KeyboardNavigationMode.Once);
VirtualizationModeProperty.Changed
.AddClassHandler<ItemsPresenter>((x, e) => x.VirtualizationModeChanged(e));
} }
/// <summary> /// <summary>
/// Gets or sets the virtualization mode for the items. /// Gets the panel used to display the items.
/// </summary> /// </summary>
public ItemVirtualizationMode VirtualizationMode public Panel? Panel { get; private set; }
{
get { return GetValue(VirtualizationModeProperty); }
set { SetValue(VirtualizationModeProperty, value); }
}
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally. /// Gets the owner <see cref="ItemsControl"/>.
/// </summary> /// </summary>
bool ILogicalScrollable.CanHorizontallyScroll internal ItemsControl? ItemsControl { get; private set; }
{
get { return _canHorizontallyScroll; }
set
{
_canHorizontallyScroll = value;
InvalidateMeasure();
}
}
/// <summary> public override sealed void ApplyTemplate()
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
bool ILogicalScrollable.CanVerticallyScroll
{ {
get { return _canVerticallyScroll; } if (Panel is null)
set
{ {
_canVerticallyScroll = value; Panel = ItemsPanel.Build();
InvalidateMeasure(); Panel.SetValue(TemplatedParentProperty, TemplatedParent);
LogicalChildren.Add(Panel);
VisualChildren.Add(Panel);
CreateGeneratorIfSimplePanel();
} }
} }
/// <inheritdoc/>
bool ILogicalScrollable.IsLogicalScrollEnabled
{
get { return Virtualizer?.IsLogicalScrollEnabled ?? false; }
}
/// <inheritdoc/>
Size IScrollable.Extent => Virtualizer?.Extent ?? Size.Empty;
/// <inheritdoc/> protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
Vector IScrollable.Offset
{ {
get { return Virtualizer?.Offset ?? new Vector(); } base.OnPropertyChanged(change);
set
if (change.Property == TemplatedParentProperty)
{ {
if (Virtualizer != null) _generator?.Dispose();
_generator = null;
if (change.NewValue is ItemsControl itemsControl)
{ {
Virtualizer.Offset = CoerceOffset(value); ItemsControl = itemsControl;
((IItemsPresenterHost)itemsControl)?.RegisterItemsPresenter(this);
CreateGeneratorIfSimplePanel();
} }
} }
else if (change.Property == ItemsPanelProperty)
{
_generator?.Dispose();
_generator = null;
LogicalChildren.Clear();
VisualChildren.Clear();
Panel = null;
InvalidateMeasure();
}
} }
/// <inheritdoc/> private void CreateGeneratorIfSimplePanel()
Size IScrollable.Viewport => Virtualizer?.Viewport ?? Bounds.Size;
/// <inheritdoc/>
event EventHandler? ILogicalScrollable.ScrollInvalidated
{
add => _scrollInvalidated += value;
remove => _scrollInvalidated -= value;
}
/// <inheritdoc/>
Size ILogicalScrollable.ScrollSize => new Size(ScrollViewer.DefaultSmallChange, 1);
/// <inheritdoc/>
Size ILogicalScrollable.PageScrollSize => Virtualizer?.Viewport ?? new Size(16, 16);
internal ItemVirtualizer? Virtualizer { get; private set; }
/// <inheritdoc/>
bool ILogicalScrollable.BringIntoView(Control target, Rect targetRect)
{
return false;
}
/// <inheritdoc/>
Control? ILogicalScrollable.GetControlInDirection(NavigationDirection direction, Control? from)
{
return Virtualizer?.GetControlInDirection(direction, from);
}
/// <inheritdoc/>
void ILogicalScrollable.RaiseScrollInvalidated(EventArgs e)
{
_scrollInvalidated?.Invoke(this, e);
}
public override void ScrollIntoView(int index)
{
Virtualizer?.ScrollIntoView(index);
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
return Virtualizer?.MeasureOverride(availableSize) ?? Size.Empty;
}
protected override Size ArrangeOverride(Size finalSize)
{
return Virtualizer?.ArrangeOverride(finalSize) ?? Size.Empty;
}
/// <inheritdoc/>
protected override void PanelCreated(Panel panel)
{
Virtualizer?.Dispose();
Virtualizer = ItemVirtualizer.Create(this);
_scrollInvalidated?.Invoke(this, EventArgs.Empty);
KeyboardNavigation.SetTabNavigation(
(InputElement)panel,
KeyboardNavigation.GetTabNavigation(this));
}
protected override void ItemsChanged(NotifyCollectionChangedEventArgs e)
{
Virtualizer?.ItemsChanged(Items, e);
}
private Vector CoerceOffset(Vector value)
{ {
var scrollable = (ILogicalScrollable)this; if (ItemsControl is null || Panel is null || Panel is IVirtualizingPanel)
var maxX = Math.Max(scrollable.Extent.Width - scrollable.Viewport.Width, 0); return;
var maxY = Math.Max(scrollable.Extent.Height - scrollable.Viewport.Height, 0);
return new Vector(Clamp(value.X, 0, maxX), Clamp(value.Y, 0, maxY));
}
private void VirtualizationModeChanged(AvaloniaPropertyChangedEventArgs e) _generator?.Dispose();
{ _generator = new(this);
Virtualizer?.Dispose();
Virtualizer = ItemVirtualizer.Create(this);
_scrollInvalidated?.Invoke(this, EventArgs.Empty);
} }
} }
} }

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);
}
}
}

108
src/Avalonia.Controls/Presenters/ItemsPresenterContainerGenerator.cs

@ -0,0 +1,108 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Generates containers for <see cref="ItemsPresenter"/>s that have non-virtualizing panels.
/// </summary>
internal class ItemsPresenterContainerGenerator : IDisposable
{
private static NotifyCollectionChangedEventArgs s_Reset = new(NotifyCollectionChangedAction.Reset);
private ItemsPresenter _presenter;
public ItemsPresenterContainerGenerator(ItemsPresenter presenter)
{
Debug.Assert(presenter.ItemsControl is not null);
Debug.Assert(presenter.Panel is not null or IVirtualizingPanel);
_presenter = presenter;
_presenter.ItemsControl.PropertyChanged += OnItemsControlPropertyChanged;
if (_presenter.ItemsControl.Items is INotifyCollectionChanged incc)
incc.CollectionChanged += OnItemsChanged;
OnItemsChanged(null, s_Reset);
}
public void Dispose()
{
_presenter.ItemsControl!.PropertyChanged -= OnItemsControlPropertyChanged;
if (_presenter.ItemsControl.Items is INotifyCollectionChanged incc)
incc.CollectionChanged -= OnItemsChanged;
_presenter.Panel!.Children.Clear();
}
private void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == ItemsControl.ItemsProperty)
{
if (e.OldValue is INotifyCollectionChanged inccOld)
inccOld.CollectionChanged -= OnItemsChanged;
OnItemsChanged(null, s_Reset);
if (e.NewValue is INotifyCollectionChanged inccNew)
inccNew.CollectionChanged += OnItemsChanged;
}
}
private void OnItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (_presenter.ItemsControl?.Items is null || _presenter.Panel is null)
return;
var generator = _presenter.ItemsControl.ItemContainerGenerator;
var panel = _presenter.Panel;
void Add(int index, IEnumerable items)
{
var i = index;
foreach (var item in items)
{
var c = generator.Materialize(i, item);
panel.Children.Insert(i++, c.ContainerControl);
}
}
void Remove(int index, int count)
{
for (var i = 0; i < count; ++i)
panel.Children.RemoveAt(i + index);
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
generator.InsertSpace(e.NewStartingIndex, e.NewItems!.Count);
Add(e.NewStartingIndex, e.NewItems!);
break;
case NotifyCollectionChangedAction.Remove:
generator.RemoveRange(e.OldStartingIndex, e.OldItems!.Count);
Remove(e.OldStartingIndex, e.OldItems!.Count);
break;
case NotifyCollectionChangedAction.Replace:
generator.RemoveRange(e.OldStartingIndex, e.OldItems!.Count);
Remove(e.OldStartingIndex, e.OldItems!.Count);
generator.InsertSpace(e.NewStartingIndex, e.NewItems!.Count);
Add(e.NewStartingIndex, e.NewItems!);
break;
case NotifyCollectionChangedAction.Move:
generator.RemoveRange(e.OldStartingIndex, e.OldItems!.Count);
Remove(e.OldStartingIndex, e.OldItems!.Count);
generator.InsertSpace(e.NewStartingIndex, e.NewItems!.Count);
Add(e.NewStartingIndex, e.NewItems!);
break;
case NotifyCollectionChangedAction.Reset:
generator.Clear();
panel.Children.Clear();
if (_presenter.ItemsControl.Items is { } items)
Add(0, items);
break;
}
}
}
}

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

@ -373,7 +373,7 @@ namespace Avalonia.Controls.Primitives
/// Scrolls the specified item into view. /// Scrolls the specified item into view.
/// </summary> /// </summary>
/// <param name="index">The index of the item.</param> /// <param name="index">The index of the item.</param>
public void ScrollIntoView(int index) => Presenter?.ScrollIntoView(index); public void ScrollIntoView(int index) => throw new NotImplementedException(); ////Presenter?.ScrollIntoView(index);
/// <summary> /// <summary>
/// Scrolls the specified item into view. /// Scrolls the specified item into view.

252
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -1,252 +1,20 @@
using System; using System;
using System.Collections.Specialized;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Layout;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
public class VirtualizingStackPanel : StackPanel, IVirtualizingPanel public class VirtualizingStackPanel : StackPanel
{ {
private Size _availableSpace; /// <summary>
private double _takenSpace; /// Defines the <see cref="VirtualizationMode"/> property.
private int _canBeRemoved; /// </summary>
private double _averageItemSize; public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty =
private int _averageCount; AvaloniaProperty.Register<VirtualizingStackPanel, ItemVirtualizationMode>(
private double _pixelOffset; nameof(VirtualizationMode));
private double _crossAxisOffset;
private bool _forceRemeasure;
bool IVirtualizingPanel.IsFull public ItemVirtualizationMode VirtualizationMode
{ {
get get => GetValue(VirtualizationModeProperty);
{ set => SetValue(VirtualizationModeProperty, value);
return Orientation == Orientation.Horizontal ?
_takenSpace >= _availableSpace.Width :
_takenSpace >= _availableSpace.Height;
}
}
IVirtualizingController? IVirtualizingPanel.Controller { get; set; }
int IVirtualizingPanel.OverflowCount => _canBeRemoved;
Orientation IVirtualizingPanel.ScrollDirection => Orientation;
double IVirtualizingPanel.AverageItemSize => _averageItemSize;
double IVirtualizingPanel.PixelOverflow
{
get
{
var bounds = Orientation == Orientation.Horizontal ?
_availableSpace.Width : _availableSpace.Height;
return Math.Max(0, _takenSpace - bounds);
}
}
double IVirtualizingPanel.PixelOffset
{
get { return _pixelOffset; }
set
{
if (_pixelOffset != value)
{
_pixelOffset = value;
InvalidateArrange();
}
}
}
double IVirtualizingPanel.CrossAxisOffset
{
get { return _crossAxisOffset; }
set
{
if (_crossAxisOffset != value)
{
_crossAxisOffset = value;
InvalidateArrange();
}
}
}
private IVirtualizingController? Controller => ((IVirtualizingPanel)this).Controller;
void IVirtualizingPanel.ForceInvalidateMeasure()
{
InvalidateMeasure();
_forceRemeasure = true;
}
protected override Size MeasureOverride(Size availableSize)
{
if (_forceRemeasure || availableSize != PreviousMeasure)
{
_forceRemeasure = false;
_availableSpace = availableSize;
Controller?.UpdateControls();
}
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
_availableSpace = finalSize;
_canBeRemoved = 0;
_takenSpace = 0;
_averageItemSize = 0;
_averageCount = 0;
var result = base.ArrangeOverride(finalSize);
_takenSpace += _pixelOffset;
Controller?.UpdateControls();
return result;
}
protected override void ChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
base.ChildrenChanged(sender, e);
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (Control control in e.NewItems!)
{
UpdateAdd(control);
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (Control control in e.OldItems!)
{
UpdateRemove(control);
}
break;
}
}
protected override IInputElement? GetControlInDirection(NavigationDirection direction, Control? from)
{
var logicalScrollable = Parent as ILogicalScrollable;
if (logicalScrollable?.IsLogicalScrollEnabled == true)
{
return logicalScrollable.GetControlInDirection(direction, from);
}
else
{
return base.GetControlInDirection(direction, from);
}
}
internal override void ArrangeChild(
Control child,
Rect rect,
Size panelSize,
Orientation orientation)
{
if (orientation == Orientation.Vertical)
{
rect = new Rect(
rect.X - _crossAxisOffset,
rect.Y - _pixelOffset,
rect.Width,
rect.Height);
child.Arrange(rect);
if (rect.Y >= _availableSpace.Height)
{
++_canBeRemoved;
}
if (rect.Bottom >= _takenSpace)
{
_takenSpace = rect.Bottom;
}
AddToAverageItemSize(rect.Height);
}
else
{
rect = new Rect(
rect.X - _pixelOffset,
rect.Y - _crossAxisOffset,
rect.Width,
rect.Height);
child.Arrange(rect);
if (rect.X >= _availableSpace.Width)
{
++_canBeRemoved;
}
if (rect.Right >= _takenSpace)
{
_takenSpace = rect.Right;
}
AddToAverageItemSize(rect.Width);
}
}
private void UpdateAdd(Control child)
{
var bounds = Bounds;
var spacing = Spacing;
child.Measure(_availableSpace);
++_averageCount;
if (Orientation == Orientation.Vertical)
{
var height = child.DesiredSize.Height;
_takenSpace += height + spacing;
AddToAverageItemSize(height);
}
else
{
var width = child.DesiredSize.Width;
_takenSpace += width + spacing;
AddToAverageItemSize(width);
}
}
private void UpdateRemove(Control child)
{
var bounds = Bounds;
var spacing = Spacing;
if (Orientation == Orientation.Vertical)
{
var height = child.DesiredSize.Height;
_takenSpace -= height + spacing;
RemoveFromAverageItemSize(height);
}
else
{
var width = child.DesiredSize.Width;
_takenSpace -= width + spacing;
RemoveFromAverageItemSize(width);
}
if (_canBeRemoved > 0)
{
--_canBeRemoved;
}
}
private void AddToAverageItemSize(double value)
{
++_averageCount;
_averageItemSize += (value - _averageItemSize) / _averageCount;
}
private void RemoveFromAverageItemSize(double value)
{
_averageItemSize = ((_averageItemSize * _averageCount) - value) / (_averageCount - 1);
--_averageCount;
} }
} }
} }

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

@ -8,7 +8,6 @@
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"> CornerRadius="{TemplateBinding CornerRadius}">
<CarouselPresenter IsVirtualized="{TemplateBinding IsVirtualized}" <CarouselPresenter IsVirtualized="{TemplateBinding IsVirtualized}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}" ItemsPanel="{TemplateBinding ItemsPanel}"
Margin="{TemplateBinding Padding}" Margin="{TemplateBinding Padding}"
SelectedIndex="{TemplateBinding SelectedIndex}" SelectedIndex="{TemplateBinding SelectedIndex}"

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

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

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

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

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

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

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

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

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

@ -273,11 +273,8 @@
<Border Name="border" BoxShadow="inset -6 0 3 -3 #20000000" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <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)}"> <ScrollViewer Name="PART_ScrollViewer" HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}" VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
<ItemsPresenter Name="PART_ItemsPresenter" <ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}" ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}" Margin="{TemplateBinding Padding}"/>
Margin="{TemplateBinding Padding}"
VirtualizationMode="{TemplateBinding VirtualizationMode}"/>
</ScrollViewer> </ScrollViewer>
</Border> </Border>
</ControlTemplate> </ControlTemplate>

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -95,7 +95,6 @@
</Border> </Border>
<ItemsPresenter Name="PART_ItemsPresenter" <ItemsPresenter Name="PART_ItemsPresenter"
IsVisible="{TemplateBinding IsExpanded}" IsVisible="{TemplateBinding IsExpanded}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}" /> ItemsPanel="{TemplateBinding ItemsPanel}" />
</StackPanel> </StackPanel>
</ControlTemplate> </ControlTemplate>

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

@ -10,7 +10,6 @@
CornerRadius="{TemplateBinding CornerRadius}"> CornerRadius="{TemplateBinding CornerRadius}">
<CarouselPresenter Margin="{TemplateBinding Padding}" <CarouselPresenter Margin="{TemplateBinding Padding}"
IsVirtualized="{TemplateBinding IsVirtualized}" IsVirtualized="{TemplateBinding IsVirtualized}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}" ItemsPanel="{TemplateBinding ItemsPanel}"
PageTransition="{TemplateBinding PageTransition}" PageTransition="{TemplateBinding PageTransition}"
SelectedIndex="{TemplateBinding SelectedIndex}" /> SelectedIndex="{TemplateBinding SelectedIndex}" />

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -63,7 +63,6 @@
</Border> </Border>
<ItemsPresenter Name="PART_ItemsPresenter" <ItemsPresenter Name="PART_ItemsPresenter"
IsVisible="{TemplateBinding IsExpanded}" IsVisible="{TemplateBinding IsExpanded}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}" /> ItemsPanel="{TemplateBinding ItemsPanel}" />
</StackPanel> </StackPanel>
</ControlTemplate> </ControlTemplate>

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

@ -300,7 +300,6 @@ namespace Avalonia.Controls.UnitTests
{ {
Name = "PART_ItemsPresenter", Name = "PART_ItemsPresenter",
[~CarouselPresenter.IsVirtualizedProperty] = control[~Carousel.IsVirtualizedProperty], [~CarouselPresenter.IsVirtualizedProperty] = control[~Carousel.IsVirtualizedProperty],
[~CarouselPresenter.ItemsProperty] = control[~Carousel.ItemsProperty],
[~CarouselPresenter.ItemsPanelProperty] = control[~Carousel.ItemsPanelProperty], [~CarouselPresenter.ItemsPanelProperty] = control[~Carousel.ItemsPanelProperty],
[~CarouselPresenter.SelectedIndexProperty] = control[~Carousel.SelectedIndexProperty], [~CarouselPresenter.SelectedIndexProperty] = control[~Carousel.SelectedIndexProperty],
[~CarouselPresenter.PageTransitionProperty] = control[~Carousel.PageTransitionProperty], [~CarouselPresenter.PageTransitionProperty] = control[~Carousel.PageTransitionProperty],

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

@ -204,7 +204,6 @@ namespace Avalonia.Controls.UnitTests
Child = new ItemsPresenter Child = new ItemsPresenter
{ {
Name = "PART_ItemsPresenter", Name = "PART_ItemsPresenter",
[!ItemsPresenter.ItemsProperty] = parent[!ComboBox.ItemsProperty],
}.RegisterInNameScope(scope) }.RegisterInNameScope(scope)
}.RegisterInNameScope(scope) }.RegisterInNameScope(scope)
} }

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

@ -544,7 +544,6 @@ namespace Avalonia.Controls.UnitTests
var presenter = new ItemsPresenter var presenter = new ItemsPresenter
{ {
[StyledElement.TemplatedParentProperty] = target, [StyledElement.TemplatedParentProperty] = target,
[~ItemsPresenter.ItemsProperty] = target[~ItemsControl.ItemsProperty],
}; };
presenter.ApplyTemplate(); presenter.ApplyTemplate();
@ -777,7 +776,6 @@ namespace Avalonia.Controls.UnitTests
Child = new ItemsPresenter Child = new ItemsPresenter
{ {
Name = "PART_ItemsPresenter", Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
}.RegisterInNameScope(scope) }.RegisterInNameScope(scope)
}; };
}); });
@ -785,7 +783,7 @@ namespace Avalonia.Controls.UnitTests
private class TestItemsControl : ItemsControl private class TestItemsControl : ItemsControl
{ {
public new IItemsPresenter Presenter public new ItemsPresenter Presenter
{ {
get { return base.Presenter; } get { return base.Presenter; }
set { base.Presenter = value; } set { base.Presenter = value; }

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

@ -1,3 +1,4 @@
using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
@ -330,38 +331,39 @@ namespace Avalonia.Controls.UnitTests
[Fact] [Fact]
public void ListBox_After_Scroll_IndexOutOfRangeException_Shouldnt_Be_Thrown() public void ListBox_After_Scroll_IndexOutOfRangeException_Shouldnt_Be_Thrown()
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) throw new NotImplementedException();
{ ////using (UnitTestApplication.Start(TestServices.StyledWindow))
var items = Enumerable.Range(0, 11).Select(x => $"{x}").ToArray(); ////{
//// var items = Enumerable.Range(0, 11).Select(x => $"{x}").ToArray();
var target = new ListBox //// var target = new ListBox
{ //// {
Template = ListBoxTemplate(), //// Template = ListBoxTemplate(),
Items = items, //// Items = items,
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 11 }) //// ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 11 })
}; //// };
Prepare(target); //// Prepare(target);
var panel = target.Presenter.Panel as IVirtualizingPanel; //// var panel = target.Presenter.Panel as IVirtualizingPanel;
var listBoxItems = panel.Children.OfType<ListBoxItem>(); //// var listBoxItems = panel.Children.OfType<ListBoxItem>();
//virtualization should have created exactly 10 items //// //virtualization should have created exactly 10 items
Assert.Equal(10, listBoxItems.Count()); //// Assert.Equal(10, listBoxItems.Count());
Assert.Equal("0", listBoxItems.First().DataContext); //// Assert.Equal("0", listBoxItems.First().DataContext);
Assert.Equal("9", listBoxItems.Last().DataContext); //// Assert.Equal("9", listBoxItems.Last().DataContext);
//instead pixeloffset > 0 there could be pretty complex sequence for repro //// //instead pixeloffset > 0 there could be pretty complex sequence for repro
//it involves add/remove/scroll to end multiple actions //// //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 //// //which i can't find so far :(, but this is the simplest way to add it to unit test
panel.PixelOffset = 1; //// panel.PixelOffset = 1;
//here scroll to end -> IndexOutOfRangeException is thrown //// //here scroll to end -> IndexOutOfRangeException is thrown
target.Scroll.Offset = new Vector(0, 2); //// target.Scroll.Offset = new Vector(0, 2);
Assert.True(true); //// Assert.True(true);
} ////}
} }
[Fact] [Fact]
@ -600,9 +602,7 @@ namespace Avalonia.Controls.UnitTests
Content = new ItemsPresenter Content = new ItemsPresenter
{ {
Name = "PART_ItemsPresenter", Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(),
[~ItemsPresenter.ItemsPanelProperty] = parent.GetObservable(ItemsControl.ItemsPanelProperty).ToBinding(), [~ItemsPresenter.ItemsPanelProperty] = parent.GetObservable(ItemsControl.ItemsPanelProperty).ToBinding(),
[~ItemsPresenter.VirtualizationModeProperty] = parent.GetObservable(ListBox.VirtualizationModeProperty).ToBinding(),
}.RegisterInNameScope(scope) }.RegisterInNameScope(scope)
}.RegisterInNameScope(scope)); }.RegisterInNameScope(scope));
} }

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

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

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

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

1464
tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs

File diff suppressed because it is too large

370
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs

@ -1,14 +1,15 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using Moq;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Input; using Avalonia.UnitTests;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Xunit; using Xunit;
#nullable enable
namespace Avalonia.Controls.UnitTests.Presenters namespace Avalonia.Controls.UnitTests.Presenters
{ {
public class ItemsPresenterTests public class ItemsPresenterTests
@ -27,327 +28,126 @@ namespace Avalonia.Controls.UnitTests.Presenters
} }
[Fact] [Fact]
public void Should_Add_Containers() public void Panel_Should_Be_Visual_Child()
{
var target = new ItemsPresenter
{
Items = new[] { "foo", "bar" },
};
target.ApplyTemplate();
Assert.Equal(2, target.Panel.Children.Count);
Assert.IsType<ContentPresenter>(target.Panel.Children[0]);
Assert.IsType<ContentPresenter>(target.Panel.Children[1]);
Assert.Equal("foo", ((ContentPresenter)target.Panel.Children[0]).Content);
Assert.Equal("bar", ((ContentPresenter)target.Panel.Children[1]).Content);
}
[Fact]
public void Should_Add_Containers_Of_Correct_Type()
{
var target = new ItemsPresenter
{
Items = new[] { "foo", "bar" },
};
target.ItemContainerGenerator = new ItemContainerGenerator<ListBoxItem>(
target,
ListBoxItem.ContentProperty,
null);
target.ApplyTemplate();
Assert.Equal(2, target.Panel.Children.Count);
Assert.IsType<ListBoxItem>(target.Panel.Children[0]);
Assert.IsType<ListBoxItem>(target.Panel.Children[1]);
}
[Fact]
public void Should_Create_Containers_Only_Once()
{
var parent = new TestItemsControl();
var target = new ItemsPresenter
{
Items = new[] { "foo", "bar" },
[StyledElement.TemplatedParentProperty] = parent,
};
var raised = 0;
parent.ItemContainerGenerator.Materialized += (s, e) => ++raised;
target.ApplyTemplate();
Assert.Equal(2, target.Panel.Children.Count);
Assert.Equal(2, raised);
}
[Fact]
public void ItemContainerGenerator_Should_Be_Picked_Up_From_TemplatedControl()
{
var parent = new TestItemsControl();
var target = new ItemsPresenter
{
[StyledElement.TemplatedParentProperty] = parent,
};
Assert.IsType<ItemContainerGenerator<TestItem>>(target.ItemContainerGenerator);
}
[Fact]
public void Should_Remove_Containers()
{ {
var items = new AvaloniaList<string>(new[] { "foo", "bar" }); var (target, _, _) = CreateTarget();
var target = new ItemsPresenter var child = target.GetVisualChildren().Single();
{
Items = items,
};
target.ApplyTemplate();
items.RemoveAt(0);
Assert.Single(target.Panel.Children); Assert.Equal(target.Panel, child);
Assert.Equal("bar", ((ContentPresenter)target.Panel.Children[0]).Content);
Assert.Equal("bar", ((ContentPresenter)target.ItemContainerGenerator.ContainerFromIndex(0)).Content);
} }
[Fact] public class NonVirtualizingPanel
public void Clearing_Items_Should_Remove_Containers()
{ {
var items = new ObservableCollection<string> { "foo", "bar" }; [Fact]
var target = new ItemsPresenter public void Creates_Containers_For_Initial_Items()
{ {
Items = items, using var app = Start();
}; var items = new[] { "foo", "bar", "baz" };
var (target, _, _) = CreateTarget(items);
var panel = Assert.IsType<StackPanel>(target.Panel);
target.ApplyTemplate(); AssertContainers(panel, items);
items.Clear(); }
Assert.Empty(target.Panel.Children);
Assert.Empty(target.ItemContainerGenerator.Containers);
}
[Fact] [Fact]
public void Replacing_Items_Should_Update_Containers() public void Creates_Containers_For_Inserted_Items()
{
var items = new ObservableCollection<string> { "foo", "bar", "baz" };
var target = new ItemsPresenter
{ {
Items = items, using var app = Start();
}; var items = new ObservableCollection<string>(new[] { "foo", "bar", "baz" });
var (target, _, root) = CreateTarget(items);
var panel = Assert.IsType<StackPanel>(target.Panel);
target.ApplyTemplate(); items.Insert(1, "foo2");
items[1] = "baz"; root.LayoutManager.ExecuteLayoutPass();
var text = target.Panel.Children AssertContainers(panel, items);
.OfType<ContentPresenter>() }
.Select(x => x.Content)
.ToList();
Assert.Equal(new[] { "foo", "baz", "baz" }, text);
}
[Fact] [Fact]
public void Moving_Items_Should_Update_Containers() public void Removes_Containers_For_Removed_Items()
{
var items = new ObservableCollection<string> { "foo", "bar", "baz" };
var target = new ItemsPresenter
{ {
Items = items, using var app = Start();
}; var items = new ObservableCollection<string>(new[] { "foo", "bar", "baz" });
var (target, _, root) = CreateTarget(items);
var panel = Assert.IsType<StackPanel>(target.Panel);
target.ApplyTemplate(); items.RemoveAt(1);
items.Move(2, 1); root.LayoutManager.ExecuteLayoutPass();
var text = target.Panel.Children AssertContainers(panel, items);
.OfType<ContentPresenter>() }
.Select(x => x.Content)
.ToList();
Assert.Equal(new[] { "foo", "baz", "bar" }, text);
}
[Fact] [Fact]
public void Inserting_Items_Should_Update_Containers() public void Updates_Containers_For_Moved_Items()
{
var items = new ObservableCollection<string> { "foo", "bar", "baz" };
var target = new ItemsPresenter
{ {
Items = items, using var app = Start();
}; var items = new ObservableCollection<string>(new[] { "foo", "bar", "baz" });
var (target, _, root) = CreateTarget(items);
target.ApplyTemplate(); var panel = Assert.IsType<StackPanel>(target.Panel);
items.Insert(2, "insert");
var text = target.Panel.Children
.OfType<ContentPresenter>()
.Select(x => x.Content)
.ToList();
Assert.Equal(new[] { "foo", "bar", "insert", "baz" }, text);
}
[Fact] items.Move(0, 2);
public void Setting_Items_To_Null_Should_Remove_Containers() root.LayoutManager.ExecuteLayoutPass();
{
var target = new ItemsPresenter
{
Items = new[] { "foo", "bar" },
};
target.ApplyTemplate(); AssertContainers(panel, items);
target.Items = null; }
Assert.Empty(target.Panel.Children);
Assert.Empty(target.ItemContainerGenerator.Containers);
}
[Fact]
public void Should_Handle_Null_Items()
{
var items = new AvaloniaList<string>(new[] { "foo", null, "bar" });
var target = new ItemsPresenter [Fact]
public void Updates_Containers_For_Replaced_Items()
{ {
Items = items, using var app = Start();
}; var items = new ObservableCollection<string>(new[] { "foo", "bar", "baz" });
var (target, _, root) = CreateTarget(items);
target.ApplyTemplate(); var panel = Assert.IsType<StackPanel>(target.Panel);
var text = target.Panel.Children.Cast<ContentPresenter>().Select(x => x.Content).ToList(); items[1] = "bar2";
root.LayoutManager.ExecuteLayoutPass();
Assert.Equal(new[] { "foo", null, "bar" }, text); AssertContainers(panel, items);
Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(0)); }
Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(1));
Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(2));
items.RemoveAt(1);
text = target.Panel.Children.Cast<ContentPresenter>().Select(x => x.Content).ToList();
Assert.Equal(new[] { "foo", "bar" }, text);
Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(0));
Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(1));
}
[Fact] [Fact]
public void Inserting_Then_Removing_Should_Add_Remove_Containers() public void Updates_Containers_On_Items_Changed()
{
var items = new AvaloniaList<string>(Enumerable.Range(0, 5).Select(x => $"Item {x}"));
var toAdd = Enumerable.Range(0, 3).Select(x => $"Added Item {x}").ToArray();
var target = new ItemsPresenter
{ {
VirtualizationMode = ItemVirtualizationMode.None, using var app = Start();
Items = items, var items = new ObservableCollection<string>(new[] { "foo", "bar", "baz" });
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 }), var (target, itemsControl, root) = CreateTarget(items);
}; var panel = Assert.IsType<StackPanel>(target.Panel);
target.ApplyTemplate(); var newItems = new[] { "qux", "quux", "corge" };
itemsControl.Items = newItems;
root.LayoutManager.ExecuteLayoutPass();
Assert.Equal(items.Count, target.Panel.Children.Count); AssertContainers(panel, newItems);
foreach (var item in toAdd)
{
items.Insert(1, item);
} }
Assert.Equal(items.Count, target.Panel.Children.Count); private static void AssertContainers(StackPanel panel, IReadOnlyList<string> items)
foreach (var item in toAdd)
{ {
items.Remove(item); Assert.Equal(items.Count, panel.Children.Count);
}
Assert.Equal(items.Count, target.Panel.Children.Count); for (var i = 0; i < items.Count; i++)
{
var container = Assert.IsType<ContentPresenter>(panel.Children[i]);
Assert.Equal(items[i], container.DataContext);
Assert.Equal(items[i], container.Content);
}
}
} }
[Fact] private static (ItemsPresenter, ItemsControl, TestRoot) CreateTarget(IReadOnlyList<string>? items = null)
public void Should_Handle_Duplicate_Items()
{ {
var items = new AvaloniaList<int>(new[] { 1, 2, 1 }); var result = new ItemsPresenter();
var target = new ItemsPresenter var itemsControl = new ItemsControl
{ {
Items = items, Items = items,
Template = new FuncControlTemplate<ItemsControl>((_, _) => result)
}; };
target.ApplyTemplate(); var root = new TestRoot(itemsControl);
items.RemoveAt(2); root.LayoutManager.ExecuteInitialLayoutPass();
return (result, itemsControl, root);
var numbers = target.Panel.Children
.OfType<ContentPresenter>()
.Select(x => x.Content)
.Cast<int>();
Assert.Equal(new[] { 1, 2 }, numbers);
}
[Fact]
public void Panel_Should_Be_Created_From_ItemsPanel_Template()
{
var panel = new Panel();
var target = new ItemsPresenter
{
ItemsPanel = new FuncTemplate<Panel>(() => panel),
};
target.ApplyTemplate();
Assert.Same(panel, target.Panel);
Assert.Same(target, target.Panel.Parent);
} }
[Fact] private static IDisposable Start() => UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
public void Panel_TabNavigation_Should_Be_Set_To_Once()
{
var target = new ItemsPresenter();
target.ApplyTemplate();
Assert.Equal(KeyboardNavigationMode.Once, KeyboardNavigation.GetTabNavigation((InputElement)target.Panel));
}
[Fact]
public void Panel_TabNavigation_Should_Be_Set_To_ItemsPresenter_Value()
{
var target = new ItemsPresenter();
KeyboardNavigation.SetTabNavigation(target, KeyboardNavigationMode.Cycle);
target.ApplyTemplate();
Assert.Equal(KeyboardNavigationMode.Cycle, KeyboardNavigation.GetTabNavigation((InputElement)target.Panel));
}
[Fact]
public void Panel_Should_Be_Visual_Child()
{
var target = new ItemsPresenter();
target.ApplyTemplate();
var child = target.GetVisualChildren().Single();
Assert.Equal(target.Panel, child);
}
private class Item
{
public Item(string value)
{
Value = value;
}
public string Value { get; }
}
private class TestItem : ContentControl
{
}
private class TestItemsControl : ItemsControl
{
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<TestItem>(this, TestItem.ContentProperty, null);
}
}
} }
} }

748
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs

@ -1,374 +1,374 @@
using System; ////using System;
using System.Collections.Generic; ////using System.Collections.Generic;
using System.Linq; ////using System.Linq;
using Avalonia.Controls.Generators; ////using Avalonia.Controls.Generators;
using Avalonia.Controls.Presenters; ////using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; ////using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; ////using Avalonia.Controls.Templates;
using Avalonia.Layout; ////using Avalonia.Layout;
using Avalonia.Platform; ////using Avalonia.Platform;
using Avalonia.Rendering; ////using Avalonia.Rendering;
using Avalonia.UnitTests; ////using Avalonia.UnitTests;
using Avalonia.VisualTree; ////using Avalonia.VisualTree;
using Xunit; ////using Xunit;
namespace Avalonia.Controls.UnitTests.Presenters ////namespace Avalonia.Controls.UnitTests.Presenters
{ ////{
public class ItemsPresenterTests_Virtualization //// public class ItemsPresenterTests_Virtualization
{ //// {
[Fact] //// [Fact]
public void Should_Not_Create_Items_Before_Added_To_Visual_Tree() //// public void Should_Not_Create_Items_Before_Added_To_Visual_Tree()
{ //// {
var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList(); //// var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList();
var target = new TestItemsPresenter(true) //// var target = new TestItemsPresenter(true)
{ //// {
Items = items, //// Items = items,
ItemsPanel = VirtualizingPanelTemplate(Orientation.Vertical), //// ItemsPanel = VirtualizingPanelTemplate(Orientation.Vertical),
ItemTemplate = ItemTemplate(), //// ItemTemplate = ItemTemplate(),
VirtualizationMode = ItemVirtualizationMode.Simple, //// VirtualizationMode = ItemVirtualizationMode.Simple,
}; //// };
var scroller = new ScrollContentPresenter //// var scroller = new ScrollContentPresenter
{ //// {
Content = target, //// Content = target,
}; //// };
scroller.UpdateChild(); //// scroller.UpdateChild();
target.ApplyTemplate(); //// target.ApplyTemplate();
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100)); //// target.Arrange(new Rect(0, 0, 100, 100));
Assert.Empty(target.Panel.Children); //// Assert.Empty(target.Panel.Children);
var root = new TestRoot //// var root = new TestRoot
{ //// {
Child = scroller, //// Child = scroller,
}; //// };
target.InvalidateMeasure(); //// target.InvalidateMeasure();
target.Panel.InvalidateMeasure(); //// target.Panel.InvalidateMeasure();
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100)); //// target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(10, target.Panel.Children.Count); //// Assert.Equal(10, target.Panel.Children.Count);
} //// }
[Fact] //// [Fact]
public void Should_Return_IsLogicalScrollEnabled_False_When_Has_No_Virtualizing_Panel() //// public void Should_Return_IsLogicalScrollEnabled_False_When_Has_No_Virtualizing_Panel()
{ //// {
var target = CreateTarget(); //// var target = CreateTarget();
target.ClearValue(ItemsPresenter.ItemsPanelProperty); //// target.ClearValue(ItemsPresenter.ItemsPanelProperty);
target.ApplyTemplate(); //// target.ApplyTemplate();
Assert.False(((ILogicalScrollable)target).IsLogicalScrollEnabled); //// Assert.False(((ILogicalScrollable)target).IsLogicalScrollEnabled);
} //// }
[Fact] //// [Fact]
public void Should_Return_IsLogicalScrollEnabled_False_When_VirtualizationMode_None() //// public void Should_Return_IsLogicalScrollEnabled_False_When_VirtualizationMode_None()
{ //// {
var target = CreateTarget(ItemVirtualizationMode.None); //// var target = CreateTarget(ItemVirtualizationMode.None);
target.ApplyTemplate(); //// target.ApplyTemplate();
Assert.False(((ILogicalScrollable)target).IsLogicalScrollEnabled); //// Assert.False(((ILogicalScrollable)target).IsLogicalScrollEnabled);
} //// }
[Fact] //// [Fact]
public void Should_Return_IsLogicalScrollEnabled_False_When_Doesnt_Have_ScrollPresenter_Parent() //// public void Should_Return_IsLogicalScrollEnabled_False_When_Doesnt_Have_ScrollPresenter_Parent()
{ //// {
var target = new ItemsPresenter //// var target = new ItemsPresenter
{ //// {
ItemsPanel = VirtualizingPanelTemplate(), //// ItemsPanel = VirtualizingPanelTemplate(),
ItemTemplate = ItemTemplate(), //// ItemTemplate = ItemTemplate(),
VirtualizationMode = ItemVirtualizationMode.Simple, //// VirtualizationMode = ItemVirtualizationMode.Simple,
}; //// };
target.ApplyTemplate(); //// target.ApplyTemplate();
Assert.False(((ILogicalScrollable)target).IsLogicalScrollEnabled); //// Assert.False(((ILogicalScrollable)target).IsLogicalScrollEnabled);
} //// }
[Fact] //// [Fact]
public void Should_Return_IsLogicalScrollEnabled_True() //// public void Should_Return_IsLogicalScrollEnabled_True()
{ //// {
var target = CreateTarget(); //// var target = CreateTarget();
target.ApplyTemplate(); //// target.ApplyTemplate();
Assert.True(((ILogicalScrollable)target).IsLogicalScrollEnabled); //// Assert.True(((ILogicalScrollable)target).IsLogicalScrollEnabled);
} //// }
[Fact] //// [Fact]
public void Parent_ScrollContentPresenter_Properties_Should_Be_Set() //// public void Parent_ScrollContentPresenter_Properties_Should_Be_Set()
{ //// {
var target = CreateTarget(); //// var target = CreateTarget();
target.ApplyTemplate(); //// target.ApplyTemplate();
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100)); //// target.Arrange(new Rect(0, 0, 100, 100));
var scroll = (ScrollContentPresenter)target.Parent; //// var scroll = (ScrollContentPresenter)target.Parent;
Assert.Equal(new Size(10, 20), scroll.Extent); //// Assert.Equal(new Size(10, 20), scroll.Extent);
Assert.Equal(new Size(100, 10), scroll.Viewport); //// Assert.Equal(new Size(100, 10), scroll.Viewport);
} //// }
[Fact] //// [Fact]
public void Should_Fill_Panel_With_Containers() //// public void Should_Fill_Panel_With_Containers()
{ //// {
var target = CreateTarget(); //// var target = CreateTarget();
target.ApplyTemplate(); //// target.ApplyTemplate();
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
Assert.Equal(10, target.Panel.Children.Count); //// Assert.Equal(10, target.Panel.Children.Count);
target.Arrange(new Rect(0, 0, 100, 100)); //// target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(10, target.Panel.Children.Count); //// Assert.Equal(10, target.Panel.Children.Count);
} //// }
[Fact] //// [Fact]
public void Should_Only_Create_Enough_Containers_To_Display_All_Items() //// public void Should_Only_Create_Enough_Containers_To_Display_All_Items()
{ //// {
var target = CreateTarget(itemCount: 2); //// var target = CreateTarget(itemCount: 2);
target.ApplyTemplate(); //// target.ApplyTemplate();
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100)); //// target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(2, target.Panel.Children.Count); //// Assert.Equal(2, target.Panel.Children.Count);
} //// }
[Fact] //// [Fact]
public void Should_Expand_To_Fit_Containers_When_Flexible_Size() //// public void Should_Expand_To_Fit_Containers_When_Flexible_Size()
{ //// {
var target = CreateTarget(); //// var target = CreateTarget();
target.ApplyTemplate(); //// target.ApplyTemplate();
target.Measure(Size.Infinity); //// target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize)); //// target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(new Size(10, 200), target.DesiredSize); //// Assert.Equal(new Size(10, 200), target.DesiredSize);
Assert.Equal(new Size(10, 200), target.Bounds.Size); //// Assert.Equal(new Size(10, 200), target.Bounds.Size);
Assert.Equal(20, target.Panel.Children.Count); //// Assert.Equal(20, target.Panel.Children.Count);
} //// }
[Fact] //// [Fact]
public void Initial_Item_DataContexts_Should_Be_Correct() //// public void Initial_Item_DataContexts_Should_Be_Correct()
{ //// {
var target = CreateTarget(); //// var target = CreateTarget();
var items = (IList<string>)target.Items; //// var items = (IList<string>)target.Items;
target.ApplyTemplate(); //// target.ApplyTemplate();
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100)); //// target.Arrange(new Rect(0, 0, 100, 100));
for (var i = 0; i < target.Panel.Children.Count; ++i) //// for (var i = 0; i < target.Panel.Children.Count; ++i)
{ //// {
Assert.Equal(items[i], target.Panel.Children[i].DataContext); //// Assert.Equal(items[i], target.Panel.Children[i].DataContext);
} //// }
} //// }
[Fact] //// [Fact]
public void Should_Add_New_Items_When_Control_Is_Enlarged() //// public void Should_Add_New_Items_When_Control_Is_Enlarged()
{ //// {
var target = CreateTarget(); //// var target = CreateTarget();
var items = (IList<string>)target.Items; //// var items = (IList<string>)target.Items;
target.ApplyTemplate(); //// target.ApplyTemplate();
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100)); //// target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(10, target.Panel.Children.Count); //// Assert.Equal(10, target.Panel.Children.Count);
target.Measure(new Size(120, 120)); //// target.Measure(new Size(120, 120));
target.Arrange(new Rect(0, 0, 100, 120)); //// target.Arrange(new Rect(0, 0, 100, 120));
Assert.Equal(12, target.Panel.Children.Count); //// Assert.Equal(12, target.Panel.Children.Count);
for (var i = 0; i < target.Panel.Children.Count; ++i) //// for (var i = 0; i < target.Panel.Children.Count; ++i)
{ //// {
Assert.Equal(items[i], target.Panel.Children[i].DataContext); //// Assert.Equal(items[i], target.Panel.Children[i].DataContext);
} //// }
} //// }
[Fact] //// [Fact]
public void Should_Not_Create_Virtualizer_Before_Panel() //// public void Should_Not_Create_Virtualizer_Before_Panel()
{ //// {
var target = CreateTarget(); //// var target = CreateTarget();
Assert.Null(target.Panel); //// Assert.Null(target.Panel);
Assert.Null(target.Virtualizer); //// Assert.Null(target.Virtualizer);
} //// }
[Fact] //// [Fact]
public void Changing_VirtualizationMode_None_To_Simple_Should_Update_Control() //// public void Changing_VirtualizationMode_None_To_Simple_Should_Update_Control()
{ //// {
var target = CreateTarget(mode: ItemVirtualizationMode.None); //// var target = CreateTarget(mode: ItemVirtualizationMode.None);
var scroll = (ScrollContentPresenter)target.Parent; //// var scroll = (ScrollContentPresenter)target.Parent;
scroll.Measure(new Size(100, 100)); //// scroll.Measure(new Size(100, 100));
scroll.Arrange(new Rect(0, 0, 100, 100)); //// scroll.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(20, target.Panel.Children.Count); //// Assert.Equal(20, target.Panel.Children.Count);
Assert.Equal(new Size(100, 200), scroll.Extent); //// Assert.Equal(new Size(100, 200), scroll.Extent);
Assert.Equal(new Size(100, 100), scroll.Viewport); //// Assert.Equal(new Size(100, 100), scroll.Viewport);
target.VirtualizationMode = ItemVirtualizationMode.Simple; //// target.VirtualizationMode = ItemVirtualizationMode.Simple;
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100)); //// target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(10, target.Panel.Children.Count); //// Assert.Equal(10, target.Panel.Children.Count);
Assert.Equal(new Size(10, 20), scroll.Extent); //// Assert.Equal(new Size(10, 20), scroll.Extent);
Assert.Equal(new Size(100, 10), scroll.Viewport); //// Assert.Equal(new Size(100, 10), scroll.Viewport);
} //// }
[Fact] //// [Fact]
public void Changing_VirtualizationMode_None_To_Simple_Should_Add_Correct_Number_Of_Controls() //// public void Changing_VirtualizationMode_None_To_Simple_Should_Add_Correct_Number_Of_Controls()
{ //// {
using (UnitTestApplication.Start(new TestServices())) //// using (UnitTestApplication.Start(new TestServices()))
{ //// {
var target = CreateTarget(mode: ItemVirtualizationMode.None); //// var target = CreateTarget(mode: ItemVirtualizationMode.None);
var scroll = (TestScroller)target.Parent; //// var scroll = (TestScroller)target.Parent;
scroll.Width = scroll.Height = 100; //// scroll.Width = scroll.Height = 100;
scroll.LayoutManager.ExecuteInitialLayoutPass(); //// scroll.LayoutManager.ExecuteInitialLayoutPass();
// Ensure than an intermediate measure pass doesn't add more controls than it //// // Ensure than an intermediate measure pass doesn't add more controls than it
// should. This can happen if target gets measured with Size.Infinity which //// // should. This can happen if target gets measured with Size.Infinity which
// is what the available size should be when VirtualizationMode == None but not //// // is what the available size should be when VirtualizationMode == None but not
// what it should after VirtualizationMode is changed to Simple. //// // what it should after VirtualizationMode is changed to Simple.
target.Panel.Children.CollectionChanged += (s, e) => //// target.Panel.Children.CollectionChanged += (s, e) =>
{ //// {
Assert.InRange(target.Panel.Children.Count, 0, 10); //// Assert.InRange(target.Panel.Children.Count, 0, 10);
}; //// };
target.VirtualizationMode = ItemVirtualizationMode.Simple; //// target.VirtualizationMode = ItemVirtualizationMode.Simple;
((ILayoutRoot)scroll.GetVisualRoot()).LayoutManager.ExecuteLayoutPass(); //// ((ILayoutRoot)scroll.GetVisualRoot()).LayoutManager.ExecuteLayoutPass();
Assert.Equal(10, target.Panel.Children.Count); //// Assert.Equal(10, target.Panel.Children.Count);
} //// }
} //// }
[Fact] //// [Fact]
public void Changing_VirtualizationMode_Simple_To_None_Should_Update_Control() //// public void Changing_VirtualizationMode_Simple_To_None_Should_Update_Control()
{ //// {
var target = CreateTarget(); //// var target = CreateTarget();
var scroll = (ScrollContentPresenter)target.Parent; //// var scroll = (ScrollContentPresenter)target.Parent;
scroll.Measure(new Size(100, 100)); //// scroll.Measure(new Size(100, 100));
scroll.Arrange(new Rect(0, 0, 100, 100)); //// scroll.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(10, target.Panel.Children.Count); //// Assert.Equal(10, target.Panel.Children.Count);
Assert.Equal(new Size(10, 20), scroll.Extent); //// Assert.Equal(new Size(10, 20), scroll.Extent);
Assert.Equal(new Size(100, 10), scroll.Viewport); //// Assert.Equal(new Size(100, 10), scroll.Viewport);
target.VirtualizationMode = ItemVirtualizationMode.None; //// target.VirtualizationMode = ItemVirtualizationMode.None;
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100)); //// target.Arrange(new Rect(0, 0, 100, 100));
// Here - unlike changing the other way - we need to do a layout pass on the scroll //// // Here - unlike changing the other way - we need to do a layout pass on the scroll
// content presenter as non-logical scroll values are only updated on arrange. //// // content presenter as non-logical scroll values are only updated on arrange.
scroll.Measure(new Size(100, 100)); //// scroll.Measure(new Size(100, 100));
scroll.Arrange(new Rect(0, 0, 100, 100)); //// scroll.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(20, target.Panel.Children.Count); //// Assert.Equal(20, target.Panel.Children.Count);
Assert.Equal(new Size(100, 200), scroll.Extent); //// Assert.Equal(new Size(100, 200), scroll.Extent);
Assert.Equal(new Size(100, 100), scroll.Viewport); //// Assert.Equal(new Size(100, 100), scroll.Viewport);
} //// }
private static ItemsPresenter CreateTarget( //// private static ItemsPresenter CreateTarget(
ItemVirtualizationMode mode = ItemVirtualizationMode.Simple, //// ItemVirtualizationMode mode = ItemVirtualizationMode.Simple,
Orientation orientation = Orientation.Vertical, //// Orientation orientation = Orientation.Vertical,
bool useContainers = true, //// bool useContainers = true,
int itemCount = 20) //// int itemCount = 20)
{ //// {
ItemsPresenter result; //// ItemsPresenter result;
var items = Enumerable.Range(0, itemCount).Select(x => $"Item {x}").ToList(); //// var items = Enumerable.Range(0, itemCount).Select(x => $"Item {x}").ToList();
var scroller = new TestScroller //// var scroller = new TestScroller
{ //// {
CanHorizontallyScroll = false, //// CanHorizontallyScroll = false,
CanVerticallyScroll = true, //// CanVerticallyScroll = true,
Content = result = new TestItemsPresenter(useContainers) //// Content = result = new TestItemsPresenter(useContainers)
{ //// {
Items = items, //// Items = items,
ItemsPanel = VirtualizingPanelTemplate(orientation), //// ItemsPanel = VirtualizingPanelTemplate(orientation),
ItemTemplate = ItemTemplate(), //// ItemTemplate = ItemTemplate(),
VirtualizationMode = mode, //// VirtualizationMode = mode,
} //// }
}; //// };
scroller.UpdateChild(); //// scroller.UpdateChild();
return result; //// return result;
} //// }
private static IDataTemplate ItemTemplate() //// private static IDataTemplate ItemTemplate()
{ //// {
return new FuncDataTemplate<string>((x, _) => new Canvas //// return new FuncDataTemplate<string>((x, _) => new Canvas
{ //// {
Width = 10, //// Width = 10,
Height = 10, //// Height = 10,
}); //// });
} //// }
private static ITemplate<Panel> VirtualizingPanelTemplate( //// private static ITemplate<Panel> VirtualizingPanelTemplate(
Orientation orientation = Orientation.Vertical) //// Orientation orientation = Orientation.Vertical)
{ //// {
return new FuncTemplate<Panel>(() => new VirtualizingStackPanel //// return new FuncTemplate<Panel>(() => new VirtualizingStackPanel
{ //// {
Orientation = orientation, //// Orientation = orientation,
}); //// });
} //// }
private class TestScroller : ScrollContentPresenter, IRenderRoot, ILayoutRoot //// private class TestScroller : ScrollContentPresenter, IRenderRoot, ILayoutRoot
{ //// {
public TestScroller() //// public TestScroller()
{ //// {
LayoutManager = new LayoutManager(this); //// LayoutManager = new LayoutManager(this);
} //// }
public IRenderer Renderer { get; } //// public IRenderer Renderer { get; }
public Size ClientSize { get; } //// public Size ClientSize { get; }
public double RenderScaling => 1; //// public double RenderScaling => 1;
public Size MaxClientSize => Size.Infinity; //// public Size MaxClientSize => Size.Infinity;
public double LayoutScaling => 1; //// public double LayoutScaling => 1;
public ILayoutManager LayoutManager { get; } //// public ILayoutManager LayoutManager { get; }
public IRenderTarget CreateRenderTarget() => throw new NotImplementedException(); //// public IRenderTarget CreateRenderTarget() => throw new NotImplementedException();
public void Invalidate(Rect rect) => throw new NotImplementedException(); //// public void Invalidate(Rect rect) => throw new NotImplementedException();
public Point PointToClient(PixelPoint p) => throw new NotImplementedException(); //// public Point PointToClient(PixelPoint p) => throw new NotImplementedException();
public PixelPoint PointToScreen(Point p) => throw new NotImplementedException(); //// public PixelPoint PointToScreen(Point p) => throw new NotImplementedException();
} //// }
private class TestItemsPresenter : ItemsPresenter //// private class TestItemsPresenter : ItemsPresenter
{ //// {
private bool _useContainers; //// private bool _useContainers;
public TestItemsPresenter(bool useContainers) //// public TestItemsPresenter(bool useContainers)
{ //// {
_useContainers = useContainers; //// _useContainers = useContainers;
} //// }
protected override IItemContainerGenerator CreateItemContainerGenerator() //// protected override IItemContainerGenerator CreateItemContainerGenerator()
{ //// {
return _useContainers ? //// return _useContainers ?
new ItemContainerGenerator<TestContainer>(this, TestContainer.ContentProperty, null) : //// new ItemContainerGenerator<TestContainer>(this, TestContainer.ContentProperty, null) :
new ItemContainerGenerator(this); //// new ItemContainerGenerator(this);
} //// }
} //// }
private class TestContainer : ContentControl //// private class TestContainer : ContentControl
{ //// {
public TestContainer() //// public TestContainer()
{ //// {
Width = 10; //// Width = 10;
Height = 10; //// Height = 10;
} //// }
} //// }
} //// }
} ////}

1822
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs

File diff suppressed because it is too large

5
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@ -1115,10 +1115,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{ {
Name = "popup", Name = "popup",
PlacementTarget = control, PlacementTarget = control,
Child = new ItemsPresenter Child = new ItemsPresenter(),
{
[~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty],
}
}.RegisterInNameScope(scope); }.RegisterInNameScope(scope);
} }

2
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -2090,9 +2090,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
new ItemsPresenter new ItemsPresenter
{ {
Name = "itemsPresenter", Name = "itemsPresenter",
[~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty],
[~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty],
[~ItemsPresenter.VirtualizationModeProperty] = control[~ListBox.VirtualizationModeProperty],
}.RegisterInNameScope(scope)); }.RegisterInNameScope(scope));
} }

1
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs

@ -125,7 +125,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
new ItemsPresenter new ItemsPresenter
{ {
Name = "itemsPresenter", Name = "itemsPresenter",
[~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty],
[~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty],
}.RegisterInNameScope(scope)); }.RegisterInNameScope(scope));
} }

1
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@ -1536,7 +1536,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
new ItemsPresenter new ItemsPresenter
{ {
Name = "PART_ItemsPresenter", Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty],
[~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty],
}.RegisterInNameScope(scope)); }.RegisterInNameScope(scope));
} }

1
tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs

@ -106,7 +106,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
return new ItemsPresenter return new ItemsPresenter
{ {
Name = "itemsPresenter", Name = "itemsPresenter",
[!ItemsPresenter.ItemsProperty] = parent[!ItemsControl.ItemsProperty],
}.RegisterInNameScope(scope); }.RegisterInNameScope(scope);
} }
} }

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

@ -1303,7 +1303,6 @@ namespace Avalonia.Controls.UnitTests
return new FuncControlTemplate<TreeView>((parent, scope) => new ItemsPresenter return new FuncControlTemplate<TreeView>((parent, scope) => new ItemsPresenter
{ {
Name = "PART_ItemsPresenter", Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
}.RegisterInNameScope(scope)); }.RegisterInNameScope(scope));
} }
@ -1321,7 +1320,6 @@ namespace Avalonia.Controls.UnitTests
new ItemsPresenter new ItemsPresenter
{ {
Name = "PART_ItemsPresenter", Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
}.RegisterInNameScope(scope) }.RegisterInNameScope(scope)
} }
}); });

522
tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

@ -1,261 +1,261 @@
using System; ////using System;
using System.Collections.Generic; ////using System.Collections.Generic;
using Avalonia.Controls.Primitives; ////using Avalonia.Controls.Primitives;
using Avalonia.Input; ////using Avalonia.Input;
using Avalonia.LogicalTree; ////using Avalonia.LogicalTree;
using Moq; ////using Moq;
using Xunit; ////using Xunit;
namespace Avalonia.Controls.UnitTests ////namespace Avalonia.Controls.UnitTests
{ ////{
public class VirtualizingStackPanelTests //// public class VirtualizingStackPanelTests
{ //// {
public class Vertical //// public class Vertical
{ //// {
[Fact] //// [Fact]
public void Measure_Invokes_Controller_UpdateControls() //// public void Measure_Invokes_Controller_UpdateControls()
{ //// {
var target = new VirtualizingStackPanel(); //// var target = new VirtualizingStackPanel();
var controller = new Mock<IVirtualizingController>(); //// var controller = new Mock<IVirtualizingController>();
((IVirtualizingPanel)target).Controller = controller.Object; //// ((IVirtualizingPanel)target).Controller = controller.Object;
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
controller.Verify(x => x.UpdateControls(), Times.Once()); //// controller.Verify(x => x.UpdateControls(), Times.Once());
} //// }
[Fact] //// [Fact]
public void Measure_Invokes_Controller_UpdateControls_If_AvailableSize_Changes() //// public void Measure_Invokes_Controller_UpdateControls_If_AvailableSize_Changes()
{ //// {
var target = new VirtualizingStackPanel(); //// var target = new VirtualizingStackPanel();
var controller = new Mock<IVirtualizingController>(); //// var controller = new Mock<IVirtualizingController>();
((IVirtualizingPanel)target).Controller = controller.Object; //// ((IVirtualizingPanel)target).Controller = controller.Object;
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
target.InvalidateMeasure(); //// target.InvalidateMeasure();
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
target.InvalidateMeasure(); //// target.InvalidateMeasure();
target.Measure(new Size(100, 101)); //// target.Measure(new Size(100, 101));
controller.Verify(x => x.UpdateControls(), Times.Exactly(2)); //// controller.Verify(x => x.UpdateControls(), Times.Exactly(2));
} //// }
[Fact] //// [Fact]
public void Measure_Does_Not_Invoke_Controller_UpdateControls_If_AvailableSize_Is_The_Same() //// public void Measure_Does_Not_Invoke_Controller_UpdateControls_If_AvailableSize_Is_The_Same()
{ //// {
var target = new VirtualizingStackPanel(); //// var target = new VirtualizingStackPanel();
var controller = new Mock<IVirtualizingController>(); //// var controller = new Mock<IVirtualizingController>();
((IVirtualizingPanel)target).Controller = controller.Object; //// ((IVirtualizingPanel)target).Controller = controller.Object;
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
target.InvalidateMeasure(); //// target.InvalidateMeasure();
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
controller.Verify(x => x.UpdateControls(), Times.Once()); //// controller.Verify(x => x.UpdateControls(), Times.Once());
} //// }
[Fact] //// [Fact]
public void Measure_Invokes_Controller_UpdateControls_If_AvailableSize_Is_The_Same_After_ForceInvalidateMeasure() //// public void Measure_Invokes_Controller_UpdateControls_If_AvailableSize_Is_The_Same_After_ForceInvalidateMeasure()
{ //// {
var target = new VirtualizingStackPanel(); //// var target = new VirtualizingStackPanel();
var controller = new Mock<IVirtualizingController>(); //// var controller = new Mock<IVirtualizingController>();
((IVirtualizingPanel)target).Controller = controller.Object; //// ((IVirtualizingPanel)target).Controller = controller.Object;
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
((IVirtualizingPanel)target).ForceInvalidateMeasure(); //// ((IVirtualizingPanel)target).ForceInvalidateMeasure();
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
controller.Verify(x => x.UpdateControls(), Times.Exactly(2)); //// controller.Verify(x => x.UpdateControls(), Times.Exactly(2));
} //// }
[Fact] //// [Fact]
public void Arrange_Invokes_Controller_UpdateControls() //// public void Arrange_Invokes_Controller_UpdateControls()
{ //// {
var target = new VirtualizingStackPanel(); //// var target = new VirtualizingStackPanel();
var controller = new Mock<IVirtualizingController>(); //// var controller = new Mock<IVirtualizingController>();
((IVirtualizingPanel)target).Controller = controller.Object; //// ((IVirtualizingPanel)target).Controller = controller.Object;
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 110, 110)); //// target.Arrange(new Rect(0, 0, 110, 110));
controller.Verify(x => x.UpdateControls(), Times.Exactly(2)); //// controller.Verify(x => x.UpdateControls(), Times.Exactly(2));
} //// }
[Fact] //// [Fact]
public void Reports_IsFull_False_Until_Measure_Height_Is_Reached() //// public void Reports_IsFull_False_Until_Measure_Height_Is_Reached()
{ //// {
var target = new VirtualizingStackPanel(); //// var target = new VirtualizingStackPanel();
var vp = (IVirtualizingPanel)target; //// var vp = (IVirtualizingPanel)target;
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
Assert.Equal(new Size(0, 0), target.DesiredSize); //// Assert.Equal(new Size(0, 0), target.DesiredSize);
Assert.Equal(new Size(0, 0), target.Bounds.Size); //// Assert.Equal(new Size(0, 0), target.Bounds.Size);
Assert.False(vp.IsFull); //// Assert.False(vp.IsFull);
Assert.Equal(0, vp.OverflowCount); //// Assert.Equal(0, vp.OverflowCount);
target.Children.Add(new Canvas { Width = 50, Height = 50 }); //// target.Children.Add(new Canvas { Width = 50, Height = 50 });
Assert.False(vp.IsFull); //// Assert.False(vp.IsFull);
Assert.Equal(0, vp.OverflowCount); //// Assert.Equal(0, vp.OverflowCount);
target.Children.Add(new Canvas { Width = 50, Height = 50 }); //// target.Children.Add(new Canvas { Width = 50, Height = 50 });
Assert.True(vp.IsFull); //// Assert.True(vp.IsFull);
Assert.Equal(0, vp.OverflowCount); //// Assert.Equal(0, vp.OverflowCount);
} //// }
[Fact] //// [Fact]
public void Reports_Overflow_After_Arrange() //// public void Reports_Overflow_After_Arrange()
{ //// {
var target = new VirtualizingStackPanel(); //// var target = new VirtualizingStackPanel();
var vp = (IVirtualizingPanel)target; //// var vp = (IVirtualizingPanel)target;
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
target.Arrange(new Rect(target.DesiredSize)); //// target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(new Size(0, 0), target.Bounds.Size); //// Assert.Equal(new Size(0, 0), target.Bounds.Size);
target.Children.Add(new Canvas { Width = 50, Height = 50 }); //// target.Children.Add(new Canvas { Width = 50, Height = 50 });
target.Children.Add(new Canvas { Width = 50, Height = 50 }); //// target.Children.Add(new Canvas { Width = 50, Height = 50 });
target.Children.Add(new Canvas { Width = 50, Height = 50 }); //// target.Children.Add(new Canvas { Width = 50, Height = 50 });
target.Children.Add(new Canvas { Width = 50, Height = 50 }); //// target.Children.Add(new Canvas { Width = 50, Height = 50 });
Assert.Equal(0, vp.OverflowCount); //// Assert.Equal(0, vp.OverflowCount);
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
target.Arrange(new Rect(target.DesiredSize)); //// target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(2, vp.OverflowCount); //// Assert.Equal(2, vp.OverflowCount);
} //// }
[Fact] //// [Fact]
public void Reports_Correct_Overflow_During_Arrange() //// public void Reports_Correct_Overflow_During_Arrange()
{ //// {
var target = new VirtualizingStackPanel(); //// var target = new VirtualizingStackPanel();
var vp = (IVirtualizingPanel)target; //// var vp = (IVirtualizingPanel)target;
var controller = new Mock<IVirtualizingController>(); //// var controller = new Mock<IVirtualizingController>();
var called = false; //// var called = false;
target.Children.Add(new Canvas { Width = 50, Height = 50 }); //// target.Children.Add(new Canvas { Width = 50, Height = 50 });
target.Children.Add(new Canvas { Width = 50, Height = 52 }); //// target.Children.Add(new Canvas { Width = 50, Height = 52 });
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
controller.Setup(x => x.UpdateControls()).Callback(() => //// controller.Setup(x => x.UpdateControls()).Callback(() =>
{ //// {
Assert.Equal(2, vp.PixelOverflow); //// Assert.Equal(2, vp.PixelOverflow);
Assert.Equal(0, vp.OverflowCount); //// Assert.Equal(0, vp.OverflowCount);
called = true; //// called = true;
}); //// });
vp.Controller = controller.Object; //// vp.Controller = controller.Object;
target.Arrange(new Rect(target.DesiredSize)); //// target.Arrange(new Rect(target.DesiredSize));
Assert.True(called); //// Assert.True(called);
} //// }
[Fact] //// [Fact]
public void Reports_PixelOverflow_After_Arrange() //// public void Reports_PixelOverflow_After_Arrange()
{ //// {
var target = new VirtualizingStackPanel(); //// var target = new VirtualizingStackPanel();
target.Children.Add(new Canvas { Width = 50, Height = 50 }); //// target.Children.Add(new Canvas { Width = 50, Height = 50 });
target.Children.Add(new Canvas { Width = 50, Height = 52 }); //// target.Children.Add(new Canvas { Width = 50, Height = 52 });
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
target.Arrange(new Rect(target.DesiredSize)); //// target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(2, ((IVirtualizingPanel)target).PixelOverflow); //// Assert.Equal(2, ((IVirtualizingPanel)target).PixelOverflow);
} //// }
[Fact] //// [Fact]
public void Reports_PixelOverflow_After_Arrange_Smaller_Than_Measure() //// public void Reports_PixelOverflow_After_Arrange_Smaller_Than_Measure()
{ //// {
var target = new VirtualizingStackPanel(); //// var target = new VirtualizingStackPanel();
target.Children.Add(new Canvas { Width = 50, Height = 50 }); //// target.Children.Add(new Canvas { Width = 50, Height = 50 });
target.Children.Add(new Canvas { Width = 50, Height = 52 }); //// target.Children.Add(new Canvas { Width = 50, Height = 52 });
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 50, 50)); //// target.Arrange(new Rect(0, 0, 50, 50));
Assert.Equal(52, ((IVirtualizingPanel)target).PixelOverflow); //// Assert.Equal(52, ((IVirtualizingPanel)target).PixelOverflow);
} //// }
[Fact] //// [Fact]
public void Reports_PixelOverflow_With_PixelOffset() //// public void Reports_PixelOverflow_With_PixelOffset()
{ //// {
var target = new VirtualizingStackPanel(); //// var target = new VirtualizingStackPanel();
var vp = (IVirtualizingPanel)target; //// var vp = (IVirtualizingPanel)target;
target.Children.Add(new Canvas { Width = 50, Height = 50 }); //// target.Children.Add(new Canvas { Width = 50, Height = 50 });
target.Children.Add(new Canvas { Width = 50, Height = 52 }); //// target.Children.Add(new Canvas { Width = 50, Height = 52 });
vp.PixelOffset = 2; //// vp.PixelOffset = 2;
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
target.Arrange(new Rect(target.DesiredSize)); //// target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(2, vp.PixelOverflow); //// Assert.Equal(2, vp.PixelOverflow);
} //// }
[Fact] //// [Fact]
public void PixelOffset_Can_Be_More_Than_Child_Without_Affecting_IsFull() //// public void PixelOffset_Can_Be_More_Than_Child_Without_Affecting_IsFull()
{ //// {
var target = new VirtualizingStackPanel(); //// var target = new VirtualizingStackPanel();
var vp = (IVirtualizingPanel)target; //// var vp = (IVirtualizingPanel)target;
target.Children.Add(new Canvas { Width = 50, Height = 50 }); //// target.Children.Add(new Canvas { Width = 50, Height = 50 });
target.Children.Add(new Canvas { Width = 50, Height = 52 }); //// target.Children.Add(new Canvas { Width = 50, Height = 52 });
vp.PixelOffset = 55; //// vp.PixelOffset = 55;
target.Measure(new Size(100, 100)); //// target.Measure(new Size(100, 100));
target.Arrange(new Rect(target.DesiredSize)); //// target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(55, vp.PixelOffset); //// Assert.Equal(55, vp.PixelOffset);
Assert.Equal(2, vp.PixelOverflow); //// Assert.Equal(2, vp.PixelOverflow);
Assert.True(vp.IsFull); //// Assert.True(vp.IsFull);
} //// }
[Fact] //// [Fact]
public void Passes_Navigation_Request_To_ILogicalScrollable_Parent() //// public void Passes_Navigation_Request_To_ILogicalScrollable_Parent()
{ //// {
var target = new VirtualizingStackPanel(); //// var target = new VirtualizingStackPanel();
var presenter = new TestPresenter { Child = target }; //// var presenter = new TestPresenter { Child = target };
var from = new Canvas(); //// var from = new Canvas();
((INavigableContainer)target).GetControl(NavigationDirection.Next, from, false); //// ((INavigableContainer)target).GetControl(NavigationDirection.Next, from, false);
Assert.Equal(1, presenter.NavigationRequests.Count); //// Assert.Equal(1, presenter.NavigationRequests.Count);
Assert.Equal((NavigationDirection.Next, from), presenter.NavigationRequests[0]); //// Assert.Equal((NavigationDirection.Next, from), presenter.NavigationRequests[0]);
} //// }
private class TestPresenter : Decorator, ILogicalScrollable //// private class TestPresenter : Decorator, ILogicalScrollable
{ //// {
public bool CanHorizontallyScroll { get; set; } //// public bool CanHorizontallyScroll { get; set; }
public bool CanVerticallyScroll { get; set; } //// public bool CanVerticallyScroll { get; set; }
public bool IsLogicalScrollEnabled => true; //// public bool IsLogicalScrollEnabled => true;
public Size ScrollSize { get; } //// public Size ScrollSize { get; }
public Size PageScrollSize { get; } //// public Size PageScrollSize { get; }
public Size Extent { get; } //// public Size Extent { get; }
public Vector Offset { get; set; } //// public Vector Offset { get; set; }
public Size Viewport { get; } //// public Size Viewport { get; }
public event EventHandler ScrollInvalidated; //// public event EventHandler ScrollInvalidated;
public List<(NavigationDirection, Control)> NavigationRequests { get; } = new(); //// public List<(NavigationDirection, Control)> NavigationRequests { get; } = new();
public bool BringIntoView(Control target, Rect targetRect) //// public bool BringIntoView(Control target, Rect targetRect)
{ //// {
throw new NotImplementedException(); //// throw new NotImplementedException();
} //// }
public Control GetControlInDirection(NavigationDirection direction, Control from) //// public Control GetControlInDirection(NavigationDirection direction, Control from)
{ //// {
NavigationRequests.Add((direction, from)); //// NavigationRequests.Add((direction, from));
return null; //// return null;
} //// }
public void RaiseScrollInvalidated(EventArgs e) //// public void RaiseScrollInvalidated(EventArgs e)
{ //// {
throw new NotImplementedException(); //// throw new NotImplementedException();
} //// }
} //// }
} //// }
} //// }
} ////}

1
tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs

@ -139,7 +139,6 @@ namespace Avalonia.ReactiveUI.UnitTests
Child = new ItemsPresenter Child = new ItemsPresenter
{ {
Name = "PART_ItemsPresenter", Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
}.RegisterInNameScope(scope) }.RegisterInNameScope(scope)
}); });
} }

1
tests/Avalonia.UnitTests/TestExtensions.cs

@ -9,6 +9,5 @@ namespace Avalonia.UnitTests
public static class TestExtensions public static class TestExtensions
{ {
public static void ApplyTemplate(this IContentPresenter presenter) => ((Layoutable)presenter).ApplyTemplate(); public static void ApplyTemplate(this IContentPresenter presenter) => ((Layoutable)presenter).ApplyTemplate();
public static void ApplyTemplate(this IItemsPresenter presenter) => ((Layoutable)presenter).ApplyTemplate();
} }
} }

Loading…
Cancel
Save