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

4
samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml

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

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

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

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

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

2
src/Avalonia.Controls/ComboBox.cs

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

78
src/Avalonia.Controls/IVirtualizingPanel.cs

@ -7,83 +7,5 @@ namespace Avalonia.Controls
/// </summary>
public interface IVirtualizingPanel
{
/// <summary>
/// Gets the children of the panel.
/// </summary>
Controls Children { get; }
/// <summary>
/// Gets or sets the controller for the virtualizing panel.
/// </summary>
/// <remarks>
/// A virtualizing controller is responsible for maintaining the controls in the virtualizing
/// panel. This property will be set by the controller when virtualization is initialized.
/// Note that this property may remain null if the panel is added to a control that does
/// not act as a virtualizing controller.
/// </remarks>
IVirtualizingController? Controller { get; set; }
/// <summary>
/// Gets a value indicating whether the panel is full.
/// </summary>
/// <remarks>
/// This property should return false until enough children are added to fill the space
/// passed into the last measure or arrange in the direction of scroll. It should be
/// updated immediately after a child is added or removed.
/// </remarks>
bool IsFull { get; }
/// <summary>
/// Gets the number of items that can be removed while keeping the panel full.
/// </summary>
/// <remarks>
/// This property should return the number of children that are completely out of the
/// panel's current bounds in the direction of scroll. It should be updated after an
/// arrange.
/// </remarks>
int OverflowCount { get; }
/// <summary>
/// Gets the direction of scroll.
/// </summary>
Orientation ScrollDirection { get; }
/// <summary>
/// Gets the average size of the materialized items in the direction of scroll.
/// </summary>
double AverageItemSize { get; }
/// <summary>
/// Gets or sets a size in pixels by which the content is overflowing the panel, in the
/// direction of scroll.
/// </summary>
/// <remarks>
/// This may be non-zero even when <see cref="OverflowCount"/> is zero if the last item
/// overflows the panel bounds.
/// </remarks>
double PixelOverflow { get; }
/// <summary>
/// Gets or sets the current pixel offset of the items in the direction of scroll.
/// </summary>
double PixelOffset { get; set; }
/// <summary>
/// Gets or sets the current scroll offset in the cross axis.
/// </summary>
double CrossAxisOffset { get; set; }
/// <summary>
/// Invalidates the measure of the control and forces a call to
/// <see cref="IVirtualizingController.UpdateControls"/> on the next measure.
/// </summary>
/// <remarks>
/// The implementation for this method should call
/// <see cref="Layoutable.InvalidateMeasure"/> and also ensure that the next call to
/// <see cref="Layoutable.Measure(Size)"/> calls
/// <see cref="IVirtualizingController.UpdateControls"/> on the next measure even if
/// the available size hasn't changed.
/// </remarks>
void ForceInvalidateMeasure();
}
}

12
src/Avalonia.Controls/ItemsControl.cs

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

2
src/Avalonia.Controls/ListBox.cs

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

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

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

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

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

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

@ -18,9 +18,9 @@ namespace Avalonia.Controls.Presenters
public interface IItemsPresenterHost
{
/// <summary>
/// Registers an <see cref="IItemsPresenter"/> with a host control.
/// Registers an <see cref="ItemsPresenter"/> with a host control.
/// </summary>
/// <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.Collections;
using System.Collections.Specialized;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using static Avalonia.Utilities.MathUtilities;
using System.Diagnostics;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Displays items inside an <see cref="ItemsControl"/>.
/// Presents items inside an <see cref="Avalonia.Controls.ItemsControl"/>.
/// </summary>
public class ItemsPresenter : ItemsPresenterBase, ILogicalScrollable
public class ItemsPresenter : Control
{
/// <summary>
/// Defines the <see cref="VirtualizationMode"/> property.
/// Defines the <see cref="ItemsPanel"/> property.
/// </summary>
public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty =
AvaloniaProperty.Register<ItemsPresenter, ItemVirtualizationMode>(
nameof(VirtualizationMode),
defaultValue: ItemVirtualizationMode.None);
public static readonly StyledProperty<ITemplate<Panel>> ItemsPanelProperty =
ItemsControl.ItemsPanelProperty.AddOwner<ItemsPresenter>();
private bool _canHorizontallyScroll;
private bool _canVerticallyScroll;
private EventHandler? _scrollInvalidated;
private ItemsPresenterContainerGenerator? _generator;
/// <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>
static ItemsPresenter()
public ITemplate<Panel> ItemsPanel
{
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(
typeof(ItemsPresenter),
KeyboardNavigationMode.Once);
VirtualizationModeProperty.Changed
.AddClassHandler<ItemsPresenter>((x, e) => x.VirtualizationModeChanged(e));
get => GetValue(ItemsPanelProperty);
set => SetValue(ItemsPanelProperty, value);
}
/// <summary>
/// Gets or sets the virtualization mode for the items.
/// Gets the panel used to display the items.
/// </summary>
public ItemVirtualizationMode VirtualizationMode
{
get { return GetValue(VirtualizationModeProperty); }
set { SetValue(VirtualizationModeProperty, value); }
}
public Panel? Panel { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// Gets the owner <see cref="ItemsControl"/>.
/// </summary>
bool ILogicalScrollable.CanHorizontallyScroll
{
get { return _canHorizontallyScroll; }
set
{
_canHorizontallyScroll = value;
InvalidateMeasure();
}
}
internal ItemsControl? ItemsControl { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
bool ILogicalScrollable.CanVerticallyScroll
public override sealed void ApplyTemplate()
{
get { return _canVerticallyScroll; }
set
if (Panel is null)
{
_canVerticallyScroll = value;
InvalidateMeasure();
Panel = ItemsPanel.Build();
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/>
Vector IScrollable.Offset
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
get { return Virtualizer?.Offset ?? new Vector(); }
set
base.OnPropertyChanged(change);
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/>
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)
private void CreateGeneratorIfSimplePanel()
{
var scrollable = (ILogicalScrollable)this;
var maxX = Math.Max(scrollable.Extent.Width - scrollable.Viewport.Width, 0);
var maxY = Math.Max(scrollable.Extent.Height - scrollable.Viewport.Height, 0);
return new Vector(Clamp(value.X, 0, maxX), Clamp(value.Y, 0, maxY));
}
if (ItemsControl is null || Panel is null || Panel is IVirtualizingPanel)
return;
private void VirtualizationModeChanged(AvaloniaPropertyChangedEventArgs e)
{
Virtualizer?.Dispose();
Virtualizer = ItemVirtualizer.Create(this);
_scrollInvalidated?.Invoke(this, EventArgs.Empty);
_generator?.Dispose();
_generator = new(this);
}
}
}

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.
/// </summary>
/// <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>
/// Scrolls the specified item into view.

252
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -1,252 +1,20 @@
using System;
using System.Collections.Specialized;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Layout;
namespace Avalonia.Controls
{
public class VirtualizingStackPanel : StackPanel, IVirtualizingPanel
public class VirtualizingStackPanel : StackPanel
{
private Size _availableSpace;
private double _takenSpace;
private int _canBeRemoved;
private double _averageItemSize;
private int _averageCount;
private double _pixelOffset;
private double _crossAxisOffset;
private bool _forceRemeasure;
/// <summary>
/// Defines the <see cref="VirtualizationMode"/> property.
/// </summary>
public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty =
AvaloniaProperty.Register<VirtualizingStackPanel, ItemVirtualizationMode>(
nameof(VirtualizationMode));
bool IVirtualizingPanel.IsFull
public ItemVirtualizationMode VirtualizationMode
{
get
{
return Orientation == Orientation.Horizontal ?
_takenSpace >= _availableSpace.Width :
_takenSpace >= _availableSpace.Height;
}
}
IVirtualizingController? IVirtualizingPanel.Controller { get; set; }
int IVirtualizingPanel.OverflowCount => _canBeRemoved;
Orientation IVirtualizingPanel.ScrollDirection => Orientation;
double IVirtualizingPanel.AverageItemSize => _averageItemSize;
double IVirtualizingPanel.PixelOverflow
{
get
{
var bounds = Orientation == Orientation.Horizontal ?
_availableSpace.Width : _availableSpace.Height;
return Math.Max(0, _takenSpace - bounds);
}
}
double IVirtualizingPanel.PixelOffset
{
get { return _pixelOffset; }
set
{
if (_pixelOffset != value)
{
_pixelOffset = value;
InvalidateArrange();
}
}
}
double IVirtualizingPanel.CrossAxisOffset
{
get { return _crossAxisOffset; }
set
{
if (_crossAxisOffset != value)
{
_crossAxisOffset = value;
InvalidateArrange();
}
}
}
private IVirtualizingController? Controller => ((IVirtualizingPanel)this).Controller;
void IVirtualizingPanel.ForceInvalidateMeasure()
{
InvalidateMeasure();
_forceRemeasure = true;
}
protected override Size MeasureOverride(Size availableSize)
{
if (_forceRemeasure || availableSize != 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;
get => GetValue(VirtualizationModeProperty);
set => SetValue(VirtualizationModeProperty, value);
}
}
}

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

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

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

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

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

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

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

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

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

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

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}">
<ScrollViewer Name="PART_ScrollViewer" HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}" VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{TemplateBinding Padding}"
VirtualizationMode="{TemplateBinding VirtualizationMode}"/>
Margin="{TemplateBinding Padding}"/>
</ScrollViewer>
</Border>
</ControlTemplate>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -324,7 +324,6 @@ namespace Avalonia.Controls.UnitTests
Content = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(),
}.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.Linq;
using Moq;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
#nullable enable
namespace Avalonia.Controls.UnitTests.Presenters
{
public class ItemsPresenterTests
@ -27,327 +28,126 @@ namespace Avalonia.Controls.UnitTests.Presenters
}
[Fact]
public void Should_Add_Containers()
{
var target = new ItemsPresenter
{
Items = new[] { "foo", "bar" },
};
target.ApplyTemplate();
Assert.Equal(2, target.Panel.Children.Count);
Assert.IsType<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()
public void Panel_Should_Be_Visual_Child()
{
var items = new AvaloniaList<string>(new[] { "foo", "bar" });
var target = new ItemsPresenter
{
Items = items,
};
target.ApplyTemplate();
items.RemoveAt(0);
var (target, _, _) = CreateTarget();
var child = target.GetVisualChildren().Single();
Assert.Single(target.Panel.Children);
Assert.Equal("bar", ((ContentPresenter)target.Panel.Children[0]).Content);
Assert.Equal("bar", ((ContentPresenter)target.ItemContainerGenerator.ContainerFromIndex(0)).Content);
Assert.Equal(target.Panel, child);
}
[Fact]
public void Clearing_Items_Should_Remove_Containers()
public class NonVirtualizingPanel
{
var items = new ObservableCollection<string> { "foo", "bar" };
var target = new ItemsPresenter
[Fact]
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();
items.Clear();
Assert.Empty(target.Panel.Children);
Assert.Empty(target.ItemContainerGenerator.Containers);
}
AssertContainers(panel, items);
}
[Fact]
public void Replacing_Items_Should_Update_Containers()
{
var items = new ObservableCollection<string> { "foo", "bar", "baz" };
var target = new ItemsPresenter
[Fact]
public void Creates_Containers_For_Inserted_Items()
{
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[1] = "baz";
items.Insert(1, "foo2");
root.LayoutManager.ExecuteLayoutPass();
var text = target.Panel.Children
.OfType<ContentPresenter>()
.Select(x => x.Content)
.ToList();
Assert.Equal(new[] { "foo", "baz", "baz" }, text);
}
AssertContainers(panel, items);
}
[Fact]
public void Moving_Items_Should_Update_Containers()
{
var items = new ObservableCollection<string> { "foo", "bar", "baz" };
var target = new ItemsPresenter
[Fact]
public void Removes_Containers_For_Removed_Items()
{
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.Move(2, 1);
items.RemoveAt(1);
root.LayoutManager.ExecuteLayoutPass();
var text = target.Panel.Children
.OfType<ContentPresenter>()
.Select(x => x.Content)
.ToList();
Assert.Equal(new[] { "foo", "baz", "bar" }, text);
}
AssertContainers(panel, items);
}
[Fact]
public void Inserting_Items_Should_Update_Containers()
{
var items = new ObservableCollection<string> { "foo", "bar", "baz" };
var target = new ItemsPresenter
[Fact]
public void Updates_Containers_For_Moved_Items()
{
Items = items,
};
target.ApplyTemplate();
items.Insert(2, "insert");
var text = target.Panel.Children
.OfType<ContentPresenter>()
.Select(x => x.Content)
.ToList();
Assert.Equal(new[] { "foo", "bar", "insert", "baz" }, text);
}
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);
[Fact]
public void Setting_Items_To_Null_Should_Remove_Containers()
{
var target = new ItemsPresenter
{
Items = new[] { "foo", "bar" },
};
items.Move(0, 2);
root.LayoutManager.ExecuteLayoutPass();
target.ApplyTemplate();
target.Items = null;
Assert.Empty(target.Panel.Children);
Assert.Empty(target.ItemContainerGenerator.Containers);
}
[Fact]
public void Should_Handle_Null_Items()
{
var items = new AvaloniaList<string>(new[] { "foo", null, "bar" });
AssertContainers(panel, items);
}
var target = new ItemsPresenter
[Fact]
public void Updates_Containers_For_Replaced_Items()
{
Items = items,
};
target.ApplyTemplate();
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);
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);
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));
}
AssertContainers(panel, items);
}
[Fact]
public void Inserting_Then_Removing_Should_Add_Remove_Containers()
{
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
[Fact]
public void Updates_Containers_On_Items_Changed()
{
VirtualizationMode = ItemVirtualizationMode.None,
Items = items,
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 }),
};
using var app = Start();
var items = new ObservableCollection<string>(new[] { "foo", "bar", "baz" });
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);
foreach (var item in toAdd)
{
items.Insert(1, item);
AssertContainers(panel, newItems);
}
Assert.Equal(items.Count, target.Panel.Children.Count);
foreach (var item in toAdd)
private static void AssertContainers(StackPanel panel, IReadOnlyList<string> items)
{
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]
public void Should_Handle_Duplicate_Items()
private static (ItemsPresenter, ItemsControl, TestRoot) CreateTarget(IReadOnlyList<string>? items = null)
{
var items = new AvaloniaList<int>(new[] { 1, 2, 1 });
var result = new ItemsPresenter();
var target = new ItemsPresenter
var itemsControl = new ItemsControl
{
Items = items,
Template = new FuncControlTemplate<ItemsControl>((_, _) => result)
};
target.ApplyTemplate();
items.RemoveAt(2);
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);
var root = new TestRoot(itemsControl);
root.LayoutManager.ExecuteInitialLayoutPass();
return (result, itemsControl, root);
}
[Fact]
public void Panel_TabNavigation_Should_Be_Set_To_Once()
{
var target = new ItemsPresenter();
target.ApplyTemplate();
Assert.Equal(KeyboardNavigationMode.Once, KeyboardNavigation.GetTabNavigation((InputElement)target.Panel));
}
[Fact]
public void Panel_TabNavigation_Should_Be_Set_To_ItemsPresenter_Value()
{
var target = new ItemsPresenter();
KeyboardNavigation.SetTabNavigation(target, KeyboardNavigationMode.Cycle);
target.ApplyTemplate();
Assert.Equal(KeyboardNavigationMode.Cycle, KeyboardNavigation.GetTabNavigation((InputElement)target.Panel));
}
[Fact]
public void Panel_Should_Be_Visual_Child()
{
var target = new ItemsPresenter();
target.ApplyTemplate();
var child = target.GetVisualChildren().Single();
Assert.Equal(target.Panel, child);
}
private class Item
{
public Item(string value)
{
Value = value;
}
public string Value { get; }
}
private class TestItem : ContentControl
{
}
private class TestItemsControl : ItemsControl
{
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<TestItem>(this, TestItem.ContentProperty, null);
}
}
private static IDisposable Start() => UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
}
}

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

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

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

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

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

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

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

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

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

@ -106,7 +106,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
return new ItemsPresenter
{
Name = "itemsPresenter",
[!ItemsPresenter.ItemsProperty] = parent[!ItemsControl.ItemsProperty],
}.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
{
Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
}.RegisterInNameScope(scope));
}
@ -1321,7 +1320,6 @@ namespace Avalonia.Controls.UnitTests
new ItemsPresenter
{
Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
}.RegisterInNameScope(scope)
}
});

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

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

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

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

1
tests/Avalonia.UnitTests/TestExtensions.cs

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

Loading…
Cancel
Save