Browse Source

Merge branch 'item-virtualization-avalonia'

pull/569/head
Steven Kirk 10 years ago
parent
commit
4ee4ec29be
  1. 34
      Avalonia.sln
  2. 1
      samples/ControlCatalog/Pages/MenuPage.xaml
  3. 6
      samples/VirtualizationTest/App.config
  4. 6
      samples/VirtualizationTest/App.xaml
  5. 16
      samples/VirtualizationTest/App.xaml.cs
  6. 51
      samples/VirtualizationTest/MainWindow.xaml
  7. 25
      samples/VirtualizationTest/MainWindow.xaml.cs
  8. 34
      samples/VirtualizationTest/Program.cs
  9. 36
      samples/VirtualizationTest/Properties/AssemblyInfo.cs
  10. 22
      samples/VirtualizationTest/ViewModels/ItemViewModel.cs
  11. 141
      samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs
  12. 180
      samples/VirtualizationTest/VirtualizationTest.csproj
  13. 26
      samples/VirtualizationTest/VirtualizationTest.v2.ncrunchproject
  14. 10
      samples/VirtualizationTest/packages.config
  15. 55
      samples/XamlTestApplicationPcl/TestScrollable.cs
  16. 5
      samples/XamlTestApplicationPcl/Views/MainWindow.cs
  17. 10
      samples/XamlTestApplicationPcl/Views/MainWindow.xaml
  18. 4
      src/Avalonia.Base/AvaloniaObject.cs
  19. 51
      src/Avalonia.Base/Collections/AvaloniaList.cs
  20. 14
      src/Avalonia.Controls/Avalonia.Controls.csproj
  21. 2
      src/Avalonia.Controls/Canvas.cs
  22. 17
      src/Avalonia.Controls/Control.cs
  23. 38
      src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
  24. 11
      src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs
  25. 180
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  26. 29
      src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
  27. 10
      src/Avalonia.Controls/Generators/ItemContainerInfo.cs
  28. 27
      src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs
  29. 6
      src/Avalonia.Controls/Generators/TreeContainerIndex.cs
  30. 11
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  31. 29
      src/Avalonia.Controls/IScrollable.cs
  32. 22
      src/Avalonia.Controls/ISetInheritanceParent.cs
  33. 22
      src/Avalonia.Controls/IVirtualizingController.cs
  34. 67
      src/Avalonia.Controls/IVirtualizingPanel.cs
  35. 21
      src/Avalonia.Controls/ItemVirtualizationMode.cs
  36. 24
      src/Avalonia.Controls/ItemsControl.cs
  37. 57
      src/Avalonia.Controls/ListBox.cs
  38. 7
      src/Avalonia.Controls/Menu.cs
  39. 7
      src/Avalonia.Controls/MenuItem.cs
  40. 17
      src/Avalonia.Controls/Mixins/SelectableMixin.cs
  41. 34
      src/Avalonia.Controls/Panel.cs
  42. 12
      src/Avalonia.Controls/Presenters/CarouselPresenter.cs
  43. 84
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  44. 2
      src/Avalonia.Controls/Presenters/IItemsPresenter.cs
  45. 198
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  46. 170
      src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs
  47. 491
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  48. 178
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  49. 54
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  50. 90
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  51. 69
      src/Avalonia.Controls/Primitives/ILogicalScrollable.cs
  52. 54
      src/Avalonia.Controls/Primitives/IScrollable.cs
  53. 25
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  54. 2
      src/Avalonia.Controls/ScrollViewer.cs
  55. 51
      src/Avalonia.Controls/StackPanel.cs
  56. 48
      src/Avalonia.Controls/Templates/DataTemplateExtensions.cs
  57. 39
      src/Avalonia.Controls/Templates/FuncDataTemplate.cs
  58. 13
      src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs
  59. 6
      src/Avalonia.Controls/Templates/IDataTemplate.cs
  60. 19
      src/Avalonia.Controls/Utils/IEnumerableUtils.cs
  61. 229
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  62. 18
      src/Avalonia.Controls/WrapPanel.cs
  63. 2
      src/Avalonia.Diagnostics/ViewLocator.cs
  64. 4
      src/Avalonia.Input/Avalonia.Input.csproj
  65. 2
      src/Avalonia.Input/IKeyboardNavigationHandler.cs
  66. 2
      src/Avalonia.Input/INavigableContainer.cs
  67. 34
      src/Avalonia.Input/KeyboardNavigationHandler.cs
  68. 27
      src/Avalonia.Input/Navigation/DirectionalNavigation.cs
  69. 26
      src/Avalonia.Input/Navigation/TabNavigation.cs
  70. 14
      src/Avalonia.Input/NavigationDirection.cs
  71. 35
      src/Avalonia.Layout/LayoutManager.cs
  72. 12
      src/Avalonia.SceneGraph/Rect.cs
  73. 5
      src/Avalonia.Themes.Default/ListBox.xaml
  74. 9
      src/Markup/Avalonia.Markup.Xaml/Context/PropertyAccessor.cs
  75. 15
      src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs
  76. 2
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  77. 9
      src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
  78. 2
      src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
  79. 28
      src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs
  80. 99
      tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs
  81. 6
      tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
  82. 7
      tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.v2.ncrunchproject
  83. 4
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  84. 35
      tests/Avalonia.Controls.UnitTests/ControlTests.cs
  85. 49
      tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs
  86. 19
      tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs
  87. 28
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  88. 209
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  89. 12
      tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs
  90. 30
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs
  91. 46
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs
  92. 302
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs
  93. 683
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs
  94. 132
      tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs
  95. 16
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  96. 2
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  97. 170
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
  98. 44
      tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs
  99. 58
      tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs
  100. 8
      tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs

34
Avalonia.sln

@ -157,6 +157,7 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DesignerSupport.TestApp", "tests\Avalonia.DesignerSupport.TestApp\Avalonia.DesignerSupport.TestApp.csproj", "{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}"
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Avalonia.RenderTests", "tests\Avalonia.RenderTests\Avalonia.RenderTests.shproj", "{48840EDD-24BF-495D-911E-2EB12AE75D3B}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VirtualizationTest", "samples\VirtualizationTest\VirtualizationTest.csproj", "{FBCAF3D0-2808-4934-8E96-3F607594517B}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
@ -1909,6 +1910,38 @@ Global
{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}.Release|x86.ActiveCfg = Release|Any CPU
{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}.Release|x86.Build.0 = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|x86.Build.0 = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|Any CPU.Build.0 = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|iPhone.ActiveCfg = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|iPhone.Build.0 = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|x86.ActiveCfg = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|x86.Build.0 = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|iPhone.Build.0 = Debug|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|x86.ActiveCfg = Debug|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|x86.Build.0 = Debug|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|Any CPU.Build.0 = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|iPhone.ActiveCfg = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|iPhone.Build.0 = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|x86.ActiveCfg = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -1960,5 +1993,6 @@ Global
{52F55355-D120-42AC-8116-8410A7D602FA} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{F1381F98-4D24-409A-A6C5-1C5B1E08BB08} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{48840EDD-24BF-495D-911E-2EB12AE75D3B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{FBCAF3D0-2808-4934-8E96-3F607594517B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection
EndGlobal

1
samples/ControlCatalog/Pages/MenuPage.xaml

@ -10,6 +10,7 @@
<Menu>
<MenuItem Header="_First">
<MenuItem Header="Standard _Menu Item"/>
<Separator/>
<MenuItem Header="Menu with _Submenu">
<MenuItem Header="Submenu _1"/>
<MenuItem Header="Submenu _2"/>

6
samples/VirtualizationTest/App.config

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
</startup>
</configuration>

6
samples/VirtualizationTest/App.xaml

@ -0,0 +1,6 @@
<Application xmlns="https://github.com/avaloniaui">
<Application.Styles>
<StyleInclude Source="resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default"/>
</Application.Styles>
</Application>

16
samples/VirtualizationTest/App.xaml.cs

@ -0,0 +1,16 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia;
using Avalonia.Markup.Xaml;
namespace VirtualizationTest
{
public class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
}
}

51
samples/VirtualizationTest/MainWindow.xaml

@ -0,0 +1,51 @@
<Window xmlns="https://github.com/avaloniaui"
Title="Avalonia Virtualization Test">
<DockPanel LastChildFill="True" Margin="16">
<StackPanel DockPanel.Dock="Right"
Margin="16 0 0 0"
MinWidth="150"
Gap="4">
<DropDown Items="{Binding VirtualizationModes}"
SelectedItem="{Binding VirtualizationMode}"/>
<DropDown Items="{Binding Orientations}"
SelectedItem="{Binding Orientation}"/>
<TextBox Watermark="Item Count"
UseFloatingWatermark="True"
Text="{Binding ItemCount}"/>
<TextBox Watermark="Extent"
UseFloatingWatermark="True"
Text="{Binding #listBox.Scroll.Extent, Mode=OneWay}"/>
<TextBox Watermark="Offset"
UseFloatingWatermark="True"
Text="{Binding #listBox.Scroll.Offset, Mode=OneWay}"/>
<TextBox Watermark="Viewport"
UseFloatingWatermark="True"
Text="{Binding #listBox.Scroll.Viewport, Mode=OneWay}"/>
<TextBox Watermark="Item to Create"
UseFloatingWatermark="True"
Text="{Binding NewItemString}"/>
<Button Command="{Binding AddItemCommand}">Add Item</Button>
<Button Command="{Binding RemoveItemCommand}">Remove Item</Button>
<Button Command="{Binding RecreateCommand}">Recreate</Button>
<Button Command="{Binding SelectFirstCommand}">Select First</Button>
<Button Command="{Binding SelectLastCommand}">Select Last</Button>
</StackPanel>
<ListBox Name="listBox"
Items="{Binding Items}"
SelectedItems="{Binding SelectedItems}"
SelectionMode="Multiple"
VirtualizationMode="{Binding VirtualizationMode}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="{Binding Orientation}"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Window>

25
samples/VirtualizationTest/MainWindow.xaml.cs

@ -0,0 +1,25 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using VirtualizationTest.ViewModels;
namespace VirtualizationTest
{
public class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
this.AttachDevTools();
DataContext = new MainWindowViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

34
samples/VirtualizationTest/Program.cs

@ -0,0 +1,34 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Logging.Serilog;
using Serilog;
namespace VirtualizationTest
{
class Program
{
static void Main(string[] args)
{
InitializeLogging();
AppBuilder.Configure<App>()
.UseWin32()
.UseDirect2D1()
.Start<MainWindow>();
}
private static void InitializeLogging()
{
#if DEBUG
SerilogLogger.Initialize(new LoggerConfiguration()
.MinimumLevel.Warning()
.WriteTo.Trace(outputTemplate: "{Area}: {Message}")
.CreateLogger());
#endif
}
}
}

36
samples/VirtualizationTest/Properties/AssemblyInfo.cs

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("VirtualizationTest")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("VirtualizationTest")]
[assembly: AssemblyCopyright("Copyright © 2016")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("fbcaf3d0-2808-4934-8e96-3f607594517b")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

22
samples/VirtualizationTest/ViewModels/ItemViewModel.cs

@ -0,0 +1,22 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using ReactiveUI;
namespace VirtualizationTest.ViewModels
{
internal class ItemViewModel : ReactiveObject
{
private string _prefix;
private int _index;
public ItemViewModel(int index, string prefix = "Item")
{
_prefix = prefix;
_index = index;
}
public string Header => $"{_prefix} {_index}";
}
}

141
samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs

@ -0,0 +1,141 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls;
using ReactiveUI;
namespace VirtualizationTest.ViewModels
{
internal class MainWindowViewModel : ReactiveObject
{
private int _itemCount = 200;
private string _newItemString = "New Item";
private int _newItemIndex;
private IReactiveList<ItemViewModel> _items;
private string _prefix = "Item";
private Orientation _orientation;
private ItemVirtualizationMode _virtualizationMode = ItemVirtualizationMode.Simple;
public MainWindowViewModel()
{
this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems);
RecreateCommand = ReactiveCommand.Create();
RecreateCommand.Subscribe(_ => Recreate());
AddItemCommand = ReactiveCommand.Create();
AddItemCommand.Subscribe(_ => AddItem());
RemoveItemCommand = ReactiveCommand.Create();
RemoveItemCommand.Subscribe(_ => Remove());
SelectFirstCommand = ReactiveCommand.Create();
SelectFirstCommand.Subscribe(_ => SelectItem(0));
SelectLastCommand = ReactiveCommand.Create();
SelectLastCommand.Subscribe(_ => SelectItem(Items.Count - 1));
}
public string NewItemString
{
get { return _newItemString; }
set { this.RaiseAndSetIfChanged(ref _newItemString, value); }
}
public int ItemCount
{
get { return _itemCount; }
set { this.RaiseAndSetIfChanged(ref _itemCount, value); }
}
public AvaloniaList<ItemViewModel> SelectedItems { get; }
= new AvaloniaList<ItemViewModel>();
public IReactiveList<ItemViewModel> Items
{
get { return _items; }
private set { this.RaiseAndSetIfChanged(ref _items, value); }
}
public Orientation Orientation
{
get { return _orientation; }
set { this.RaiseAndSetIfChanged(ref _orientation, value); }
}
public IEnumerable<Orientation> Orientations =>
Enum.GetValues(typeof(Orientation)).Cast<Orientation>();
public ItemVirtualizationMode VirtualizationMode
{
get { return _virtualizationMode; }
set { this.RaiseAndSetIfChanged(ref _virtualizationMode, value); }
}
public IEnumerable<ItemVirtualizationMode> VirtualizationModes =>
Enum.GetValues(typeof(ItemVirtualizationMode)).Cast<ItemVirtualizationMode>();
public ReactiveCommand<object> AddItemCommand { get; private set; }
public ReactiveCommand<object> RecreateCommand { get; private set; }
public ReactiveCommand<object> RemoveItemCommand { get; private set; }
public ReactiveCommand<object> SelectFirstCommand { get; private set; }
public ReactiveCommand<object> SelectLastCommand { get; private set; }
private void ResizeItems(int count)
{
if (Items == null)
{
var items = Enumerable.Range(0, count)
.Select(x => new ItemViewModel(x));
Items = new ReactiveList<ItemViewModel>(items);
}
else if (count > Items.Count)
{
var items = Enumerable.Range(Items.Count, count - Items.Count)
.Select(x => new ItemViewModel(x));
Items.AddRange(items);
}
else if (count < Items.Count)
{
Items.RemoveRange(count, Items.Count - count);
}
}
private void AddItem()
{
var index = Items.Count;
if (SelectedItems.Count > 0)
{
index = Items.IndexOf(SelectedItems[0]);
}
Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString));
}
private void Remove()
{
if (SelectedItems.Count > 0)
{
Items.RemoveAll(SelectedItems);
}
}
private void Recreate()
{
_prefix = _prefix == "Item" ? "Recreated" : "Item";
var items = Enumerable.Range(0, _itemCount)
.Select(x => new ItemViewModel(x, _prefix));
Items = new ReactiveList<ItemViewModel>(items);
}
private void SelectItem(int index)
{
SelectedItems.Clear();
SelectedItems.Add(Items[index]);
}
}
}

180
samples/VirtualizationTest/VirtualizationTest.csproj

@ -0,0 +1,180 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{FBCAF3D0-2808-4934-8E96-3F607594517B}</ProjectGuid>
<OutputType>WinExe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>VirtualizationTest</RootNamespace>
<AssemblyName>VirtualizationTest</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<StartupObject />
</PropertyGroup>
<ItemGroup>
<Reference Include="Serilog, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">
<HintPath>..\..\packages\Serilog.1.5.14\lib\net45\Serilog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Serilog.FullNetFx, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">
<HintPath>..\..\packages\Serilog.1.5.14\lib\net45\Serilog.FullNetFx.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Splat, Version=1.6.2.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\packages\Splat.1.6.2\lib\Net45\Splat.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Reactive.Core, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Interfaces, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Linq, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.PlatformServices, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
</Compile>
<Compile Include="MainWindow.xaml.cs">
<DependentUpon>MainWindow.xaml</DependentUpon>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ViewModels\ItemViewModel.cs" />
<Compile Include="ViewModels\MainWindowViewModel.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Animation\Avalonia.Animation.csproj">
<Project>{d211e587-d8bc-45b9-95a4-f297c8fa5200}</Project>
<Name>Avalonia.Animation</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj">
<Project>{b09b78d8-9b26-48b0-9149-d64a2f120f3f}</Project>
<Name>Avalonia.Base</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj">
<Project>{d2221c82-4a25-4583-9b43-d791e3f6820c}</Project>
<Name>Avalonia.Controls</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.DesignerSupport\Avalonia.DesignerSupport.csproj">
<Project>{799a7bb5-3c2c-48b6-85a7-406a12c420da}</Project>
<Name>Avalonia.DesignerSupport</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj">
<Project>{7062ae20-5dcc-4442-9645-8195bdece63e}</Project>
<Name>Avalonia.Diagnostics</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Input\Avalonia.Input.csproj">
<Project>{62024b2d-53eb-4638-b26b-85eeaa54866e}</Project>
<Name>Avalonia.Input</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Interactivity\Avalonia.Interactivity.csproj">
<Project>{6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b}</Project>
<Name>Avalonia.Interactivity</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Layout\Avalonia.Layout.csproj">
<Project>{42472427-4774-4c81-8aff-9f27b8e31721}</Project>
<Name>Avalonia.Layout</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Logging.Serilog\Avalonia.Logging.Serilog.csproj">
<Project>{B61B66A3-B82D-4875-8001-89D3394FE0C9}</Project>
<Name>Avalonia.Logging.Serilog</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj">
<Project>{6417b24e-49c2-4985-8db2-3ab9d898ec91}</Project>
<Name>Avalonia.ReactiveUI</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.SceneGraph\Avalonia.SceneGraph.csproj">
<Project>{eb582467-6abb-43a1-b052-e981ba910e3a}</Project>
<Name>Avalonia.SceneGraph</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj">
<Project>{f1baa01a-f176-4c6a-b39d-5b40bb1b148f}</Project>
<Name>Avalonia.Styling</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj">
<Project>{3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f}</Project>
<Name>Avalonia.Themes.Default</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj">
<Project>{3e53a01a-b331-47f3-b828-4a5717e77a24}</Project>
<Name>Avalonia.Markup.Xaml</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj">
<Project>{6417e941-21bc-467b-a771-0de389353ce6}</Project>
<Name>Avalonia.Markup</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Windows\Avalonia.Direct2D1\Avalonia.Direct2D1.csproj">
<Project>{3e908f67-5543-4879-a1dc-08eace79b3cd}</Project>
<Name>Avalonia.Direct2D1</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Windows\Avalonia.Win32\Avalonia.Win32.csproj">
<Project>{811a76cf-1cf6-440f-963b-bbe31bd72a82}</Project>
<Name>Avalonia.Win32</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="App.xaml">
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="MainWindow.xaml">
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

26
samples/VirtualizationTest/VirtualizationTest.v2.ncrunchproject

@ -0,0 +1,26 @@
<ProjectConfiguration>
<AutoDetectNugetBuildDependencies>true</AutoDetectNugetBuildDependencies>
<BuildPriority>1000</BuildPriority>
<CopyReferencedAssembliesToWorkspace>false</CopyReferencedAssembliesToWorkspace>
<ConsiderInconclusiveTestsAsPassing>false</ConsiderInconclusiveTestsAsPassing>
<PreloadReferencedAssemblies>false</PreloadReferencedAssemblies>
<AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking>
<AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking>
<AllowCodeAnalysis>false</AllowCodeAnalysis>
<IgnoreThisComponentCompletely>true</IgnoreThisComponentCompletely>
<RunPreBuildEvents>false</RunPreBuildEvents>
<RunPostBuildEvents>false</RunPostBuildEvents>
<PreviouslyBuiltSuccessfully>true</PreviouslyBuiltSuccessfully>
<InstrumentAssembly>true</InstrumentAssembly>
<PreventSigningOfAssembly>false</PreventSigningOfAssembly>
<AnalyseExecutionTimes>true</AnalyseExecutionTimes>
<DetectStackOverflow>true</DetectStackOverflow>
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
<DefaultTestTimeout>60000</DefaultTestTimeout>
<UseBuildConfiguration></UseBuildConfiguration>
<UseBuildPlatform></UseBuildPlatform>
<ProxyProcessPath></ProxyProcessPath>
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture>
</ProjectConfiguration>

10
samples/VirtualizationTest/packages.config

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Rx-Core" version="2.2.5" targetFramework="net452" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="net452" />
<package id="Rx-Linq" version="2.2.5" targetFramework="net452" />
<package id="Rx-Main" version="2.2.5" targetFramework="net452" />
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="net452" />
<package id="Serilog" version="1.5.14" targetFramework="net452" />
<package id="Splat" version="1.6.2" targetFramework="net452" />
</packages>

55
samples/XamlTestApplicationPcl/TestScrollable.cs

@ -2,11 +2,13 @@ using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.VisualTree;
namespace XamlTestApplication
{
public class TestScrollable : Control, IScrollable
public class TestScrollable : Control, ILogicalScrollable
{
private int itemCount = 100;
private Size _extent;
@ -14,6 +16,7 @@ namespace XamlTestApplication
private Size _viewport;
private Size _lineSize;
public bool IsLogicalScrollEnabled => true;
public Action InvalidateScroll { get; set; }
Size IScrollable.Extent
@ -53,6 +56,36 @@ namespace XamlTestApplication
}
}
public override void Render(DrawingContext context)
{
var y = 0.0;
for (var i = (int)_offset.Y; i < itemCount; ++i)
{
using (var line = new FormattedText(
"Item " + (i + 1),
TextBlock.GetFontFamily(this),
TextBlock.GetFontSize(this),
TextBlock.GetFontStyle(this),
TextAlignment.Left,
TextBlock.GetFontWeight(this)))
{
context.DrawText(Brushes.Black, new Point(-_offset.X, y), line);
y += _lineSize.Height;
}
}
}
public bool BringIntoView(IControl target, Rect targetRect)
{
throw new NotImplementedException();
}
public IControl GetControlInDirection(NavigationDirection direction, IControl from)
{
throw new NotImplementedException();
}
protected override Size MeasureOverride(Size availableSize)
{
using (var line = new FormattedText(
@ -76,25 +109,5 @@ namespace XamlTestApplication
InvalidateScroll?.Invoke();
return finalSize;
}
public override void Render(DrawingContext context)
{
var y = 0.0;
for (var i = (int)_offset.Y; i < itemCount; ++i)
{
using (var line = new FormattedText(
"Item " + (i + 1),
TextBlock.GetFontFamily(this),
TextBlock.GetFontSize(this),
TextBlock.GetFontStyle(this),
TextAlignment.Left,
TextBlock.GetFontWeight(this)))
{
context.DrawText(Brushes.Black, new Point(-_offset.X, y), line);
y += _lineSize.Height;
}
}
}
}
}

5
samples/XamlTestApplicationPcl/Views/MainWindow.cs

@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Diagnostics;
@ -25,6 +27,9 @@ namespace XamlTestApplication.Views
AvaloniaXamlLoader.Load(this);
_exitMenu = this.FindControl<MenuItem>("exitMenu");
_exitMenu.Click += (s, e) => Application.Current.Exit();
var virtualList = this.FindControl<ListBox>("virtualList");
virtualList.Items = Enumerable.Range(0, 200).Select(x => $"Item {x}").ToList();
}
}
}

10
samples/XamlTestApplicationPcl/Views/MainWindow.xaml

@ -24,6 +24,16 @@
<TabControl.Transition>
<PageSlide Duration="0.25" />
</TabControl.Transition>
<TabItem Header="Virtualization">
<DockPanel LastChildFill="True">
<StackPanel DockPanel.Dock="Right" Orientation="Vertical">
</StackPanel>
<ListBox Name="virtualList"/>
</DockPanel>
</TabItem>
<TabItem Header="Buttons">
<ScrollViewer CanScrollHorizontally="False">
<StackPanel Margin="10" Gap="4">

4
src/Avalonia.Base/AvaloniaObject.cs

@ -26,7 +26,7 @@ namespace Avalonia
/// <summary>
/// The parent object that inherited values are inherited from.
/// </summary>
private AvaloniaObject _inheritanceParent;
private IAvaloniaObject _inheritanceParent;
/// <summary>
/// The set values/bindings on this object.
@ -120,7 +120,7 @@ namespace Avalonia
/// <value>
/// The inheritance parent.
/// </value>
protected AvaloniaObject InheritanceParent
protected IAvaloniaObject InheritanceParent
{
get
{

51
src/Avalonia.Base/Collections/AvaloniaList.cs

@ -319,6 +319,57 @@ namespace Avalonia.Collections
}
}
/// <summary>
/// Moves an item to a new index.
/// </summary>
/// <param name="oldIndex">The index of the item to move.</param>
/// <param name="newIndex">The index to move the item to.</param>
public void Move(int oldIndex, int newIndex)
{
var item = this[oldIndex];
_inner.RemoveAt(oldIndex);
_inner.Insert(newIndex, item);
if (_collectionChanged != null)
{
var e = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Move,
item,
newIndex,
oldIndex);
_collectionChanged(this, e);
}
}
/// <summary>
/// Moves multiple items to a new index.
/// </summary>
/// <param name="oldIndex">The first index of the items to move.</param>
/// <param name="count">The number of items to move.</param>
/// <param name="newIndex">The index to move the items to.</param>
public void MoveRange(int oldIndex, int count, int newIndex)
{
var items = _inner.GetRange(oldIndex, count);
_inner.RemoveRange(oldIndex, count);
if (newIndex > oldIndex)
{
newIndex -= count;
}
_inner.InsertRange(newIndex, items);
if (_collectionChanged != null)
{
var e = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Move,
items,
newIndex,
oldIndex);
_collectionChanged(this, e);
}
}
/// <summary>
/// Removes an item from the collection.
/// </summary>

14
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -50,13 +50,19 @@
<Compile Include="Design.cs" />
<Compile Include="DockPanel.cs" />
<Compile Include="Expander.cs" />
<Compile Include="Generators\ItemContainer.cs" />
<Compile Include="Generators\ItemContainerInfo.cs" />
<Compile Include="Generators\MenuItemContainerGenerator.cs" />
<Compile Include="Generators\TreeContainerIndex.cs" />
<Compile Include="HotkeyManager.cs" />
<Compile Include="IApplicationLifecycle.cs" />
<Compile Include="IScrollable.cs" />
<Compile Include="INameScope.cs" />
<Compile Include="IPseudoClasses.cs" />
<Compile Include="DropDownItem.cs" />
<Compile Include="ISetInheritanceParent.cs" />
<Compile Include="ItemVirtualizationMode.cs" />
<Compile Include="IVirtualizingController.cs" />
<Compile Include="IVirtualizingPanel.cs" />
<Compile Include="LayoutTransformControl.cs" />
<Compile Include="Mixins\ContentControlMixin.cs" />
<Compile Include="NameScope.cs" />
@ -68,8 +74,11 @@
<Compile Include="Presenters\IContentPresenterHost.cs" />
<Compile Include="Presenters\IItemsPresenterHost.cs" />
<Compile Include="Presenters\ItemsPresenterBase.cs" />
<Compile Include="Presenters\ItemVirtualizerNone.cs" />
<Compile Include="Presenters\ItemVirtualizerSimple.cs" />
<Compile Include="Presenters\ItemVirtualizer.cs" />
<Compile Include="Primitives\HeaderedSelectingControl.cs" />
<Compile Include="Primitives\IScrollable.cs" />
<Compile Include="Primitives\ILogicalScrollable.cs" />
<Compile Include="Primitives\TabStripItem.cs" />
<Compile Include="Primitives\TemplateAppliedEventArgs.cs" />
<Compile Include="SelectionChangedEventArgs.cs" />
@ -170,6 +179,7 @@
<Compile Include="TopLevel.cs" />
<Compile Include="Primitives\PopupRoot.cs" />
<Compile Include="Utils\UndoRedoHelper.cs" />
<Compile Include="VirtualizingStackPanel.cs" />
<Compile Include="WindowState.cs" />
<Compile Include="Window.cs" />
<Compile Include="RowDefinition.cs" />

2
src/Avalonia.Controls/Canvas.cs

@ -136,7 +136,7 @@ namespace Avalonia.Controls
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <returns>The control.</returns>
IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from)
IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from)
{
// TODO: Implement this
return null;

17
src/Avalonia.Controls/Control.cs

@ -33,7 +33,7 @@ namespace Avalonia.Controls
/// - Implements <see cref="IStyleable"/> to allow styling to work on the control.
/// - Implements <see cref="ILogical"/> to form part of a logical tree.
/// </remarks>
public class Control : InputElement, IControl, INamed, ISetLogicalParent, ISupportInitialize
public class Control : InputElement, IControl, INamed, ISetInheritanceParent, ISetLogicalParent, ISupportInitialize
{
/// <summary>
/// Defines the <see cref="DataContext"/> property.
@ -435,7 +435,11 @@ namespace Avalonia.Controls
OnDetachedFromLogicalTreeCore(e);
}
InheritanceParent = parent as AvaloniaObject;
if (InheritanceParent == null || parent == null)
{
InheritanceParent = parent as AvaloniaObject;
}
_parent = (IControl)parent;
if (_parent is IStyleRoot || _parent?.IsAttachedToLogicalTree == true)
@ -455,6 +459,15 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Sets the control's inheritance parent.
/// </summary>
/// <param name="parent">The parent.</param>
void ISetInheritanceParent.SetParent(IAvaloniaObject parent)
{
InheritanceParent = parent;
}
/// <summary>
/// Adds a pseudo-class to be set when a property is true.
/// </summary>

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

@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections;
using System.Collections.Generic;
using Avalonia.Controls.Templates;
@ -16,7 +15,7 @@ namespace Avalonia.Controls.Generators
/// <summary>
/// Gets the currently realized containers.
/// </summary>
IEnumerable<ItemContainer> Containers { get; }
IEnumerable<ItemContainerInfo> Containers { get; }
/// <summary>
/// Gets or sets the data template used to display the items in the control.
@ -34,28 +33,33 @@ namespace Avalonia.Controls.Generators
event EventHandler<ItemContainerEventArgs> Dematerialized;
/// <summary>
/// Creates container controls for a collection of items.
/// Event raised whenever containers are recycled.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item of the data in the containing collection.
event EventHandler<ItemContainerEventArgs> Recycled;
/// <summary>
/// Creates a container control for an item.
/// </summary>
/// <param name="index">
/// The index of the item of data in the control's items.
/// </param>
/// <param name="items">The items.</param>
/// <param name="item">The item.</param>
/// <param name="selector">An optional member selector.</param>
/// <returns>The created controls.</returns>
IEnumerable<ItemContainer> Materialize(
int startingIndex,
IEnumerable items,
ItemContainerInfo Materialize(
int index,
object item,
IMemberSelector selector);
/// <summary>
/// Removes a set of created containers.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item of the data in the containing collection.
/// The index of the first item in the control's items.
/// </param>
/// <param name="count">The the number of items to remove.</param>
/// <returns>The removed containers.</returns>
IEnumerable<ItemContainer> Dematerialize(int startingIndex, int count);
IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count);
/// <summary>
/// Inserts space for newly inserted containers in the index.
@ -69,17 +73,23 @@ namespace Avalonia.Controls.Generators
/// the gap.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item of the data in the containing collection.
/// The index of the first item in the control's items.
/// </param>
/// <param name="count">The the number of items to remove.</param>
/// <returns>The removed containers.</returns>
IEnumerable<ItemContainer> RemoveRange(int startingIndex, int count);
IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count);
bool TryRecycle(
int oldIndex,
int newIndex,
object item,
IMemberSelector selector);
/// <summary>
/// Clears all created containers and returns the removed controls.
/// </summary>
/// <returns>The removed controls.</returns>
IEnumerable<ItemContainer> Clear();
IEnumerable<ItemContainerInfo> Clear();
/// <summary>
/// Gets the container control representing the item with the specified index.

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

@ -15,13 +15,10 @@ namespace Avalonia.Controls.Generators
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
/// </summary>
/// <param name="startingIndex">The index of the first container in the source items.</param>
/// <param name="container">The container.</param>
public ItemContainerEventArgs(
int startingIndex,
ItemContainer container)
public ItemContainerEventArgs(ItemContainerInfo container)
{
StartingIndex = startingIndex;
StartingIndex = container.Index;
Containers = new[] { container };
}
@ -32,7 +29,7 @@ namespace Avalonia.Controls.Generators
/// <param name="containers">The containers.</param>
public ItemContainerEventArgs(
int startingIndex,
IList<ItemContainer> containers)
IList<ItemContainerInfo> containers)
{
StartingIndex = startingIndex;
Containers = containers;
@ -41,7 +38,7 @@ namespace Avalonia.Controls.Generators
/// <summary>
/// Gets the containers.
/// </summary>
public IList<ItemContainer> Containers { get; }
public IList<ItemContainerInfo> Containers { get; }
/// <summary>
/// Gets the index of the first container in the source items.

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

@ -2,10 +2,9 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
namespace Avalonia.Controls.Generators
@ -15,7 +14,7 @@ namespace Avalonia.Controls.Generators
/// </summary>
public class ItemContainerGenerator : IItemContainerGenerator
{
private List<ItemContainer> _containers = new List<ItemContainer>();
private Dictionary<int, ItemContainerInfo> _containers = new Dictionary<int, ItemContainerInfo>();
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerGenerator"/> class.
@ -29,7 +28,7 @@ namespace Avalonia.Controls.Generators
}
/// <inheritdoc/>
public IEnumerable<ItemContainer> Containers => _containers.Where(x => x != null);
public IEnumerable<ItemContainerInfo> Containers => _containers.Values;
/// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs> Materialized;
@ -37,6 +36,9 @@ namespace Avalonia.Controls.Generators
/// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs> Dematerialized;
/// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs> Recycled;
/// <summary>
/// Gets or sets the data template used to display the items in the control.
/// </summary>
@ -48,41 +50,29 @@ namespace Avalonia.Controls.Generators
public IControl Owner { get; }
/// <inheritdoc/>
public IEnumerable<ItemContainer> Materialize(
int startingIndex,
IEnumerable items,
public ItemContainerInfo Materialize(
int index,
object item,
IMemberSelector selector)
{
Contract.Requires<ArgumentNullException>(items != null);
var i = selector != null ? selector.Select(item) : item;
var container = new ItemContainerInfo(CreateContainer(i), item, index);
int index = startingIndex;
var result = new List<ItemContainer>();
_containers.Add(container.Index, container);
Materialized?.Invoke(this, new ItemContainerEventArgs(container));
foreach (var item in items)
{
var i = selector != null ? selector.Select(item) : item;
var container = new ItemContainer(CreateContainer(i), item, index++);
result.Add(container);
}
AddContainers(result);
Materialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result));
return result.Where(x => x != null).ToList();
return container;
}
/// <inheritdoc/>
public virtual IEnumerable<ItemContainer> Dematerialize(int startingIndex, int count)
public virtual IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count)
{
var result = new List<ItemContainer>();
var result = new List<ItemContainerInfo>();
for (int i = startingIndex; i < startingIndex + count; ++i)
{
if (i < _containers.Count)
{
result.Add(_containers[i]);
_containers[i] = null;
}
result.Add(_containers[i]);
_containers.Remove(i);
}
Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result));
@ -93,18 +83,47 @@ namespace Avalonia.Controls.Generators
/// <inheritdoc/>
public virtual void InsertSpace(int index, int count)
{
_containers.InsertRange(index, Enumerable.Repeat<ItemContainer>(null, count));
if (count > 0)
{
var toMove = _containers.Where(x => x.Key >= index).ToList();
foreach (var i in toMove)
{
_containers.Remove(i.Key);
i.Value.Index += count;
_containers[i.Value.Index] = i.Value;
}
}
}
/// <inheritdoc/>
public virtual IEnumerable<ItemContainer> RemoveRange(int startingIndex, int count)
public virtual IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count)
{
List<ItemContainer> result = new List<ItemContainer>();
var result = new List<ItemContainerInfo>();
if (startingIndex < _containers.Count)
if (count > 0)
{
result.AddRange(_containers.GetRange(startingIndex, count));
_containers.RemoveRange(startingIndex, count);
for (var i = startingIndex; i < startingIndex + count; ++i)
{
ItemContainerInfo found;
if (_containers.TryGetValue(i, out found))
{
result.Add(found);
}
_containers.Remove(i);
}
var toMove = _containers.Where(x => x.Key >= startingIndex).ToList();
foreach (var i in toMove)
{
_containers.Remove(i.Key);
i.Value.Index -= count;
_containers.Add(i.Value.Index, i.Value);
}
Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result));
}
@ -112,10 +131,20 @@ namespace Avalonia.Controls.Generators
}
/// <inheritdoc/>
public virtual IEnumerable<ItemContainer> Clear()
public virtual bool TryRecycle(
int oldIndex,
int newIndex,
object item,
IMemberSelector selector)
{
return false;
}
/// <inheritdoc/>
public virtual IEnumerable<ItemContainerInfo> Clear()
{
var result = _containers.Where(x => x != null).ToList();
_containers = new List<ItemContainer>();
var result = Containers.ToList();
_containers.Clear();
if (result.Count > 0)
{
@ -128,27 +157,20 @@ namespace Avalonia.Controls.Generators
/// <inheritdoc/>
public IControl ContainerFromIndex(int index)
{
if (index < _containers.Count)
{
return _containers[index]?.ContainerControl;
}
return null;
ItemContainerInfo result;
_containers.TryGetValue(index, out result);
return result?.ContainerControl;
}
/// <inheritdoc/>
public int IndexFromContainer(IControl container)
{
var index = 0;
foreach (var i in _containers)
{
if (i?.ContainerControl == container)
if (i.Value.ContainerControl == container)
{
return index;
return i.Key;
}
++index;
}
return -1;
@ -161,44 +183,35 @@ namespace Avalonia.Controls.Generators
/// <returns>The created container control.</returns>
protected virtual IControl CreateContainer(object item)
{
var result = Owner.MaterializeDataTemplate(item, ItemTemplate);
var result = item as IControl;
if (result != null && !(item is IControl))
if (result == null)
{
result.DataContext = item;
result = new ContentPresenter
{
ContentTemplate = ItemTemplate,
Content = item,
};
}
return result;
}
/// <summary>
/// Adds a collection of containers to the index.
/// Moves a container.
/// </summary>
/// <param name="containers">The containers.</param>
protected void AddContainers(IList<ItemContainer> containers)
/// <param name="oldIndex">The old index.</param>
/// <param name="newIndex">The new index.</param>
/// <param name="item">The new item.</param>
/// <returns>The container info.</returns>
protected ItemContainerInfo MoveContainer(int oldIndex, int newIndex, object item)
{
Contract.Requires<ArgumentNullException>(containers != null);
foreach (var c in containers)
{
while (_containers.Count < c.Index)
{
_containers.Add(null);
}
if (_containers.Count == c.Index)
{
_containers.Add(c);
}
else if (_containers[c.Index] == null)
{
_containers[c.Index] = c;
}
else
{
throw new InvalidOperationException("Container already created.");
}
}
var container = _containers[oldIndex];
container.Index = newIndex;
container.Item = item;
_containers.Remove(oldIndex);
_containers.Add(newIndex, container);
return container;
}
/// <summary>
@ -207,9 +220,18 @@ namespace Avalonia.Controls.Generators
/// <param name="index">The first index.</param>
/// <param name="count">The number of elements in the range.</param>
/// <returns>The containers.</returns>
protected IEnumerable<ItemContainer> GetContainerRange(int index, int count)
protected IEnumerable<ItemContainerInfo> GetContainerRange(int index, int count)
{
return _containers.Where(x => x.Key >= index && x.Key <= index + count).Select(x => x.Value);
}
/// <summary>
/// Raises the <see cref="Recycled"/> event.
/// </summary>
/// <param name="e">The event args.</param>
protected void RaiseRecycled(ItemContainerEventArgs e)
{
return _containers.GetRange(index, count);
Recycled?.Invoke(this, e);
}
}
}

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

@ -75,5 +75,34 @@ namespace Avalonia.Controls.Generators
return result;
}
}
/// <inheritdoc/>
public override bool TryRecycle(
int oldIndex,
int newIndex,
object item,
IMemberSelector selector)
{
var container = ContainerFromIndex(oldIndex);
if (container == null)
{
throw new IndexOutOfRangeException("Could not recycle container: not materialized.");
}
var i = selector != null ? selector.Select(item) : item;
container.SetValue(ContentProperty, i);
if (!(item is IControl))
{
container.DataContext = i;
}
var info = MoveContainer(oldIndex, newIndex, i);
RaiseRecycled(new ItemContainerEventArgs(info));
return true;
}
}
}

10
src/Avalonia.Controls/Generators/ItemContainer.cs → src/Avalonia.Controls/Generators/ItemContainerInfo.cs

@ -7,17 +7,17 @@ namespace Avalonia.Controls.Generators
/// Holds information about an item container generated by an
/// <see cref="IItemContainerGenerator"/>.
/// </summary>
public class ItemContainer
public class ItemContainerInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainer"/> class.
/// Initializes a new instance of the <see cref="ItemContainerInfo"/> class.
/// </summary>
/// <param name="container">The container control.</param>
/// <param name="item">The item that the container represents.</param>
/// <param name="index">
/// The index of the item in the <see cref="ItemsControl.Items"/> collection.
/// </param>
public ItemContainer(IControl container, object item, int index)
public ItemContainerInfo(IControl container, object item, int index)
{
ContainerControl = container;
Item = item;
@ -35,11 +35,11 @@ namespace Avalonia.Controls.Generators
/// <summary>
/// Gets the item that the container represents.
/// </summary>
public object Item { get; }
public object Item { get; internal set; }
/// <summary>
/// Gets the index of the item in the <see cref="ItemsControl.Items"/> collection.
/// </summary>
public int Index { get; }
public int Index { get; internal set; }
}
}

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

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

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

@ -47,7 +47,7 @@ namespace Avalonia.Controls.Generators
Materialized?.Invoke(
this,
new ItemContainerEventArgs(0, new ItemContainer(container, item, 0)));
new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0)));
}
/// <summary>
@ -62,14 +62,14 @@ namespace Avalonia.Controls.Generators
Dematerialized?.Invoke(
this,
new ItemContainerEventArgs(0, new ItemContainer(container, item, 0)));
new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0)));
}
/// <summary>
/// Removes a set of containers from the index.
/// </summary>
/// <param name="containers">The item containers.</param>
public void Remove(IEnumerable<ItemContainer> containers)
public void Remove(IEnumerable<ItemContainerInfo> containers)
{
foreach (var container in containers)
{

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

@ -99,25 +99,30 @@ namespace Avalonia.Controls.Generators
}
}
public override IEnumerable<ItemContainer> Clear()
public override IEnumerable<ItemContainerInfo> Clear()
{
var items = base.Clear();
Index.Remove(items);
return items;
}
public override IEnumerable<ItemContainer> Dematerialize(int startingIndex, int count)
public override IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count)
{
Index.Remove(GetContainerRange(startingIndex, count));
return base.Dematerialize(startingIndex, count);
}
public override IEnumerable<ItemContainer> RemoveRange(int startingIndex, int count)
public override IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count)
{
Index.Remove(GetContainerRange(startingIndex, count));
return base.RemoveRange(startingIndex, count);
}
public override bool TryRecycle(int oldIndex, int newIndex, object item, IMemberSelector selector)
{
return false;
}
/// <summary>
/// Gets the data template for the specified item.
/// </summary>

29
src/Avalonia.Controls/IScrollable.cs

@ -0,0 +1,29 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Interface implemented by scrollable controls.
/// </summary>
public interface IScrollable
{
/// <summary>
/// Gets the extent of the scrollable content, in logical units
/// </summary>
Size Extent { get; }
/// <summary>
/// Gets or sets the current scroll offset, in logical units.
/// </summary>
Vector Offset { get; set; }
/// <summary>
/// Gets the size of the viewport, in logical units.
/// </summary>
Size Viewport { get; }
}
}

22
src/Avalonia.Controls/ISetInheritanceParent.cs

@ -0,0 +1,22 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Controls
{
/// <summary>
/// Defines an interface through which a <see cref="Control"/>'s inheritance parent can be set.
/// </summary>
/// <remarks>
/// You should not usually need to use this interface - it is for advanced scenarios only.
/// Additionally, <see cref="ISetLogicalParent"/> also sets the inheritance parent; this
/// interface is only needed where the logical and inheritance parents differ.
/// </remarks>
public interface ISetInheritanceParent
{
/// <summary>
/// Sets the control's inheritance parent.
/// </summary>
/// <param name="parent">The parent.</param>
void SetParent(IAvaloniaObject parent);
}
}

22
src/Avalonia.Controls/IVirtualizingController.cs

@ -0,0 +1,22 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Controls
{
/// <summary>
/// Interface implemented by controls that act as controllers for an
/// <see cref="IVirtualizingPanel"/>.
/// </summary>
public interface IVirtualizingController
{
/// <summary>
/// Called when the <see cref="IVirtualizingPanel"/>'s controls should be updated.
/// </summary>
/// <remarks>
/// The controller should respond to this method being called by either adding
/// children up until <see cref="IVirtualizingPanel.IsFull"/> becomes true or
/// removing <see cref="IVirtualizingPanel.OverflowCount"/> controls.
/// </remarks>
void UpdateControls();
}
}

67
src/Avalonia.Controls/IVirtualizingPanel.cs

@ -0,0 +1,67 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Controls
{
/// <summary>
/// A panel that can be used to virtualize items.
/// </summary>
public interface IVirtualizingPanel : IPanel
{
/// <summary>
/// Gets or sets the controller for the virtualizing panel.
/// </summary>
/// <remarks>
/// A virtualizing controller is responsible for maintaing 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 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; }
}
}

21
src/Avalonia.Controls/ItemVirtualizationMode.cs

@ -0,0 +1,21 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Controls
{
/// <summary>
/// Describes the item virtualization method to use for a list.
/// </summary>
public enum ItemVirtualizationMode
{
/// <summary>
/// Do not virtualize items.
/// </summary>
None,
/// <summary>
/// Virtualize items without smooth scrolling.
/// </summary>
Simple,
}
}

24
src/Avalonia.Controls/ItemsControl.cs

@ -25,7 +25,6 @@ namespace Avalonia.Controls
/// <summary>
/// The default value for the <see cref="ItemsPanel"/> property.
/// </summary>
[SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "Needs to be before or a NullReferenceException is thrown.")]
private static readonly FuncTemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel());
@ -90,6 +89,7 @@ namespace Avalonia.Controls
_itemContainerGenerator.ItemTemplate = ItemTemplate;
_itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e);
_itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e);
_itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e);
}
}
@ -265,6 +265,28 @@ namespace Avalonia.Controls
LogicalChildren.RemoveAll(toRemove);
}
/// <summary>
/// Called when containers are recycled for the <see cref="ItemsControl"/> by its
/// <see cref="ItemContainerGenerator"/>.
/// </summary>
/// <param name="e">The details of the containers.</param>
protected virtual void OnContainersRecycled(ItemContainerEventArgs e)
{
var toRemove = new List<ILogical>();
foreach (var container in e.Containers)
{
// If the item is its own container, then it will be removed from the logical tree
// when it is removed from the Items collection.
if (container?.ContainerControl != container?.Item)
{
toRemove.Add(container.ContainerControl);
}
}
LogicalChildren.RemoveAll(toRemove);
}
/// <inheritdoc/>
protected override void OnTemplateChanged(AvaloniaPropertyChangedEventArgs e)
{

57
src/Avalonia.Controls/ListBox.cs

@ -2,12 +2,11 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections;
using System.Collections.Generic;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace Avalonia.Controls
{
@ -16,6 +15,18 @@ namespace Avalonia.Controls
/// </summary>
public class ListBox : SelectingItemsControl
{
/// <summary>
/// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
/// </summary>
private static readonly FuncTemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new VirtualizingStackPanel());
/// <summary>
/// Defines the <see cref="Scroll"/> property.
/// </summary>
public static readonly DirectProperty<ListBox, IScrollable> ScrollProperty =
AvaloniaProperty.RegisterDirect<ListBox, IScrollable>(nameof(Scroll), o => o.Scroll);
/// <summary>
/// Defines the <see cref="SelectedItems"/> property.
/// </summary>
@ -28,6 +39,31 @@ namespace Avalonia.Controls
public static readonly new AvaloniaProperty<SelectionMode> SelectionModeProperty =
SelectingItemsControl.SelectionModeProperty;
/// <summary>
/// Defines the <see cref="VirtualizationMode"/> property.
/// </summary>
public static readonly AvaloniaProperty<ItemVirtualizationMode> VirtualizationModeProperty =
ItemsPresenter.VirtualizationModeProperty.AddOwner<ListBox>();
private IScrollable _scroll;
/// <summary>
/// Initializes static members of the <see cref="ItemsControl"/> class.
/// </summary>
static ListBox()
{
ItemsPanelProperty.OverrideDefaultValue<ListBox>(DefaultPanel);
}
/// <summary>
/// Gets the scroll information for the <see cref="ListBox"/>.
/// </summary>
public IScrollable Scroll
{
get { return _scroll; }
private set { SetAndRaise(ScrollProperty, ref _scroll, value); }
}
/// <inheritdoc/>
public new IList SelectedItems => base.SelectedItems;
@ -38,6 +74,15 @@ namespace Avalonia.Controls
set { base.SelectionMode = value; }
}
/// <summary>
/// Gets or sets the virtualization mode for the items.
/// </summary>
public ItemVirtualizationMode VirtualizationMode
{
get { return GetValue(VirtualizationModeProperty); }
set { SetValue(VirtualizationModeProperty, value); }
}
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
@ -75,5 +120,11 @@ namespace Avalonia.Controls
(e.InputModifiers & InputModifiers.Control) != 0);
}
}
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
Scroll = e.NameScope.Find<IScrollable>("PART_ScrollViewer");
}
}
}

7
src/Avalonia.Controls/Menu.cs

@ -4,6 +4,7 @@
using System;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
@ -131,6 +132,12 @@ namespace Avalonia.Controls
_subscription.Dispose();
}
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<MenuItem>(this, MenuItem.HeaderProperty, null);
}
/// <summary>
/// Called when a key is pressed within the menu.
/// </summary>

7
src/Avalonia.Controls/MenuItem.cs

@ -4,6 +4,7 @@
using System;
using System.Linq;
using System.Windows.Input;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
@ -204,6 +205,12 @@ namespace Avalonia.Controls
IsSelected = true;
}
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
}
/// <summary>
/// Called when a key is pressed in the <see cref="MenuItem"/>.
/// </summary>

17
src/Avalonia.Controls/Mixins/SelectableMixin.cs

@ -51,22 +51,7 @@ namespace Avalonia.Controls.Mixins
if (sender != null)
{
var itemsControl = sender.Parent as SelectingItemsControl;
if ((bool)x.NewValue)
{
((IPseudoClasses)sender.Classes).Add(":selected");
if (((IVisual)sender).IsAttachedToVisualTree &&
itemsControl?.AutoScrollToSelectedItem == true)
{
sender.BringIntoView();
}
}
else
{
((IPseudoClasses)sender.Classes).Remove(":selected");
}
((IPseudoClasses)sender.Classes).Set(":selected", (bool)x.NewValue);
sender.RaiseEvent(new RoutedEventArgs
{

34
src/Avalonia.Controls/Panel.cs

@ -79,12 +79,28 @@ namespace Avalonia.Controls
set { SetValue(BackgroundProperty, value); }
}
/// <summary>
/// Renders the visual to a <see cref="DrawingContext"/>.
/// </summary>
/// <param name="context">The drawing context.</param>
public override void Render(DrawingContext context)
{
var background = Background;
if (background != null)
{
var renderSize = Bounds.Size;
context.FillRectangle(background, new Rect(renderSize));
}
base.Render(context);
}
/// <summary>
/// Called when the <see cref="Children"/> collection changes.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
private void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e)
protected virtual void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e)
{
List<Control> controls;
@ -122,21 +138,5 @@ namespace Avalonia.Controls
InvalidateMeasure();
}
/// <summary>
/// Renders the visual to a <see cref="DrawingContext"/>.
/// </summary>
/// <param name="context">The drawing context.</param>
public override void Render(DrawingContext context)
{
var background = Background;
if (background != null)
{
var renderSize = Bounds.Size;
context.FillRectangle(background, new Rect(renderSize));
}
base.Render(context);
}
}
}

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

@ -95,9 +95,8 @@ namespace Avalonia.Controls.Presenters
}
/// <inheritdoc/>
protected override void CreatePanel()
protected override void PanelCreated(IPanel panel)
{
base.CreatePanel();
var task = MoveToPage(-1, SelectedIndex);
}
@ -175,12 +174,9 @@ namespace Avalonia.Controls.Presenters
if (container == null)
{
var item = Items.Cast<object>().ElementAt(index);
var materialized = ItemContainerGenerator.Materialize(
index,
new[] { item },
MemberSelector);
container = materialized.First().ContainerControl;
Panel.Children.Add(container);
var materialized = ItemContainerGenerator.Materialize(index, item, MemberSelector);
Panel.Children.Add(materialized.ContainerControl);
container = materialized.ContainerControl;
}
return container;

84
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -80,6 +80,7 @@ namespace Avalonia.Controls.Presenters
private IControl _child;
private bool _createdChild;
private IDataTemplate _dataTemplate;
/// <summary>
/// Initializes static members of the <see cref="ContentPresenter"/> class.
@ -95,10 +96,6 @@ namespace Avalonia.Controls.Presenters
/// </summary>
public ContentPresenter()
{
var dataContext = this.GetObservable(ContentProperty)
.Select(x => x is IControl ? AvaloniaProperty.UnsetValue : x);
Bind(Control.DataContextProperty, dataContext);
}
/// <summary>
@ -200,6 +197,13 @@ namespace Avalonia.Controls.Presenters
}
}
/// <inheritdoc/>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_dataTemplate = null;
}
/// <summary>
/// Updates the <see cref="Child"/> control based on the control's <see cref="Content"/>.
/// </summary>
@ -213,35 +217,73 @@ namespace Avalonia.Controls.Presenters
/// </remarks>
public void UpdateChild()
{
var old = Child;
var content = Content;
var result = this.MaterializeDataTemplate(content, ContentTemplate);
if (old != null)
{
VisualChildren.Remove(old);
}
var oldChild = Child;
var newChild = content as IControl;
if (result != null)
if (content != null && newChild == null)
{
if (!(content is IControl))
// We have content and it isn't a control, so first try to recycle the existing
// child control to display the new data by querying if the template that created
// the child can recycle items and that it also matches the new data.
if (oldChild != null &&
_dataTemplate != null &&
_dataTemplate.SupportsRecycling &&
_dataTemplate.Match(content))
{
result.DataContext = content;
newChild = oldChild;
}
Child = result;
if (result.Parent == null)
else
{
((ISetLogicalParent)result).SetParent((ILogical)this.TemplatedParent ?? this);
// We couldn't recycle an existing control so find a data template for the data
// and use it to create a control.
_dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default;
newChild = _dataTemplate.Build(content);
// Try to give the new control its own name scope.
var controlResult = newChild as Control;
if (controlResult != null)
{
NameScope.SetNameScope(controlResult, new NameScope());
}
}
VisualChildren.Add(result);
}
else
{
_dataTemplate = null;
}
// Remove the old child if we're not recycling it.
if (oldChild != null && newChild != oldChild)
{
VisualChildren.Remove(oldChild);
}
// Set the DataContext if the data isn't a control.
if (!(content is IControl))
{
DataContext = content;
}
// Update the Child.
if (newChild == null)
{
Child = null;
}
else if (newChild != oldChild)
{
((ISetInheritanceParent)newChild).SetParent(this);
Child = newChild;
if (newChild.Parent == null)
{
((ISetLogicalParent)newChild).SetParent((ILogical)this.TemplatedParent ?? this);
}
VisualChildren.Add(newChild);
}
_createdChild = true;
}

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

@ -6,5 +6,7 @@ namespace Avalonia.Controls.Presenters
public interface IItemsPresenter : IPresenter
{
IPanel Panel { get; }
void ScrollIntoView(object item);
}
}

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

@ -0,0 +1,198 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections;
using System.Collections.Specialized;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Input;
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 bool disposedValue;
/// <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();
}
/// <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 => Vertical ? new Size(0, ExtentValue) : new Size(ExtentValue, 0);
/// <summary>
/// Gets the <see cref="ViewportValue"/> as a <see cref="Size"/>.
/// </summary>
public Size Viewport => Vertical ? new Size(0, ViewportValue) : new Size(ViewportValue, 0);
/// <summary>
/// Gets or sets the <see cref="OffsetValue"/> as a <see cref="Vector"/>.
/// </summary>
public Vector Offset
{
get
{
return Vertical ? new Vector(0, OffsetValue) : new Vector(OffsetValue, 0);
}
set
{
OffsetValue = Vertical ? value.Y : value.X;
}
}
/// <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)
{
var virtualizingPanel = owner.Panel as IVirtualizingPanel;
var scrollable = (ILogicalScrollable)owner;
ItemVirtualizer result = null;
if (virtualizingPanel != null && scrollable.InvalidateScroll != null)
{
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;
}
/// <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 IControl GetControlInDirection(NavigationDirection direction, IControl 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();
}
/// <summary>
/// Scrolls the specified item into view.
/// </summary>
/// <param name="item">The item.</param>
public virtual void ScrollIntoView(object item)
{
}
/// <inheritdoc/>
public virtual void Dispose()
{
VirtualizingPanel.Controller = null;
VirtualizingPanel.Children.Clear();
Owner.ItemContainerGenerator.Clear();
}
/// <summary>
/// Invalidates the current scroll.
/// </summary>
protected void InvalidateScroll() => ((ILogicalScrollable)Owner).InvalidateScroll();
}
}

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

@ -0,0 +1,170 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
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);
var generator = Owner.ItemContainerGenerator;
var panel = Owner.Panel;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
if (e.NewStartingIndex + e.NewItems.Count < Items.Count())
{
generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count);
}
AddContainers(e.NewStartingIndex, e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
RemoveContainers(generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count));
break;
case NotifyCollectionChangedAction.Replace:
RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count));
var containers = AddContainers(e.NewStartingIndex, e.NewItems);
var i = e.NewStartingIndex;
foreach (var container in containers)
{
panel.Children[i++] = container.ContainerControl;
}
break;
case NotifyCollectionChangedAction.Move:
// TODO: Handle move in a more efficient manner. At the moment we just
// drop through to Reset to recreate all the containers.
case NotifyCollectionChangedAction.Reset:
RemoveContainers(generator.Clear());
if (Items != null)
{
AddContainers(0, Items);
}
break;
}
Owner.InvalidateMeasure();
}
/// <summary>
/// Scrolls the specified item into view.
/// </summary>
/// <param name="item">The item.</param>
public override void ScrollIntoView(object item)
{
var index = Items.IndexOf(item);
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, Owner.MemberSelector);
if (i.ContainerControl != null)
{
if (i.Index < panel.Children.Count)
{
// TODO: This will insert at the wrong place when there are null items.
panel.Children.Insert(i.Index, i.ContainerControl);
}
else
{
panel.Children.Add(i.ContainerControl);
}
}
result.Add(i);
}
return result;
}
private void RemoveContainers(IEnumerable<ItemContainerInfo> items)
{
var panel = Owner.Panel;
foreach (var i in items)
{
if (i.ContainerControl != null)
{
panel.Children.Remove(i.ContainerControl);
}
}
}
}
}

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

@ -0,0 +1,491 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Utilities;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Handles virtualization in an <see cref="ItemsPresenter"/> for
/// <see cref="ItemVirtualizationMode.Simple"/>.
/// </summary>
internal class ItemVirtualizerSimple : ItemVirtualizer
{
/// <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);
panel.PixelOffset = VirtualizingPanel.ScrollDirection == Orientation.Vertical ?
panel.Children[0].Bounds.Height :
panel.Children[0].Bounds.Width;
}
}
}
}
/// <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;
}
}
/// <inheritdoc/>
public override void UpdateControls()
{
CreateAndRemoveContainers();
InvalidateScroll();
}
/// <inheritdoc/>
public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e)
{
base.ItemsChanged(items, e);
if (items != null)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
if (e.NewStartingIndex >= FirstIndex &&
e.NewStartingIndex + e.NewItems.Count <= NextIndex)
{
CreateAndRemoveContainers();
RecycleContainers();
}
break;
case NotifyCollectionChangedAction.Remove:
if (e.OldStartingIndex >= FirstIndex &&
e.OldStartingIndex + e.OldItems.Count <= NextIndex)
{
RecycleContainersOnRemove();
}
break;
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Replace:
RecycleContainers();
break;
case NotifyCollectionChangedAction.Reset:
RecycleContainersOnRemove();
break;
}
}
else
{
Owner.ItemContainerGenerator.Clear();
VirtualizingPanel.Children.Clear();
}
InvalidateScroll();
}
public override IControl GetControlInDirection(NavigationDirection direction, IControl from)
{
var generator = Owner.ItemContainerGenerator;
var panel = VirtualizingPanel;
var itemIndex = generator.IndexFromContainer(from);
var vertical = VirtualizingPanel.ScrollDirection == Orientation.Vertical;
if (itemIndex == -1)
{
return null;
}
var newItemIndex = -1;
switch (direction)
{
case NavigationDirection.First:
newItemIndex = 0;
break;
case NavigationDirection.Last:
newItemIndex = ItemCount - 1;
break;
case NavigationDirection.Up:
if (vertical)
{
newItemIndex = itemIndex - 1;
}
break;
case NavigationDirection.Down:
if (vertical)
{
newItemIndex = itemIndex + 1;
}
break;
case NavigationDirection.Left:
if (!vertical)
{
newItemIndex = itemIndex - 1;
}
break;
case NavigationDirection.Right:
if (!vertical)
{
newItemIndex = itemIndex + 1;
}
break;
case NavigationDirection.PageUp:
newItemIndex = Math.Max(0, itemIndex - (int)ViewportValue);
break;
case NavigationDirection.PageDown:
newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue);
break;
}
return ScrollIntoView(newItemIndex);
}
/// <inheritdoc/>
public override void ScrollIntoView(object item)
{
var index = Items.IndexOf(item);
if (index != -1)
{
ScrollIntoView(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;
if (!panel.IsFull && Items != null)
{
var memberSelector = Owner.MemberSelector;
var index = NextIndex;
var step = 1;
while (!panel.IsFull)
{
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), memberSelector);
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)
{
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 selector = Owner.MemberSelector;
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, selector))
{
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 recyles 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;
var selector = Owner.MemberSelector;
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;
var containers = panel.Children.GetRange(first, count).ToList();
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, selector))
{
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 IControl ScrollIntoView(int index)
{
var panel = VirtualizingPanel;
var generator = Owner.ItemContainerGenerator;
var newOffset = -1.0;
if (index >= 0 && index < ItemCount)
{
if (index < FirstIndex)
{
newOffset = index;
}
else if (index >= NextIndex)
{
newOffset = index - Math.Ceiling(ViewportValue - 1);
}
else if (OffsetValue + ViewportValue >= ItemCount)
{
newOffset = OffsetValue - 1;
}
if (newOffset != -1)
{
OffsetValue = newOffset;
}
var container = generator.ContainerFromIndex(index);
var layoutManager = LayoutManager.Instance;
// 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 (layoutManager != null)
{
layoutManager.ExecuteLayoutPass();
if (!new Rect(panel.Bounds.Size).Contains(container.Bounds))
{
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);
}
}
}

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

@ -1,19 +1,29 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using System;
using System.Collections.Specialized;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Utils;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using static Avalonia.Utilities.MathUtilities;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Displays items inside an <see cref="ItemsControl"/>.
/// </summary>
public class ItemsPresenter : ItemsPresenterBase
public class ItemsPresenter : ItemsPresenterBase, ILogicalScrollable
{
/// <summary>
/// Defines the <see cref="VirtualizationMode"/> property.
/// </summary>
public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty =
AvaloniaProperty.Register<ItemsPresenter, ItemVirtualizationMode>(
nameof(VirtualizationMode),
defaultValue: ItemVirtualizationMode.Simple);
private ItemVirtualizer _virtualizer;
/// <summary>
/// Initializes static members of the <see cref="ItemsPresenter"/> class.
/// </summary>
@ -22,127 +32,101 @@ namespace Avalonia.Controls.Presenters
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(
typeof(ItemsPresenter),
KeyboardNavigationMode.Once);
VirtualizationModeProperty.Changed
.AddClassHandler<ItemsPresenter>(x => x.VirtualizationModeChanged);
}
/// <inheritdoc/>
protected override void CreatePanel()
/// <summary>
/// Gets or sets the virtualization mode for the items.
/// </summary>
public ItemVirtualizationMode VirtualizationMode
{
base.CreatePanel();
if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty))
{
KeyboardNavigation.SetDirectionalNavigation(
(InputElement)Panel,
KeyboardNavigationMode.Contained);
}
KeyboardNavigation.SetTabNavigation(
(InputElement)Panel,
KeyboardNavigation.GetTabNavigation(this));
get { return GetValue(VirtualizationModeProperty); }
set { SetValue(VirtualizationModeProperty, value); }
}
/// <inheritdoc/>
protected override void ItemsChanged(NotifyCollectionChangedEventArgs e)
bool ILogicalScrollable.IsLogicalScrollEnabled
{
var generator = ItemContainerGenerator;
get { return _virtualizer?.IsLogicalScrollEnabled ?? false; }
}
// TODO: Handle Move and Replace etc.
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
if (e.NewStartingIndex + e.NewItems.Count < Items.Count())
{
generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count);
}
/// <inheritdoc/>
Size IScrollable.Extent => _virtualizer.Extent;
AddContainers(generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector));
break;
/// <inheritdoc/>
Vector IScrollable.Offset
{
get { return _virtualizer.Offset; }
set { _virtualizer.Offset = CoerceOffset(value); }
}
/// <inheritdoc/>
Size IScrollable.Viewport => _virtualizer.Viewport;
case NotifyCollectionChangedAction.Remove:
RemoveContainers(generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count));
break;
/// <inheritdoc/>
Action ILogicalScrollable.InvalidateScroll { get; set; }
case NotifyCollectionChangedAction.Replace:
RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count));
var containers = generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector);
AddContainers(containers);
/// <inheritdoc/>
Size ILogicalScrollable.ScrollSize => new Size(0, 1);
var i = e.NewStartingIndex;
/// <inheritdoc/>
Size ILogicalScrollable.PageScrollSize => new Size(0, 1);
foreach (var container in containers)
{
Panel.Children[i++] = container.ContainerControl;
}
/// <inheritdoc/>
bool ILogicalScrollable.BringIntoView(IControl target, Rect targetRect)
{
return false;
}
break;
/// <inheritdoc/>
IControl ILogicalScrollable.GetControlInDirection(NavigationDirection direction, IControl from)
{
return _virtualizer?.GetControlInDirection(direction, from);
}
case NotifyCollectionChangedAction.Move:
// TODO: Implement Move in a more efficient manner.
case NotifyCollectionChangedAction.Reset:
RemoveContainers(generator.Clear());
public override void ScrollIntoView(object item)
{
_virtualizer?.ScrollIntoView(item);
}
if (Items != null)
{
AddContainers(generator.Materialize(0, Items, MemberSelector));
}
/// <inheritdoc/>
protected override void PanelCreated(IPanel panel)
{
_virtualizer = ItemVirtualizer.Create(this);
((ILogicalScrollable)this).InvalidateScroll?.Invoke();
break;
if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty))
{
KeyboardNavigation.SetDirectionalNavigation(
(InputElement)Panel,
KeyboardNavigationMode.Contained);
}
InvalidateMeasure();
KeyboardNavigation.SetTabNavigation(
(InputElement)Panel,
KeyboardNavigation.GetTabNavigation(this));
}
private void AddContainersToPanel(IEnumerable<ItemContainer> items)
protected override void ItemsChanged(NotifyCollectionChangedEventArgs e)
{
foreach (var i in items)
{
if (i.ContainerControl != null)
{
if (i.Index < this.Panel.Children.Count)
{
// HACK: This will insert at the wrong place when there are null items,
// but all of this will need to be rewritten when we implement
// virtualization so hope no-one notices until then :)
this.Panel.Children.Insert(i.Index, i.ContainerControl);
}
else
{
this.Panel.Children.Add(i.ContainerControl);
}
}
}
_virtualizer?.ItemsChanged(Items, e);
}
private void AddContainers(IEnumerable<ItemContainer> items)
private Vector CoerceOffset(Vector value)
{
foreach (var i in items)
{
if (i.ContainerControl != null)
{
if (i.Index < this.Panel.Children.Count)
{
// HACK: This will insert at the wrong place when there are null items,
// but all of this will need to be rewritten when we implement
// virtualization so hope no-one notices until then :)
this.Panel.Children.Insert(i.Index, i.ContainerControl);
}
else
{
this.Panel.Children.Add(i.ContainerControl);
}
}
}
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));
}
private void RemoveContainers(IEnumerable<ItemContainer> items)
private void VirtualizationModeChanged(AvaloniaPropertyChangedEventArgs e)
{
foreach (var i in items)
{
if (i.ContainerControl != null)
{
this.Panel.Children.Remove(i.ContainerControl);
}
}
_virtualizer?.Dispose();
_virtualizer = ItemVirtualizer.Create(this);
((ILogicalScrollable)this).InvalidateScroll?.Invoke();
}
}
}

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

@ -101,8 +101,7 @@ namespace Avalonia.Controls.Presenters
{
if (_generator == null)
{
var i = TemplatedParent as ItemsControl;
_generator = (i?.ItemContainerGenerator) ?? new ItemContainerGenerator(this);
_generator = CreateItemContainerGenerator();
}
return _generator;
@ -164,6 +163,31 @@ namespace Avalonia.Controls.Presenters
}
}
/// <inheritdoc/>
public virtual void ScrollIntoView(object item)
{
}
/// <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;
}
return result;
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
@ -178,11 +202,26 @@ namespace Avalonia.Controls.Presenters
return finalSize;
}
/// <summary>
/// Called when the <see cref="Panel"/> is created.
/// </summary>
/// <param name="panel">The panel.</param>
protected virtual void PanelCreated(IPanel 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>
protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e);
/// <summary>
/// Creates the <see cref="Panel"/> when <see cref="ApplyTemplate"/> is called for the first
/// time.
/// </summary>
protected virtual void CreatePanel()
private void CreatePanel()
{
Panel = ItemsPanel.Build();
Panel.SetValue(TemplatedParentProperty, TemplatedParent);
@ -201,16 +240,11 @@ namespace Avalonia.Controls.Presenters
incc.CollectionChanged += ItemsCollectionChanged;
}
PanelCreated(Panel);
ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
/// <summary>
/// Called when the items for the presenter change, either because <see cref="Items"/>
/// has been set, or the items collection has been modified.
/// </summary>
/// <param name="e">A description of the change.</param>
protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e);
/// <summary>
/// Called when the <see cref="Items"/> collection changes.
/// </summary>

90
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -15,7 +15,7 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Presents a scrolling view of content inside a <see cref="ScrollViewer"/>.
/// </summary>
public class ScrollContentPresenter : ContentPresenter, IPresenter
public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable
{
/// <summary>
/// Defines the <see cref="Extent"/> property.
@ -50,7 +50,7 @@ namespace Avalonia.Controls.Presenters
private Size _extent;
private Size _measuredExtent;
private Vector _offset;
private IDisposable _scrollableSubscription;
private IDisposable _logicalScrollSubscription;
private Size _viewport;
/// <summary>
@ -59,6 +59,7 @@ namespace Avalonia.Controls.Presenters
static ScrollContentPresenter()
{
ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true);
ChildProperty.Changed.AddClassHandler<ScrollContentPresenter>(x => x.ChildChanged);
AffectsArrange(OffsetProperty);
}
@ -69,7 +70,7 @@ namespace Avalonia.Controls.Presenters
{
AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested);
this.GetObservable(ChildProperty).Subscribe(ChildChanged);
this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription);
}
/// <summary>
@ -117,6 +118,14 @@ namespace Avalonia.Controls.Presenters
return false;
}
var scrollable = Child as ILogicalScrollable;
var control = target as IControl;
if (scrollable?.IsLogicalScrollEnabled == true && control != null)
{
return scrollable.BringIntoView(control, targetRect);
}
var transform = target.TransformToVisual(Child);
if (transform == null)
@ -169,7 +178,7 @@ namespace Avalonia.Controls.Presenters
{
var measureSize = availableSize;
if (_scrollableSubscription == null)
if (_logicalScrollSubscription == null)
{
measureSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
@ -194,21 +203,25 @@ namespace Avalonia.Controls.Presenters
protected override Size ArrangeOverride(Size finalSize)
{
var child = this.GetVisualChildren().SingleOrDefault() as ILayoutable;
var offset = default(Vector);
var logicalScroll = _logicalScrollSubscription != null;
if (_scrollableSubscription == null)
if (!logicalScroll)
{
Viewport = finalSize;
Extent = _measuredExtent;
offset = Offset;
}
if (child != null)
{
var size = new Size(
if (child != null)
{
var size = new Size(
Math.Max(finalSize.Width, child.DesiredSize.Width),
Math.Max(finalSize.Height, child.DesiredSize.Height));
child.Arrange(new Rect((Point)(-offset), size));
child.Arrange(new Rect((Point)(-Offset), size));
return finalSize;
}
}
else if (child != null)
{
child.Arrange(new Rect(finalSize));
return finalSize;
}
@ -220,9 +233,9 @@ namespace Avalonia.Controls.Presenters
{
if (Extent.Height > Viewport.Height)
{
var scrollable = Child as IScrollable;
var scrollable = Child as ILogicalScrollable;
if (scrollable != null)
if (scrollable?.IsLogicalScrollEnabled == true)
{
var y = Offset.Y + (-e.Delta.Y * scrollable.ScrollSize.Height);
y = Math.Max(y, 0);
@ -246,28 +259,53 @@ namespace Avalonia.Controls.Presenters
e.Handled = BringDescendentIntoView(e.TargetObject, e.TargetRect);
}
private void ChildChanged(IControl child)
private void ChildChanged(AvaloniaPropertyChangedEventArgs e)
{
var scrollable = child as IScrollable;
UpdateScrollableSubscription((IControl)e.NewValue);
_scrollableSubscription?.Dispose();
_scrollableSubscription = null;
if (e.OldValue != null)
{
Offset = default(Vector);
}
}
private void UpdateScrollableSubscription(IControl child)
{
var scrollable = child as ILogicalScrollable;
_logicalScrollSubscription?.Dispose();
_logicalScrollSubscription = null;
if (scrollable != null)
{
scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable);
_scrollableSubscription = new CompositeDisposable(
this.GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x),
Disposable.Create(() => scrollable.InvalidateScroll = null));
UpdateFromScrollable(scrollable);
if (scrollable.IsLogicalScrollEnabled == true)
{
_logicalScrollSubscription = new CompositeDisposable(
this.GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x),
Disposable.Create(() => scrollable.InvalidateScroll = null));
UpdateFromScrollable(scrollable);
}
}
}
private void UpdateFromScrollable(IScrollable scrollable)
private void UpdateFromScrollable(ILogicalScrollable scrollable)
{
Viewport = scrollable.Viewport;
Extent = scrollable.Extent;
Offset = scrollable.Offset;
var logicalScroll = _logicalScrollSubscription != null;
if (logicalScroll != scrollable.IsLogicalScrollEnabled)
{
UpdateScrollableSubscription(Child);
Offset = default(Vector);
InvalidateMeasure();
}
else if (scrollable.IsLogicalScrollEnabled)
{
Viewport = scrollable.Viewport;
Extent = scrollable.Extent;
Offset = scrollable.Offset;
}
}
}
}

69
src/Avalonia.Controls/Primitives/ILogicalScrollable.cs

@ -0,0 +1,69 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Input;
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Interface implemented by controls that handle their own scrolling when placed inside a
/// <see cref="ScrollViewer"/>.
/// </summary>
/// <remarks>
/// Controls that implement this interface, when placed inside a <see cref="ScrollViewer"/>
/// can override the physical scrolling behavior of the scroll viewer with logical scrolling.
/// Physical scrolling means that the scroll viewer is a simple viewport onto a larger canvas
/// whereas logical scrolling means that the scrolling is handled by the child control itself
/// and it can choose to do handle the scroll information as it sees fit.
/// </remarks>
public interface ILogicalScrollable : IScrollable
{
/// <summary>
/// Gets a value indicating whether logical scrolling is enabled on the control.
/// </summary>
bool IsLogicalScrollEnabled { get; }
/// <summary>
/// Gets or sets the scroll invalidation method.
/// </summary>
/// <remarks>
/// <para>
/// This method notifies the attached <see cref="ScrollViewer"/> of a change in
/// the <see cref="IScrollable.Extent"/>, <see cref="IScrollable.Offset"/> or
/// <see cref="IScrollable.Viewport"/> properties.
/// </para>
/// <para>
/// This property is set by the parent <see cref="ScrollViewer"/> when the
/// <see cref="ILogicalScrollable"/> is placed inside it.
/// </para>
/// </remarks>
Action InvalidateScroll { get; set; }
/// <summary>
/// Gets the size to scroll by, in logical units.
/// </summary>
Size ScrollSize { get; }
/// <summary>
/// Gets the size to page by, in logical units.
/// </summary>
Size PageScrollSize { get; }
/// <summary>
/// Attempts to bring a portion of the target visual into view by scrolling the content.
/// </summary>
/// <param name="target">The target visual.</param>
/// <param name="targetRect">The portion of the target visual to bring into view.</param>
/// <returns>True if the scroll offset was changed; otherwise false.</returns>
bool BringIntoView(IControl target, Rect targetRect);
/// <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>
IControl GetControlInDirection(NavigationDirection direction, IControl from);
}
}

54
src/Avalonia.Controls/Primitives/IScrollable.cs

@ -1,54 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Interface implemented by controls that handle their own scrolling when placed inside a
/// <see cref="ScrollViewer"/>.
/// </summary>
public interface IScrollable
{
/// <summary>
/// Gets or sets the scroll invalidation method.
/// </summary>
/// <remarks>
/// <para>
/// This method notifies the attached <see cref="ScrollViewer"/> of a change in
/// the <see cref="Extent"/>, <see cref="Offset"/> or <see cref="Viewport"/> properties.
/// </para>
/// <para>
/// This property is set by the parent <see cref="ScrollViewer"/> when the
/// <see cref="IScrollable"/> is placed inside it.
/// </para>
/// </remarks>
Action InvalidateScroll { get; set; }
/// <summary>
/// Gets the extent of the scrollable content, in logical units
/// </summary>
Size Extent { get; }
/// <summary>
/// Gets or sets the current scroll offset, in logical units.
/// </summary>
Vector Offset { get; set; }
/// <summary>
/// Gets the size of the viewport, in logical units.
/// </summary>
Size Viewport { get; }
/// <summary>
/// Gets the size to scroll by, in logical units.
/// </summary>
Size ScrollSize { get; }
/// <summary>
/// Gets the size to page by, in logical units.
/// </summary>
Size PageScrollSize { get; }
}
}

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

@ -280,6 +280,12 @@ namespace Avalonia.Controls.Primitives
}
}
/// <summary>
/// Scrolls the specified item into view.
/// </summary>
/// <param name="item">The item.</param>
public void ScrollIntoView(object item) => Presenter?.ScrollIntoView(item);
/// <summary>
/// Tries to get the container that was the source of an event.
/// </summary>
@ -394,6 +400,19 @@ namespace Avalonia.Controls.Primitives
}
}
protected override void OnContainersRecycled(ItemContainerEventArgs e)
{
foreach (var i in e.Containers)
{
if (i.ContainerControl != null && i.Item != null)
{
MarkContainerSelected(
i.ContainerControl,
SelectedItems.Contains(i.Item));
}
}
}
/// <inheritdoc/>
protected override void OnDataContextChanging()
{
@ -710,6 +729,12 @@ namespace Avalonia.Controls.Primitives
{
case NotifyCollectionChangedAction.Add:
SelectedItemsAdded(e.NewItems.Cast<object>().ToList());
if (AutoScrollToSelectedItem)
{
ScrollIntoView(e.NewItems[0]);
}
added = e.NewItems;
break;

2
src/Avalonia.Controls/ScrollViewer.cs

@ -12,7 +12,7 @@ namespace Avalonia.Controls
/// <summary>
/// A control scrolls its content if the content is bigger than the space available.
/// </summary>
public class ScrollViewer : ContentControl
public class ScrollViewer : ContentControl, IScrollable
{
/// <summary>
/// Defines the <see cref="CanScrollHorizontally"/> property.

51
src/Avalonia.Controls/StackPanel.cs

@ -72,37 +72,52 @@ namespace Avalonia.Controls
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <returns>The control.</returns>
IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from)
IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from)
{
var fromControl = from as IControl;
return (fromControl != null) ? GetControlInDirection(direction, fromControl) : null;
}
/// <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>
protected virtual IInputElement GetControlInDirection(NavigationDirection direction, IControl from)
{
var horiz = Orientation == Orientation.Horizontal;
int index = Children.IndexOf((IControl)from);
switch (direction)
{
case FocusNavigationDirection.First:
case NavigationDirection.First:
index = 0;
break;
case FocusNavigationDirection.Last:
case NavigationDirection.Last:
index = Children.Count - 1;
break;
case FocusNavigationDirection.Next:
case NavigationDirection.Next:
++index;
break;
case FocusNavigationDirection.Previous:
case NavigationDirection.Previous:
--index;
break;
case FocusNavigationDirection.Left:
case NavigationDirection.Left:
index = horiz ? index - 1 : -1;
break;
case FocusNavigationDirection.Right:
case NavigationDirection.Right:
index = horiz ? index + 1 : -1;
break;
case FocusNavigationDirection.Up:
case NavigationDirection.Up:
index = horiz ? -1 : index - 1;
break;
case FocusNavigationDirection.Down:
case NavigationDirection.Down:
index = horiz ? -1 : index + 1;
break;
default:
index = -1;
break;
}
if (index >= 0 && index < Children.Count)
@ -181,6 +196,7 @@ namespace Avalonia.Controls
/// <returns>The space taken.</returns>
protected override Size ArrangeOverride(Size finalSize)
{
var orientation = Orientation;
double arrangedWidth = finalSize.Width;
double arrangedHeight = finalSize.Height;
double gap = Gap;
@ -199,11 +215,11 @@ namespace Avalonia.Controls
double childWidth = child.DesiredSize.Width;
double childHeight = child.DesiredSize.Height;
if (Orientation == Orientation.Vertical)
if (orientation == Orientation.Vertical)
{
double width = Math.Max(childWidth, arrangedWidth);
Rect childFinal = new Rect(0, arrangedHeight, width, childHeight);
child.Arrange(childFinal);
ArrangeChild(child, childFinal, finalSize, orientation);
arrangedWidth = Math.Max(arrangedWidth, childWidth);
arrangedHeight += childHeight + gap;
}
@ -211,13 +227,13 @@ namespace Avalonia.Controls
{
double height = Math.Max(childHeight, arrangedHeight);
Rect childFinal = new Rect(arrangedWidth, 0, childWidth, height);
child.Arrange(childFinal);
ArrangeChild(child, childFinal, finalSize, orientation);
arrangedWidth += childWidth + gap;
arrangedHeight = Math.Max(arrangedHeight, childHeight);
}
}
if (Orientation == Orientation.Vertical)
if (orientation == Orientation.Vertical)
{
arrangedHeight = Math.Max(arrangedHeight - gap, finalSize.Height);
}
@ -228,5 +244,14 @@ namespace Avalonia.Controls
return new Size(arrangedWidth, arrangedHeight);
}
internal virtual void ArrangeChild(
IControl child,
Rect rect,
Size panelSize,
Orientation orientation)
{
child.Arrange(rect);
}
}
}

48
src/Avalonia.Controls/Templates/DataTemplateExtensions.cs

@ -11,54 +11,6 @@ namespace Avalonia.Controls.Templates
/// </summary>
public static class DataTemplateExtensions
{
/// <summary>
/// Materializes a piece of data based on a data template.
/// </summary>
/// <param name="control">The control materializing the data template.</param>
/// <param name="data">The data.</param>
/// <param name="primary">
/// An optional primary template that can will be tried before the
/// <see cref="IControl.DataTemplates"/> in the tree are searched.
/// </param>
/// <returns>The data materialized as a control.</returns>
public static IControl MaterializeDataTemplate(
this IControl control,
object data,
IDataTemplate primary = null)
{
if (data == null)
{
return null;
}
else
{
var asControl = data as IControl;
if (asControl != null)
{
return asControl;
}
else
{
IDataTemplate template = control.FindDataTemplate(data, primary);
IControl result;
if (template != null)
{
result = template.Build(data);
}
else
{
result = FuncDataTemplate.Default.Build(data);
}
NameScope.SetNameScope((Control)result, new NameScope());
return result;
}
}
}
/// <summary>
/// Find a data template that matches a piece of data.
/// </summary>

39
src/Avalonia.Controls/Templates/FuncDataTemplate.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Linq;
using System.Reflection;
namespace Avalonia.Controls.Templates
@ -12,10 +13,26 @@ namespace Avalonia.Controls.Templates
public class FuncDataTemplate : FuncTemplate<object, IControl>, IDataTemplate
{
/// <summary>
/// The default data template used in the case where not matching data template is found.
/// The default data template used in the case where no matching data template is found.
/// </summary>
public static readonly FuncDataTemplate Default =
new FuncDataTemplate(typeof(object), o => (o != null) ? new TextBlock { Text = o.ToString() } : null);
new FuncDataTemplate<object>(
data =>
{
if (data != null)
{
var result = new TextBlock();
result.Bind(
TextBlock.TextProperty,
result.GetObservable(Control.DataContextProperty).Select(x => x?.ToString()));
return result;
}
else
{
return null;
}
},
true);
/// <summary>
/// The implementation of the <see cref="Match"/> method.
@ -29,8 +46,12 @@ namespace Avalonia.Controls.Templates
/// <param name="build">
/// A function which when passed an object of <paramref name="type"/> returns a control.
/// </param>
public FuncDataTemplate(Type type, Func<object, IControl> build)
: this(o => IsInstance(o, type), build)
/// <param name="supportsRecycling">Whether the control can be recycled.</param>
public FuncDataTemplate(
Type type,
Func<object, IControl> build,
bool supportsRecycling = false)
: this(o => IsInstance(o, type), build, supportsRecycling)
{
}
@ -43,14 +64,22 @@ namespace Avalonia.Controls.Templates
/// <param name="build">
/// A function which returns a control for matching data.
/// </param>
public FuncDataTemplate(Func<object, bool> match, Func<object, IControl> build)
/// <param name="supportsRecycling">Whether the control can be recycled.</param>
public FuncDataTemplate(
Func<object, bool> match,
Func<object, IControl> build,
bool supportsRecycling = false)
: base(build)
{
Contract.Requires<ArgumentNullException>(match != null);
_match = match;
SupportsRecycling = supportsRecycling;
}
/// <inheritdoc/>
public bool SupportsRecycling { get; }
/// <summary>
/// Checks to see if this data template matches the specified data.
/// </summary>

13
src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs

@ -17,8 +17,9 @@ namespace Avalonia.Controls.Templates
/// <param name="build">
/// A function which when passed an object of <typeparamref name="T"/> returns a control.
/// </param>
public FuncDataTemplate(Func<T, IControl> build)
: base(typeof(T), CastBuild(build))
/// <param name="supportsRecycling">Whether the control can be recycled.</param>
public FuncDataTemplate(Func<T, IControl> build, bool supportsRecycling = false)
: base(typeof(T), CastBuild(build), supportsRecycling)
{
}
@ -31,8 +32,12 @@ namespace Avalonia.Controls.Templates
/// <param name="build">
/// A function which when passed an object of <typeparamref name="T"/> returns a control.
/// </param>
public FuncDataTemplate(Func<T, bool> match, Func<T, IControl> build)
: base(CastMatch(match), CastBuild(build))
/// <param name="supportsRecycling">Whether the control can be recycled.</param>
public FuncDataTemplate(
Func<T, bool> match,
Func<T, IControl> build,
bool supportsRecycling = false)
: base(CastMatch(match), CastBuild(build), supportsRecycling)
{
}

6
src/Avalonia.Controls/Templates/IDataTemplate.cs

@ -8,6 +8,12 @@ namespace Avalonia.Controls.Templates
/// </summary>
public interface IDataTemplate : ITemplate<object, IControl>
{
/// <summary>
/// Gets a value indicating whether the data template supports recycling of the generated
/// control.
/// </summary>
bool SupportsRecycling { get; }
/// <summary>
/// Checks to see if this data template matches the specified data.
/// </summary>

19
src/Avalonia.Controls/Utils/IEnumerableUtils.cs

@ -17,17 +17,22 @@ namespace Avalonia.Controls.Utils
public static int Count(this IEnumerable items)
{
Contract.Requires<ArgumentNullException>(items != null);
var collection = items as ICollection;
if (collection != null)
if (items != null)
{
return collection.Count;
var collection = items as ICollection;
if (collection != null)
{
return collection.Count;
}
else
{
return Enumerable.Count(items.Cast<object>());
}
}
else
{
return Enumerable.Count(items.Cast<object>());
return 0;
}
}

229
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -0,0 +1,229 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Specialized;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
public class VirtualizingStackPanel : StackPanel, IVirtualizingPanel
{
private Size _availableSpace;
private double _takenSpace;
private int _canBeRemoved;
private double _averageItemSize;
private int _averageCount;
private double _pixelOffset;
bool IVirtualizingPanel.IsFull
{
get
{
return Orientation == Orientation.Horizontal ?
_takenSpace >= _availableSpace.Width :
_takenSpace >= _availableSpace.Height;
}
}
IVirtualizingController IVirtualizingPanel.Controller { get; set; }
int IVirtualizingPanel.OverflowCount => _canBeRemoved;
Orientation IVirtualizingPanel.ScrollDirection => Orientation;
double IVirtualizingPanel.AverageItemSize => _averageItemSize;
double IVirtualizingPanel.PixelOverflow
{
get
{
var bounds = Orientation == Orientation.Horizontal ?
_availableSpace.Width : _availableSpace.Height;
return Math.Max(0, _takenSpace - bounds);
}
}
double IVirtualizingPanel.PixelOffset
{
get { return _pixelOffset; }
set
{
if (_pixelOffset != value)
{
_pixelOffset = value;
InvalidateArrange();
}
}
}
private IVirtualizingController Controller => ((IVirtualizingPanel)this).Controller;
protected override Size MeasureOverride(Size availableSize)
{
if (availableSize != ((ILayoutable)this).PreviousMeasure)
{
// TODO: We need to put a reasonable limit on this, probably based on the max
// window size.
_availableSpace = availableSize;
Controller?.UpdateControls();
}
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
_availableSpace = finalSize;
_canBeRemoved = 0;
_takenSpace = 0;
_averageItemSize = 0;
_averageCount = 0;
var result = base.ArrangeOverride(finalSize);
_takenSpace += _pixelOffset;
Controller?.UpdateControls();
return result;
}
protected override void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e)
{
base.ChildrenChanged(sender, e);
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (IControl control in e.NewItems)
{
UpdateAdd(control);
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (IControl control in e.OldItems)
{
UpdateRemove(control);
}
break;
}
}
protected override IInputElement GetControlInDirection(NavigationDirection direction, IControl from)
{
var logicalScrollable = Parent as ILogicalScrollable;
var fromControl = from as IControl;
if (logicalScrollable?.IsLogicalScrollEnabled == true && fromControl != null)
{
return logicalScrollable.GetControlInDirection(direction, fromControl);
}
else
{
return base.GetControlInDirection(direction, from);
}
}
internal override void ArrangeChild(
IControl child,
Rect rect,
Size panelSize,
Orientation orientation)
{
if (orientation == Orientation.Vertical)
{
rect = new Rect(rect.X, 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, rect.Width, rect.Height);
child.Arrange(rect);
if (rect.X >= _availableSpace.Width)
{
++_canBeRemoved;
}
if (rect.Right >= _takenSpace)
{
_takenSpace = rect.Right;
}
AddToAverageItemSize(rect.Width);
}
}
private void UpdateAdd(IControl child)
{
var bounds = Bounds;
var gap = Gap;
child.Measure(_availableSpace);
++_averageCount;
if (Orientation == Orientation.Vertical)
{
var height = child.DesiredSize.Height;
_takenSpace += height + gap;
AddToAverageItemSize(height);
}
else
{
var width = child.DesiredSize.Width;
_takenSpace += width + gap;
AddToAverageItemSize(width);
}
}
private void UpdateRemove(IControl child)
{
var bounds = Bounds;
var gap = Gap;
if (Orientation == Orientation.Vertical)
{
var height = child.DesiredSize.Height;
_takenSpace -= height + gap;
RemoveFromAverageItemSize(height);
}
else
{
var width = child.DesiredSize.Width;
_takenSpace -= width + gap;
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;
}
}
}

18
src/Avalonia.Controls/WrapPanel.cs

@ -48,35 +48,35 @@ namespace Avalonia.Controls
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <returns>The control.</returns>
IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from)
IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from)
{
var horiz = Orientation == Orientation.Horizontal;
int index = Children.IndexOf((IControl)from);
switch (direction)
{
case FocusNavigationDirection.First:
case NavigationDirection.First:
index = 0;
break;
case FocusNavigationDirection.Last:
case NavigationDirection.Last:
index = Children.Count - 1;
break;
case FocusNavigationDirection.Next:
case NavigationDirection.Next:
++index;
break;
case FocusNavigationDirection.Previous:
case NavigationDirection.Previous:
--index;
break;
case FocusNavigationDirection.Left:
case NavigationDirection.Left:
index = horiz ? index - 1 : -1;
break;
case FocusNavigationDirection.Right:
case NavigationDirection.Right:
index = horiz ? index + 1 : -1;
break;
case FocusNavigationDirection.Up:
case NavigationDirection.Up:
index = horiz ? -1 : index - 1;
break;
case FocusNavigationDirection.Down:
case NavigationDirection.Down:
index = horiz ? -1 : index + 1;
break;
}

2
src/Avalonia.Diagnostics/ViewLocator.cs

@ -9,6 +9,8 @@ namespace Avalonia.Diagnostics
{
public class ViewLocator<TViewModel> : IDataTemplate
{
public bool SupportsRecycling => false;
public IControl Build(object data)
{
var name = data.GetType().FullName.Replace("ViewModel", "View");

4
src/Avalonia.Input/Avalonia.Input.csproj

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
@ -79,7 +79,7 @@
<Compile Include="IMainMenu.cs" />
<Compile Include="IAccessKeyHandler.cs" />
<Compile Include="FocusManager.cs" />
<Compile Include="FocusNavigationDirection.cs" />
<Compile Include="NavigationDirection.cs" />
<Compile Include="ICloseable.cs" />
<Compile Include="IFocusManager.cs" />
<Compile Include="IFocusScope.cs" />

2
src/Avalonia.Input/IKeyboardNavigationHandler.cs

@ -25,7 +25,7 @@ namespace Avalonia.Input
/// <param name="modifiers">Any input modifiers active at the time of focus.</param>
void Move(
IInputElement element,
FocusNavigationDirection direction,
NavigationDirection direction,
InputModifiers modifiers = InputModifiers.None);
}
}

2
src/Avalonia.Input/INavigableContainer.cs

@ -14,6 +14,6 @@ namespace Avalonia.Input
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <returns>The control.</returns>
IInputElement GetControl(FocusNavigationDirection direction, IInputElement from);
IInputElement GetControl(NavigationDirection direction, IInputElement from);
}
}

34
src/Avalonia.Input/KeyboardNavigationHandler.cs

@ -48,11 +48,11 @@ namespace Avalonia.Input
/// </returns>
public static IInputElement GetNext(
IInputElement element,
FocusNavigationDirection direction)
NavigationDirection direction)
{
Contract.Requires<ArgumentNullException>(element != null);
if (direction == FocusNavigationDirection.Next || direction == FocusNavigationDirection.Previous)
if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
{
return TabNavigation.GetNextInTabOrder(element, direction);
}
@ -70,7 +70,7 @@ namespace Avalonia.Input
/// <param name="modifiers">Any input modifiers active at the time of focus.</param>
public void Move(
IInputElement element,
FocusNavigationDirection direction,
NavigationDirection direction,
InputModifiers modifiers = InputModifiers.None)
{
Contract.Requires<ArgumentNullException>(element != null);
@ -79,8 +79,8 @@ namespace Avalonia.Input
if (next != null)
{
var method = direction == FocusNavigationDirection.Next ||
direction == FocusNavigationDirection.Previous ?
var method = direction == NavigationDirection.Next ||
direction == NavigationDirection.Previous ?
NavigationMethod.Tab : NavigationMethod.Directional;
FocusManager.Instance.Focus(next, method, modifiers);
}
@ -97,25 +97,37 @@ namespace Avalonia.Input
if (current != null)
{
FocusNavigationDirection? direction = null;
NavigationDirection? direction = null;
switch (e.Key)
{
case Key.Tab:
direction = (e.Modifiers & InputModifiers.Shift) == 0 ?
FocusNavigationDirection.Next : FocusNavigationDirection.Previous;
NavigationDirection.Next : NavigationDirection.Previous;
break;
case Key.Up:
direction = FocusNavigationDirection.Up;
direction = NavigationDirection.Up;
break;
case Key.Down:
direction = FocusNavigationDirection.Down;
direction = NavigationDirection.Down;
break;
case Key.Left:
direction = FocusNavigationDirection.Left;
direction = NavigationDirection.Left;
break;
case Key.Right:
direction = FocusNavigationDirection.Right;
direction = NavigationDirection.Right;
break;
case Key.PageUp:
direction = NavigationDirection.PageUp;
break;
case Key.PageDown:
direction = NavigationDirection.PageDown;
break;
case Key.Home:
direction = NavigationDirection.First;
break;
case Key.End:
direction = NavigationDirection.Last;
break;
}

27
src/Avalonia.Input/Navigation/DirectionalNavigation.cs

@ -24,18 +24,17 @@ namespace Avalonia.Input.Navigation
/// </returns>
public static IInputElement GetNext(
IInputElement element,
FocusNavigationDirection direction)
NavigationDirection direction)
{
Contract.Requires<ArgumentNullException>(element != null);
Contract.Requires<ArgumentException>(
direction != FocusNavigationDirection.Next &&
direction != FocusNavigationDirection.Previous);
direction != NavigationDirection.Next &&
direction != NavigationDirection.Previous);
var container = element.GetVisualParent<IInputElement>();
if (container != null)
{
var isForward = IsForward(direction);
var mode = KeyboardNavigation.GetDirectionalNavigation((InputElement)container);
switch (mode)
@ -63,12 +62,12 @@ namespace Avalonia.Input.Navigation
/// </summary>
/// <param name="direction">The direction.</param>
/// <returns>True if the direction is forward.</returns>
private static bool IsForward(FocusNavigationDirection direction)
private static bool IsForward(NavigationDirection direction)
{
return direction == FocusNavigationDirection.Next ||
direction == FocusNavigationDirection.Last ||
direction == FocusNavigationDirection.Right ||
direction == FocusNavigationDirection.Down;
return direction == NavigationDirection.Next ||
direction == NavigationDirection.Last ||
direction == NavigationDirection.Right ||
direction == NavigationDirection.Down;
}
/// <summary>
@ -77,7 +76,7 @@ namespace Avalonia.Input.Navigation
/// <param name="container">The element.</param>
/// <param name="direction">The direction to search.</param>
/// <returns>The element or null if not found.##</returns>
private static IInputElement GetFocusableDescendent(IInputElement container, FocusNavigationDirection direction)
private static IInputElement GetFocusableDescendent(IInputElement container, NavigationDirection direction)
{
return IsForward(direction) ?
GetFocusableDescendents(container).FirstOrDefault() :
@ -121,9 +120,9 @@ namespace Avalonia.Input.Navigation
private static IInputElement GetNextInContainer(
IInputElement element,
IInputElement container,
FocusNavigationDirection direction)
NavigationDirection direction)
{
if (direction == FocusNavigationDirection.Down)
if (direction == NavigationDirection.Down)
{
var descendent = GetFocusableDescendents(element).FirstOrDefault();
@ -156,7 +155,7 @@ namespace Avalonia.Input.Navigation
element = null;
}
if (element != null && direction == FocusNavigationDirection.Up)
if (element != null && direction == NavigationDirection.Up)
{
var descendent = GetFocusableDescendents(element).LastOrDefault();
@ -180,7 +179,7 @@ namespace Avalonia.Input.Navigation
/// <returns>The first element, or null if there are no more elements.</returns>
private static IInputElement GetFirstInNextContainer(
IInputElement container,
FocusNavigationDirection direction)
NavigationDirection direction)
{
var parent = container.GetVisualParent<IInputElement>();
var isForward = IsForward(direction);

26
src/Avalonia.Input/Navigation/TabNavigation.cs

@ -24,12 +24,12 @@ namespace Avalonia.Input.Navigation
/// </returns>
public static IInputElement GetNextInTabOrder(
IInputElement element,
FocusNavigationDirection direction)
NavigationDirection direction)
{
Contract.Requires<ArgumentNullException>(element != null);
Contract.Requires<ArgumentException>(
direction == FocusNavigationDirection.Next ||
direction == FocusNavigationDirection.Previous);
direction == NavigationDirection.Next ||
direction == NavigationDirection.Previous);
var container = element.GetVisualParent<IInputElement>();
@ -63,9 +63,9 @@ namespace Avalonia.Input.Navigation
/// <param name="container">The element.</param>
/// <param name="direction">The direction to search.</param>
/// <returns>The element or null if not found.##</returns>
private static IInputElement GetFocusableDescendent(IInputElement container, FocusNavigationDirection direction)
private static IInputElement GetFocusableDescendent(IInputElement container, NavigationDirection direction)
{
return direction == FocusNavigationDirection.Next ?
return direction == NavigationDirection.Next ?
GetFocusableDescendents(container).FirstOrDefault() :
GetFocusableDescendents(container).LastOrDefault();
}
@ -128,9 +128,9 @@ namespace Avalonia.Input.Navigation
private static IInputElement GetNextInContainer(
IInputElement element,
IInputElement container,
FocusNavigationDirection direction)
NavigationDirection direction)
{
if (direction == FocusNavigationDirection.Next)
if (direction == NavigationDirection.Next)
{
var descendent = GetFocusableDescendents(element).FirstOrDefault();
@ -165,7 +165,7 @@ namespace Avalonia.Input.Navigation
element = null;
}
if (element != null && direction == FocusNavigationDirection.Previous)
if (element != null && direction == NavigationDirection.Previous)
{
var descendent = GetFocusableDescendents(element).LastOrDefault();
@ -189,14 +189,14 @@ namespace Avalonia.Input.Navigation
/// <returns>The first element, or null if there are no more elements.</returns>
private static IInputElement GetFirstInNextContainer(
IInputElement container,
FocusNavigationDirection direction)
NavigationDirection direction)
{
var parent = container.GetVisualParent<IInputElement>();
IInputElement next = null;
if (parent != null)
{
if (direction == FocusNavigationDirection.Previous && parent.CanFocus())
if (direction == NavigationDirection.Previous && parent.CanFocus())
{
return parent;
}
@ -204,7 +204,7 @@ namespace Avalonia.Input.Navigation
var siblings = parent.GetVisualChildren()
.OfType<IInputElement>()
.Where(FocusExtensions.CanFocusDescendents);
var sibling = direction == FocusNavigationDirection.Next ?
var sibling = direction == NavigationDirection.Next ?
siblings.SkipWhile(x => x != container).Skip(1).FirstOrDefault() :
siblings.TakeWhile(x => x != container).LastOrDefault();
@ -216,7 +216,7 @@ namespace Avalonia.Input.Navigation
}
else
{
next = direction == FocusNavigationDirection.Next ?
next = direction == NavigationDirection.Next ?
GetFocusableDescendents(sibling).FirstOrDefault() :
GetFocusableDescendents(sibling).LastOrDefault();
}
@ -229,7 +229,7 @@ namespace Avalonia.Input.Navigation
}
else
{
next = direction == FocusNavigationDirection.Next ?
next = direction == NavigationDirection.Next ?
GetFocusableDescendents(container).FirstOrDefault() :
GetFocusableDescendents(container).LastOrDefault();
}

14
src/Avalonia.Input/FocusNavigationDirection.cs → src/Avalonia.Input/NavigationDirection.cs

@ -4,9 +4,9 @@
namespace Avalonia.Input
{
/// <summary>
/// Describes how focus should be moved.
/// Describes how focus should be moved by directional or tab keys.
/// </summary>
public enum FocusNavigationDirection
public enum NavigationDirection
{
/// <summary>
/// Move the focus to the next control in the tab order.
@ -47,5 +47,15 @@ namespace Avalonia.Input
/// Move the focus down.
/// </summary>
Down,
/// <summary>
/// Move the focus up a page.
/// </summary>
PageUp,
/// <summary>
/// Move the focus down a page.
/// </summary>
PageDown,
}
}

35
src/Avalonia.Layout/LayoutManager.cs

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Logging;
using Avalonia.Threading;
@ -13,8 +14,8 @@ namespace Avalonia.Layout
/// </summary>
public class LayoutManager : ILayoutManager
{
private readonly Queue<ILayoutable> _toMeasure = new Queue<ILayoutable>();
private readonly Queue<ILayoutable> _toArrange = new Queue<ILayoutable>();
private readonly HashSet<ILayoutable> _toMeasure = new HashSet<ILayoutable>();
private readonly HashSet<ILayoutable> _toArrange = new HashSet<ILayoutable>();
private bool _queued;
private bool _running;
@ -29,8 +30,8 @@ namespace Avalonia.Layout
Contract.Requires<ArgumentNullException>(control != null);
Dispatcher.UIThread.VerifyAccess();
_toMeasure.Enqueue(control);
_toArrange.Enqueue(control);
_toMeasure.Add(control);
_toArrange.Add(control);
QueueLayoutPass();
}
@ -40,7 +41,7 @@ namespace Avalonia.Layout
Contract.Requires<ArgumentNullException>(control != null);
Dispatcher.UIThread.VerifyAccess();
_toArrange.Enqueue(control);
_toArrange.Add(control);
QueueLayoutPass();
}
@ -107,7 +108,7 @@ namespace Avalonia.Layout
{
while (_toMeasure.Count > 0)
{
var next = _toMeasure.Dequeue();
var next = _toMeasure.First();
Measure(next);
}
}
@ -116,7 +117,7 @@ namespace Avalonia.Layout
{
while (_toArrange.Count > 0 && _toMeasure.Count == 0)
{
var next = _toArrange.Dequeue();
var next = _toArrange.First();
Arrange(next);
}
}
@ -124,29 +125,45 @@ namespace Avalonia.Layout
private void Measure(ILayoutable control)
{
var root = control as ILayoutRoot;
var parent = control.VisualParent as ILayoutable;
if (root != null)
{
root.Measure(root.MaxClientSize);
}
else if (control.PreviousMeasure.HasValue)
else if (parent != null)
{
Measure(parent);
}
if (!control.IsMeasureValid)
{
control.Measure(control.PreviousMeasure.Value);
}
_toMeasure.Remove(control);
}
private void Arrange(ILayoutable control)
{
var root = control as ILayoutRoot;
var parent = control.VisualParent as ILayoutable;
if (root != null)
{
root.Arrange(new Rect(root.DesiredSize));
}
else if (control.PreviousArrange.HasValue)
else if (parent != null)
{
Arrange(parent);
}
if (control.PreviousArrange.HasValue)
{
control.Arrange(control.PreviousArrange.Value);
}
_toArrange.Remove(control);
}
private void QueueLayoutPass()

12
src/Avalonia.SceneGraph/Rect.cs

@ -224,7 +224,7 @@ namespace Avalonia
}
/// <summary>
/// Determines whether a points in in the bounds of the rectangle.
/// Determines whether a point in in the bounds of the rectangle.
/// </summary>
/// <param name="p">The point.</param>
/// <returns>true if the point is in the bounds of the rectangle; otherwise false.</returns>
@ -234,6 +234,16 @@ namespace Avalonia
p.Y >= _y && p.Y <= _y + _height;
}
/// <summary>
/// Determines whether the rectangle fully contains another rectangle.
/// </summary>
/// <param name="r">The rectangle.</param>
/// <returns>true if the rectangle is fully contained; otherwise false.</returns>
public bool Contains(Rect r)
{
return Contains(r.TopLeft) && Contains(r.BottomRight);
}
/// <summary>
/// Centers another rectangle in this rectangle.
/// </summary>

5
src/Avalonia.Themes.Default/ListBox.xaml

@ -7,13 +7,14 @@
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer>
<ScrollViewer Name="PART_ScrollViewer">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{TemplateBinding Padding}"
MemberSelector="{TemplateBinding MemberSelector}"/>
MemberSelector="{TemplateBinding MemberSelector}"
VirtualizationMode="{TemplateBinding VirtualizationMode}"/>
</ScrollViewer>
</Border>
</ControlTemplate>

9
src/Markup/Avalonia.Markup.Xaml/Context/PropertyAccessor.cs

@ -136,7 +136,14 @@ namespace Avalonia.Markup.Xaml.Context
if (control != null)
{
DelayedBinding.Add(control, property, binding);
if (property != Control.DataContextProperty)
{
DelayedBinding.Add(control, property, binding);
}
else
{
control.Bind(property, binding);
}
}
else
{

15
src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs

@ -20,6 +20,7 @@ namespace Avalonia.Markup.Xaml.Data
/// </summary>
public Binding()
{
FallbackValue = AvaloniaProperty.UnsetValue;
}
/// <summary>
@ -27,6 +28,7 @@ namespace Avalonia.Markup.Xaml.Data
/// </summary>
/// <param name="path">The binding path.</param>
public Binding(string path)
: this()
{
Path = path;
}
@ -122,12 +124,23 @@ namespace Avalonia.Markup.Xaml.Data
throw new NotSupportedException();
}
var fallback = FallbackValue;
// If we're binding to DataContext and our fallback is UnsetValue then override
// the fallback value to null, as broken bindings to DataContext must reset the
// DataContext in order to not propagate incorrect DataContexts to child controls.
// See Avalonia.Markup.Xaml.UnitTests.Data.DataContext_Binding_Should_Produce_Correct_Results.
if (targetProperty == Control.DataContextProperty && fallback == AvaloniaProperty.UnsetValue)
{
fallback = null;
}
var subject = new ExpressionSubject(
observer,
targetProperty?.PropertyType ?? typeof(object),
fallback,
Converter ?? DefaultValueConverter.Instance,
ConverterParameter,
FallbackValue,
Priority);
return new InstancedBinding(subject, Mode, Priority);

2
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@ -36,7 +36,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
public IValueConverter Converter { get; set; }
public object ConverterParameter { get; set; }
public string ElementName { get; set; }
public object FallbackValue { get; set; }
public object FallbackValue { get; set; } = AvaloniaProperty.UnsetValue;
public BindingMode Mode { get; set; }
public string Path { get; set; }
public BindingPriority Priority { get; set; } = BindingPriority.LocalValue;

9
src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs

@ -16,6 +16,8 @@ namespace Avalonia.Markup.Xaml.Templates
[Content]
public TemplateContent Content { get; set; }
public bool SupportsRecycling => true;
public bool Match(object data)
{
if (DataType == null)
@ -28,11 +30,6 @@ namespace Avalonia.Markup.Xaml.Templates
}
}
public IControl Build(object data)
{
var visualTreeForItem = Content.Load();
visualTreeForItem.DataContext = data;
return visualTreeForItem;
}
public IControl Build(object data) => Content.Load();
}
}

2
src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs

@ -24,6 +24,8 @@ namespace Avalonia.Markup.Xaml.Templates
[AssignBinding]
public Binding ItemsSource { get; set; }
public bool SupportsRecycling => true;
public bool Match(object data)
{
if (DataType == null)

28
src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs

@ -41,16 +41,36 @@ namespace Avalonia.Markup.Data
/// <param name="converterParameter">
/// A parameter to pass to <paramref name="converter"/>.
/// </param>
/// <param name="priority">The binding priority.</param>
public ExpressionSubject(
ExpressionObserver inner,
Type targetType,
IValueConverter converter,
object converterParameter = null,
BindingPriority priority = BindingPriority.LocalValue)
: this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
/// <param name="targetType">The type to convert the value to.</param>
/// <param name="fallbackValue">
/// The value to use when the binding is unable to produce a value.
/// </param>
/// <param name="converter">The value converter to use.</param>
/// <param name="converterParameter">
/// A parameter to pass to <paramref name="converter"/>.
/// </param>
/// <param name="priority">The binding priority.</param>
public ExpressionSubject(
ExpressionObserver inner,
Type targetType,
Type targetType,
object fallbackValue,
IValueConverter converter,
object converterParameter = null,
object fallbackValue = null,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<ArgumentNullException>(inner != null);
@ -117,7 +137,7 @@ namespace Avalonia.Markup.Data
_inner.Expression,
error.Exception.Message);
if (_fallbackValue != null)
if (_fallbackValue != AvaloniaProperty.UnsetValue)
{
if (TypeUtilities.TryConvert(
type,
@ -162,7 +182,7 @@ namespace Avalonia.Markup.Data
ConverterParameter,
CultureInfo.CurrentUICulture);
if (_fallbackValue != null &&
if (_fallbackValue != AvaloniaProperty.UnsetValue &&
(converted == AvaloniaProperty.UnsetValue ||
converted is BindingError))
{

99
tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs

@ -53,6 +53,36 @@ namespace Avalonia.Base.UnitTests.Collections
Assert.Throws<ArgumentOutOfRangeException>(() => target.InsertRange(1, new List<int>() { 1 }));
}
[Fact]
public void Move_Should_Update_Collection()
{
var target = new AvaloniaList<int>(new[] { 1, 2, 3 });
target.Move(2, 0);
Assert.Equal(new[] { 3, 1, 2 }, target);
}
[Fact]
public void MoveRange_Should_Update_Collection()
{
var target = new AvaloniaList<int>(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
target.MoveRange(4, 3, 0);
Assert.Equal(new[] { 5, 6, 7, 1, 2, 3, 4, 8, 9, 10 }, target);
}
[Fact]
public void MoveRange_Can_Move_To_End()
{
var target = new AvaloniaList<int>(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
target.MoveRange(0, 5, 10);
Assert.Equal(new[] { 6, 7, 8, 9, 10, 1, 2, 3, 4, 5 }, target);
}
[Fact]
public void Adding_Item_Should_Raise_CollectionChanged()
{
@ -95,6 +125,29 @@ namespace Avalonia.Base.UnitTests.Collections
Assert.True(raised);
}
[Fact]
public void Replacing_Item_Should_Raise_CollectionChanged()
{
var target = new AvaloniaList<int>(new[] { 1, 2 });
var raised = false;
target.CollectionChanged += (s, e) =>
{
Assert.Equal(target, s);
Assert.Equal(NotifyCollectionChangedAction.Replace, e.Action);
Assert.Equal(new[] { 2 }, e.OldItems.Cast<int>());
Assert.Equal(new[] { 3 }, e.NewItems.Cast<int>());
Assert.Equal(1, e.OldStartingIndex);
Assert.Equal(1, e.NewStartingIndex);
raised = true;
};
target[1] = 3;
Assert.True(raised);
}
[Fact]
public void Inserting_Item_Should_Raise_CollectionChanged()
{
@ -158,6 +211,52 @@ namespace Avalonia.Base.UnitTests.Collections
Assert.True(raised);
}
[Fact]
public void Moving_Item_Should_Raise_CollectionChanged()
{
var target = new AvaloniaList<int>(new[] { 1, 2, 3 });
var raised = false;
target.CollectionChanged += (s, e) =>
{
Assert.Equal(target, s);
Assert.Equal(NotifyCollectionChangedAction.Move, e.Action);
Assert.Equal(new[] { 3 }, e.OldItems.Cast<int>());
Assert.Equal(new[] { 3 }, e.NewItems.Cast<int>());
Assert.Equal(2, e.OldStartingIndex);
Assert.Equal(0, e.NewStartingIndex);
raised = true;
};
target.Move(2, 0);
Assert.True(raised);
}
[Fact]
public void Moving_Items_Should_Raise_CollectionChanged()
{
var target = new AvaloniaList<int>(new[] { 1, 2, 3 });
var raised = false;
target.CollectionChanged += (s, e) =>
{
Assert.Equal(target, s);
Assert.Equal(NotifyCollectionChangedAction.Move, e.Action);
Assert.Equal(new[] { 2, 3 }, e.OldItems.Cast<int>());
Assert.Equal(new[] { 2, 3 }, e.NewItems.Cast<int>());
Assert.Equal(1, e.OldStartingIndex);
Assert.Equal(0, e.NewStartingIndex);
raised = true;
};
target.MoveRange(1, 2, 0);
Assert.True(raised);
}
[Fact]
public void Clearing_Items_Should_Raise_CollectionChanged_Reset()
{

6
tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj

@ -91,6 +91,8 @@
<ItemGroup>
<Compile Include="ClassesTests.cs" />
<Compile Include="LayoutTransformControlTests.cs" />
<Compile Include="Presenters\ItemsPresenterTests_Virtualization_Simple.cs" />
<Compile Include="Presenters\ItemsPresenterTests_Virtualization.cs" />
<Compile Include="TextBoxTests_ValidationState.cs" />
<Compile Include="UserControlTests.cs" />
<Compile Include="DockPanelTests.cs" />
@ -100,7 +102,6 @@
<Compile Include="HeaderedItemsControlTests .cs" />
<Compile Include="ControlTests_NameScope.cs" />
<Compile Include="Generators\ItemContainerGeneratorTests.cs" />
<Compile Include="Generators\ItemContainerGeneratorTypedTests.cs" />
<Compile Include="GridLengthTests.cs" />
<Compile Include="ImageTests.cs" />
<Compile Include="Presenters\ContentPresenterTests.cs" />
@ -108,7 +109,7 @@
<Compile Include="ListBoxTests_Single.cs" />
<Compile Include="NameScopeTests.cs" />
<Compile Include="Primitives\SelectingItemsControlTests_Multiple.cs" />
<Compile Include="Presenters\ScrollContentPresenterTests_IScrollable.cs" />
<Compile Include="Presenters\ScrollContentPresenterTests_ILogicalScrollable.cs" />
<Compile Include="TextBoxTests.cs" />
<Compile Include="TextBlockTests.cs" />
<Compile Include="WindowingPlatformMock.cs" />
@ -142,6 +143,7 @@
<Compile Include="TestTemplatedControl.cs" />
<Compile Include="Utils\AncestorFinderTests.cs" />
<Compile Include="Utils\HotKeyManagerTests.cs" />
<Compile Include="VirtualizingStackPanelTests.cs" />
<Compile Include="WrapPanelTests.cs" />
</ItemGroup>
<ItemGroup>

7
tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.v2.ncrunchproject

@ -17,10 +17,11 @@
<DetectStackOverflow>true</DetectStackOverflow>
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
<DefaultTestTimeout>60000</DefaultTestTimeout>
<UseBuildConfiguration />
<UseBuildPlatform />
<ProxyProcessPath />
<UseBuildConfiguration></UseBuildConfiguration>
<UseBuildPlatform></UseBuildPlatform>
<ProxyProcessPath></ProxyProcessPath>
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture>
<HiddenWarnings>LongTestTimesWithoutParallelExecution</HiddenWarnings>
</ProjectConfiguration>

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

@ -50,8 +50,8 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(1, target.GetLogicalChildren().Count());
var child = target.GetLogicalChildren().Single();
Assert.IsType<TextBlock>(child);
Assert.Equal("Foo", ((TextBlock)child).Text);
Assert.IsType<ContentPresenter>(child);
Assert.Equal("Foo", ((ContentPresenter)child).Content);
}
[Fact]

35
tests/Avalonia.Controls.UnitTests/ControlTests.cs

@ -22,22 +22,51 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
public void LogicalParent_Should_Be_Set_To_Parent()
public void Setting_Parent_Should_Also_Set_InheritanceParent()
{
var parent = new Decorator();
var target = new TestControl();
parent.Child = target;
Assert.Equal(parent, target.Parent);
Assert.Equal(parent, target.InheritanceParent);
}
[Fact]
public void LogicalParent_Should_Be_Cleared_When_Removed_From_Parent()
public void Setting_Parent_Should_Not_Set_InheritanceParent_If_Already_Set()
{
var parent = new Decorator();
var inheritanceParent = new Decorator();
var target = new TestControl();
((ISetInheritanceParent)target).SetParent(inheritanceParent);
parent.Child = target;
Assert.Equal(parent, target.Parent);
Assert.Equal(inheritanceParent, target.InheritanceParent);
}
[Fact]
public void InheritanceParent_Should_Be_Cleared_When_Removed_From_Parent()
{
var parent = new Decorator();
var target = new TestControl();
parent.Child = target;
parent.Child = null;
Assert.Null(target.InheritanceParent);
}
[Fact]
public void InheritanceParent_Should_Be_Cleared_When_Removed_From_Parent_When_Has_Different_InheritanceParent()
{
var parent = new Decorator();
var inheritanceParent = new Decorator();
var target = new TestControl();
((ISetInheritanceParent)target).SetParent(inheritanceParent);
parent.Child = target;
parent.Child = null;
@ -262,7 +291,7 @@ namespace Avalonia.Controls.UnitTests
private class TestControl : Control
{
public new AvaloniaObject InheritanceParent => base.InheritanceParent;
public new IAvaloniaObject InheritanceParent => base.InheritanceParent;
}
}
}

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

@ -1,8 +1,10 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Presenters;
using Xunit;
namespace Avalonia.Controls.UnitTests.Generators
@ -15,11 +17,11 @@ namespace Avalonia.Controls.UnitTests.Generators
var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator();
var target = new ItemContainerGenerator(owner);
var containers = target.Materialize(0, items, null);
var containers = Materialize(target, 0, items);
var result = containers
.Select(x => x.ContainerControl)
.OfType<TextBlock>()
.Select(x => x.Text)
.OfType<ContentPresenter>()
.Select(x => x.Content)
.ToList();
Assert.Equal(items, result);
@ -31,7 +33,7 @@ namespace Avalonia.Controls.UnitTests.Generators
var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator();
var target = new ItemContainerGenerator(owner);
var containers = target.Materialize(0, items, null).ToList();
var containers = Materialize(target, 0, items);
Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0));
Assert.Equal(containers[1].ContainerControl, target.ContainerFromIndex(1));
@ -44,7 +46,7 @@ namespace Avalonia.Controls.UnitTests.Generators
var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator();
var target = new ItemContainerGenerator(owner);
var containers = target.Materialize(0, items, null).ToList();
var containers = Materialize(target, 0, items);
Assert.Equal(0, target.IndexFromContainer(containers[0].ContainerControl));
Assert.Equal(1, target.IndexFromContainer(containers[1].ContainerControl));
@ -57,7 +59,7 @@ namespace Avalonia.Controls.UnitTests.Generators
var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator();
var target = new ItemContainerGenerator(owner);
var containers = target.Materialize(0, items, null).ToList();
var containers = Materialize(target, 0, items);
target.Dematerialize(1, 1);
@ -72,26 +74,57 @@ namespace Avalonia.Controls.UnitTests.Generators
var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator();
var target = new ItemContainerGenerator(owner);
var containers = target.Materialize(0, items, null);
var containers = Materialize(target, 0, items);
var expected = target.Containers.Take(2).ToList();
var result = target.Dematerialize(0, 2);
Assert.Equal(expected, result);
}
[Fact]
public void InsertSpace_Should_Alter_Successive_Container_Indexes()
{
var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator();
var target = new ItemContainerGenerator(owner);
var containers = Materialize(target, 0, items);
target.InsertSpace(1, 3);
Assert.Equal(3, target.Containers.Count());
Assert.Equal(new[] { 0, 4, 5 }, target.Containers.Select(x => x.Index));
}
[Fact]
public void RemoveRange_Should_Alter_Successive_Container_Indexes()
{
var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator();
var target = new ItemContainerGenerator(owner);
var containers = target.Materialize(0, items, null).ToList();
var containers = Materialize(target, 0, items);
var removed = target.RemoveRange(1, 1).Single();
Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0));
Assert.Equal(containers[2].ContainerControl, target.ContainerFromIndex(1));
Assert.Equal(containers[1], removed);
Assert.Equal(new[] { 0, 1 }, target.Containers.Select(x => x.Index));
}
private IList<ItemContainerInfo> Materialize(
IItemContainerGenerator generator,
int index,
string[] items)
{
var result = new List<ItemContainerInfo>();
foreach (var item in items)
{
var container = generator.Materialize(index++, item, null);
result.Add(container);
}
return result;
}
}
}

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

@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Generators;
using Xunit;
@ -15,7 +16,7 @@ namespace Avalonia.Controls.UnitTests.Generators
var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator();
var target = new ItemContainerGenerator<ListBoxItem>(owner, ListBoxItem.ContentProperty, null);
var containers = target.Materialize(0, items, null);
var containers = Materialize(target, 0, items);
var result = containers
.Select(x => x.ContainerControl)
.OfType<ListBoxItem>()
@ -24,5 +25,21 @@ namespace Avalonia.Controls.UnitTests.Generators
Assert.Equal(items, result);
}
private IList<ItemContainerInfo> Materialize(
IItemContainerGenerator generator,
int index,
string[] items)
{
var result = new List<ItemContainerInfo>();
foreach (var item in items)
{
var container = generator.Materialize(index++, item, null);
result.Add(container);
}
return result;
}
}
}

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

@ -27,7 +27,10 @@ namespace Avalonia.Controls.UnitTests
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Assert.IsType<Canvas>(target.Presenter.Panel.Children[0]);
var container = (ContentPresenter)target.Presenter.Panel.Children[0];
container.UpdateChild();
Assert.IsType<Canvas>(container.Child);
}
[Fact]
@ -44,7 +47,7 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
public void Item_Should_Have_TemplatedParent_Set_To_Null()
public void Container_Should_Have_TemplatedParent_Set_To_Null()
{
var target = new ItemsControl();
@ -53,9 +56,9 @@ namespace Avalonia.Controls.UnitTests
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var item = (TextBlock)target.Presenter.Panel.GetVisualChildren().First();
var container = (ContentPresenter)target.Presenter.Panel.Children[0];
Assert.Null(item.TemplatedParent);
Assert.Null(container.TemplatedParent);
}
[Fact]
@ -135,7 +138,7 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
public void Adding_String_Item_Should_Make_TextBlock_Appear_In_LogicalChildren()
public void Adding_String_Item_Should_Make_ContentPresenter_Appear_In_LogicalChildren()
{
var target = new ItemsControl();
var child = new Control();
@ -147,7 +150,7 @@ namespace Avalonia.Controls.UnitTests
var logical = (ILogical)target;
Assert.Equal(1, logical.LogicalChildren.Count);
Assert.IsType<TextBlock>(logical.LogicalChildren[0]);
Assert.IsType<ContentPresenter>(logical.LogicalChildren[0]);
}
[Fact]
@ -367,6 +370,7 @@ namespace Avalonia.Controls.UnitTests
target.Presenter.ApplyTemplate();
var dataContexts = target.Presenter.Panel.Children
.Do(x => (x as ContentPresenter)?.UpdateChild())
.Cast<Control>()
.Select(x => x.DataContext)
.ToList();
@ -390,8 +394,8 @@ namespace Avalonia.Controls.UnitTests
target.Presenter.ApplyTemplate();
var text = target.Presenter.Panel.Children
.Cast<TextBlock>()
.Select(x => x.Text)
.Cast<ContentPresenter>()
.Select(x => x.Content)
.ToList();
Assert.Equal(new[] { "Foo", "Bar" }, text);
@ -419,7 +423,7 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
public void DataTemplate_Created_Item_Should_Be_NameScope()
public void DataTemplate_Created_Content_Should_Be_NameScope()
{
var items = new object[]
{
@ -435,8 +439,10 @@ namespace Avalonia.Controls.UnitTests
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var item = target.Presenter.Panel.LogicalChildren[0];
Assert.NotNull(NameScope.GetNameScope((TextBlock)item));
var container = (ContentPresenter)target.Presenter.Panel.LogicalChildren[0];
container.UpdateChild();
Assert.NotNull(NameScope.GetNameScope((TextBlock)container.Child));
}
private class Item

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

@ -7,6 +7,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
@ -19,19 +20,14 @@ namespace Avalonia.Controls.UnitTests
{
var target = new ListBox
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
Template = ListBoxTemplate(),
Items = new[] { "Foo" },
ItemTemplate = new FuncDataTemplate<string>(_ => new Canvas()),
};
target.Items = new[] { "Foo" };
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Prepare(target);
var container = (ListBoxItem)target.Presenter.Panel.Children[0];
container.Template = ListBoxItemTemplate();
container.ApplyTemplate();
((ContentPresenter)container.Presenter).UpdateChild();
Assert.IsType<Canvas>(container.Presenter.Child);
}
@ -40,10 +36,10 @@ namespace Avalonia.Controls.UnitTests
{
var target = new ListBox
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
Template = ListBoxTemplate(),
};
ApplyTemplate(target);
Prepare(target);
Assert.IsType<ItemsPresenter>(target.Presenter);
}
@ -51,113 +47,152 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void ListBoxItem_Containers_Should_Be_Generated()
{
var items = new[] { "Foo", "Bar", "Baz " };
var target = new ListBox
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
Items = items,
};
var items = new[] { "Foo", "Bar", "Baz " };
var target = new ListBox
{
Template = ListBoxTemplate(),
Items = items,
};
ApplyTemplate(target);
Prepare(target);
var text = target.Presenter.Panel.Children
.OfType<ListBoxItem>()
.Do(x => x.Template = ListBoxItemTemplate())
.Do(x => { x.ApplyTemplate(); ((ContentPresenter)x.Presenter).UpdateChild(); })
.Select(x => x.Presenter.Child)
.OfType<TextBlock>()
.Select(x => x.Text)
.ToList();
var text = target.Presenter.Panel.Children
.OfType<ListBoxItem>()
.Select(x => x.Presenter.Child)
.OfType<TextBlock>()
.Select(x => x.Text)
.ToList();
Assert.Equal(items, text);
Assert.Equal(items, text);
}
}
[Fact]
public void LogicalChildren_Should_Be_Set_For_DataTemplate_Generated_Items()
{
var target = new ListBox
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
};
var target = new ListBox
{
Template = ListBoxTemplate(),
Items = new[] { "Foo", "Bar", "Baz " },
};
ApplyTemplate(target);
Prepare(target);
Assert.Equal(3, target.GetLogicalChildren().Count());
Assert.Equal(3, target.GetLogicalChildren().Count());
foreach (var child in target.GetLogicalChildren())
{
Assert.IsType<ListBoxItem>(child);
foreach (var child in target.GetLogicalChildren())
{
Assert.IsType<ListBoxItem>(child);
}
}
}
[Fact]
public void DataContexts_Should_Be_Correctly_Set()
{
var items = new object[]
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
"Foo",
new Item("Bar"),
new TextBlock { Text = "Baz" },
new ListBoxItem { Content = "Qux" },
};
var items = new object[]
{
"Foo",
new Item("Bar"),
new TextBlock { Text = "Baz" },
new ListBoxItem { Content = "Qux" },
};
var target = new ListBox
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
DataContext = "Base",
DataTemplates = new DataTemplates
var target = new ListBox
{
Template = ListBoxTemplate(),
DataContext = "Base",
DataTemplates = new DataTemplates
{
new FuncDataTemplate<Item>(x => new Button { Content = x })
},
Items = items,
};
Items = items,
};
ApplyTemplate(target);
Prepare(target);
var dataContexts = target.Presenter.Panel.Children
.Cast<Control>()
.Select(x => x.DataContext)
.ToList();
var dataContexts = target.Presenter.Panel.Children
.Cast<Control>()
.Select(x => x.DataContext)
.ToList();
Assert.Equal(
new object[] { items[0], items[1], "Base", "Base" },
dataContexts);
Assert.Equal(
new object[] { items[0], items[1], "Base", "Base" },
dataContexts);
}
}
private Control CreateListBoxTemplate(ITemplatedControl parent)
[Fact]
public void Selection_Should_Be_Cleared_On_Recycled_Items()
{
return new ScrollViewer
var target = new ListBox
{
Template = new FuncControlTemplate(CreateScrollViewerTemplate),
Content = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty),
}
Template = ListBoxTemplate(),
Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(),
ItemTemplate = new FuncDataTemplate<string>(x => new TextBlock { Height = 10 }),
SelectedIndex = 0,
};
Prepare(target);
// Make sure we're virtualized and first item is selected.
Assert.Equal(10, target.Presenter.Panel.Children.Count);
Assert.True(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected);
// Scroll down a page.
target.Scroll.Offset = new Vector(0, 10);
// Make sure recycled item isn't now selected.
Assert.False(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected);
}
private FuncControlTemplate ListBoxTemplate()
{
return new FuncControlTemplate<ListBox>(parent =>
new ScrollViewer
{
Name = "PART_ScrollViewer",
Template = ScrollViewerTemplate(),
Content = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty),
[~ItemsPresenter.ItemsPanelProperty] = parent.GetObservable(ItemsControl.ItemsPanelProperty),
}
});
}
private FuncControlTemplate ListBoxItemTemplate()
{
return new FuncControlTemplate<ListBoxItem>(parent => new ContentPresenter
{
Name = "PART_ContentPresenter",
[!ContentPresenter.ContentProperty] = parent[!ListBoxItem.ContentProperty],
[!ContentPresenter.ContentTemplateProperty] = parent[!ListBoxItem.ContentTemplateProperty],
});
return new FuncControlTemplate<ListBoxItem>(parent =>
new ContentPresenter
{
Name = "PART_ContentPresenter",
[!ContentPresenter.ContentProperty] = parent[!ListBoxItem.ContentProperty],
[!ContentPresenter.ContentTemplateProperty] = parent[!ListBoxItem.ContentTemplateProperty],
});
}
private Control CreateScrollViewerTemplate(ITemplatedControl parent)
private FuncControlTemplate ScrollViewerTemplate()
{
return new ScrollContentPresenter
{
Name = "PART_ContentPresenter",
[~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty),
};
return new FuncControlTemplate<ScrollViewer>(parent =>
new ScrollContentPresenter
{
Name = "PART_ContentPresenter",
[~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty),
[~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty],
[~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty],
[~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty],
});
}
private void ApplyTemplate(ListBox target)
private void Prepare(ListBox target)
{
// Apply the template to the ListBox itself.
target.ApplyTemplate();
@ -171,6 +206,28 @@ namespace Avalonia.Controls.UnitTests
// Now the ItemsPresenter should be reigstered, so apply its template.
target.Presenter.ApplyTemplate();
// Because ListBox items are virtualized we need to do a layout to make them appear.
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
// Now set and apply the item templates.
foreach (ListBoxItem item in target.Presenter.Panel.Children)
{
item.Template = ListBoxItemTemplate();
item.ApplyTemplate();
item.Presenter.ApplyTemplate();
((ContentPresenter)item.Presenter).UpdateChild();
}
// The items were created before the template was applied, so now we need to go back
// and re-arrange everything.
foreach (IControl i in target.GetSelfAndVisualDescendents())
{
i.InvalidateMeasure();
}
target.Arrange(new Rect(0, 0, 100, 100));
}
private class Item

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

@ -60,8 +60,8 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.ApplyTemplate();
Assert.IsType<TextBlock>(target.Panel.Children[0]);
Assert.Equal("foo", ((TextBlock)target.Panel.Children[0]).Text);
Assert.IsType<ContentPresenter>(target.Panel.Children[0]);
Assert.Equal("foo", ((ContentPresenter)target.Panel.Children[0]).Content);
}
[Fact]
@ -76,8 +76,8 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.ApplyTemplate();
target.SelectedIndex = 1;
Assert.IsType<TextBlock>(target.Panel.Children[0]);
Assert.Equal("bar", ((TextBlock)target.Panel.Children[0]).Text);
Assert.IsType<ContentPresenter>(target.Panel.Children[0]);
Assert.Equal("bar", ((ContentPresenter)target.Panel.Children[0]).Content);
}
[Fact]
@ -155,7 +155,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
}
[Fact]
public void Should_have_correct_index_itemscontainer()
public void Should_Have_Correct_ItemsContainer_Index()
{
ObservableCollection<string> items = new ObservableCollection<string>();
@ -186,7 +186,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
items.Remove(items[0]);
Assert.Equal(1, target.ItemContainerGenerator.Containers.Count());
Assert.Equal(1, target.Panel.Children.Count);
Assert.Equal(1, target.ItemContainerGenerator.Containers.First().Index);
Assert.Equal(0, target.ItemContainerGenerator.Containers.First().Index);
items.Remove(items[0]);
Assert.Equal(0, target.ItemContainerGenerator.Containers.Count());

30
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs

@ -9,6 +9,7 @@ using Avalonia.Controls.Templates;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
using System;
namespace Avalonia.Controls.UnitTests.Presenters
{
@ -142,17 +143,42 @@ namespace Avalonia.Controls.UnitTests.Presenters
}
[Fact]
public void Assigning_NonControl_To_Content_Should_Set_DataContext()
public void Assigning_NonControl_To_Content_Should_Set_DataContext_On_UpdateChild()
{
var target = new ContentPresenter
{
Content = "foo",
};
target.UpdateChild();
Assert.Equal("foo", target.DataContext);
}
private class TestContentControl : TemplatedControl
[Fact]
public void Tries_To_Recycle_DataTemplate()
{
var target = new ContentPresenter
{
DataTemplates = new DataTemplates
{
new FuncDataTemplate<string>(_ => new Border(), true),
},
Content = "foo",
};
target.UpdateChild();
var control = target.Child;
Assert.IsType<Border>(control);
target.Content = "bar";
target.UpdateChild();
Assert.Same(control, target.Child);
}
private class TestContentControl : ContentControl
{
public IControl Child { get; set; }
}

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

@ -38,10 +38,10 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.ApplyTemplate();
Assert.Equal(2, target.Panel.Children.Count);
Assert.IsType<TextBlock>(target.Panel.Children[0]);
Assert.IsType<TextBlock>(target.Panel.Children[1]);
Assert.Equal("foo", ((TextBlock)target.Panel.Children[0]).Text);
Assert.Equal("bar", ((TextBlock)target.Panel.Children[1]).Text);
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]
@ -88,8 +88,8 @@ namespace Avalonia.Controls.UnitTests.Presenters
items.RemoveAt(0);
Assert.Equal(1, target.Panel.Children.Count);
Assert.Equal("bar", ((TextBlock)target.Panel.Children[0]).Text);
Assert.Equal("bar", ((TextBlock)target.ItemContainerGenerator.ContainerFromIndex(0)).Text);
Assert.Equal("bar", ((ContentPresenter)target.Panel.Children[0]).Content);
Assert.Equal("bar", ((ContentPresenter)target.ItemContainerGenerator.ContainerFromIndex(0)).Content);
}
[Fact]
@ -121,8 +121,8 @@ namespace Avalonia.Controls.UnitTests.Presenters
items[1] = "baz";
var text = target.Panel.Children
.OfType<TextBlock>()
.Select(x => x.Text)
.OfType<ContentPresenter>()
.Select(x => x.Content)
.ToList();
Assert.Equal(new[] { "foo", "baz", "baz" }, text);
@ -141,8 +141,8 @@ namespace Avalonia.Controls.UnitTests.Presenters
items.Move(2, 1);
var text = target.Panel.Children
.OfType<TextBlock>()
.Select(x => x.Text)
.OfType<ContentPresenter>()
.Select(x => x.Content)
.ToList();
Assert.Equal(new[] { "foo", "baz", "bar" }, text);
@ -161,8 +161,8 @@ namespace Avalonia.Controls.UnitTests.Presenters
items.Insert(2, "insert");
var text = target.Panel.Children
.OfType<TextBlock>()
.Select(x => x.Text)
.OfType<ContentPresenter>()
.Select(x => x.Content)
.ToList();
Assert.Equal(new[] { "foo", "bar", "insert", "baz" }, text);
@ -195,16 +195,16 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.ApplyTemplate();
var text = target.Panel.Children.Cast<TextBlock>().Select(x => x.Text).ToList();
var text = target.Panel.Children.Cast<ContentPresenter>().Select(x => x.Content).ToList();
Assert.Equal(new[] { "foo", "bar" }, text);
Assert.Equal(new[] { "foo", null, "bar" }, text);
Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(0));
Assert.Null(target.ItemContainerGenerator.ContainerFromIndex(1));
Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(1));
Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(2));
items.RemoveAt(1);
text = target.Panel.Children.Cast<TextBlock>().Select(x => x.Text).ToList();
text = target.Panel.Children.Cast<ContentPresenter>().Select(x => x.Content).ToList();
Assert.Equal(new[] { "foo", "bar" }, text);
Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(0));
@ -224,8 +224,11 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.ApplyTemplate();
items.RemoveAt(2);
var text = target.Panel.Children.OfType<TextBlock>().Select(x => x.Text);
Assert.Equal(new[] { "1", "2" }, text);
var numbers = target.Panel.Children
.OfType<ContentPresenter>()
.Select(x => x.Content)
.Cast<int>();
Assert.Equal(new[] { 1, 2 }, numbers);
}
[Fact]
@ -288,8 +291,8 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.ApplyTemplate();
var text = target.Panel.Children
.Cast<TextBlock>()
.Select(x => x.Text)
.Cast<ContentPresenter>()
.Select(x => x.Content)
.ToList();
Assert.Equal(new[] { "Foo", "Bar" }, text);
@ -308,7 +311,8 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.ApplyTemplate();
var dataContexts = target.Panel.Children
.Cast<TextBlock>()
.Cast<ContentPresenter>()
.Do(x => x.UpdateChild())
.Select(x => x.DataContext)
.ToList();

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

@ -0,0 +1,302 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
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.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests.Presenters
{
public class ItemsPresenterTests_Virtualization
{
[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(0, 20), scroll.Extent);
Assert.Equal(new Size(0, 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 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(10, 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(0, 20), scroll.Extent);
Assert.Equal(new Size(0, 10), scroll.Viewport);
}
[Fact]
public void Changing_VirtualizationMode_None_To_Simple_Should_Add_Correct_Number_Of_Controls()
{
using (UnitTestApplication.Start(TestServices.RealLayoutManager))
{
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));
// 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;
LayoutManager.Instance.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(0, 20), scroll.Extent);
Assert.Equal(new Size(0, 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(10, 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 ScrollContentPresenter
{
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<IPanel> VirtualizingPanelTemplate(
Orientation orientation = Orientation.Vertical)
{
return new FuncTemplate<IPanel>(() => new VirtualizingStackPanel
{
Orientation = orientation,
});
}
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;
}
}
}
}

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

@ -0,0 +1,683 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests.Presenters
{
public class ItemsPresenterTests_Virtualization_Simple
{
[Fact]
public void Should_Return_Items_Count_For_Extent_Vertical()
{
var target = CreateTarget();
target.ApplyTemplate();
Assert.Equal(new Size(0, 20), ((ILogicalScrollable)target).Extent);
}
[Fact]
public void Should_Return_Items_Count_For_Extent_Horizontal()
{
var target = CreateTarget(orientation: Orientation.Horizontal);
target.ApplyTemplate();
Assert.Equal(new Size(20, 0), ((ILogicalScrollable)target).Extent);
}
[Fact]
public void Should_Have_Number_Of_Visible_Items_As_Viewport_Vertical()
{
var target = CreateTarget();
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Size(0, 10), ((ILogicalScrollable)target).Viewport);
}
[Fact]
public void Should_Have_Number_Of_Visible_Items_As_Viewport_Horizontal()
{
var target = CreateTarget(orientation: Orientation.Horizontal);
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Size(10, 0), ((ILogicalScrollable)target).Viewport);
}
[Fact]
public void Should_Remove_Items_When_Control_Is_Shrank()
{
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(100, 80));
target.Arrange(new Rect(0, 0, 100, 80));
Assert.Equal(8, target.Panel.Children.Count);
}
[Fact]
public void Should_Add_New_Containers_At_Top_When_Control_Is_Scrolled_To_Bottom_And_Enlarged()
{
var target = CreateTarget();
var items = (IList<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);
((IScrollable)target).Offset = new Vector(0, 10);
target.Measure(new Size(120, 120));
target.Arrange(new Rect(0, 0, 100, 120));
Assert.Equal(12, target.Panel.Children.Count);
for (var i = 0; i < target.Panel.Children.Count; ++i)
{
Assert.Equal(items[i + 8], target.Panel.Children[i].DataContext);
}
}
[Fact]
public void Should_Update_Containers_When_Items_Changes()
{
var target = CreateTarget();
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
target.Items = new[] { "foo", "bar", "baz" };
Assert.Equal(3, target.Panel.Children.Count);
}
[Fact]
public void Should_Decrease_The_Viewport_Size_By_One_If_There_Is_A_Partial_Item()
{
var target = CreateTarget();
target.ApplyTemplate();
target.Measure(new Size(100, 95));
target.Arrange(new Rect(0, 0, 100, 95));
Assert.Equal(new Size(0, 9), ((ILogicalScrollable)target).Viewport);
}
[Fact]
public void Moving_To_And_From_The_End_With_Partial_Item_Should_Set_Panel_PixelOffset()
{
var target = CreateTarget();
target.ApplyTemplate();
target.Measure(new Size(100, 95));
target.Arrange(new Rect(0, 0, 100, 95));
((ILogicalScrollable)target).Offset = new Vector(0, 11);
var minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index);
Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset);
Assert.Equal(10, minIndex);
Assert.Equal(10, ((IVirtualizingPanel)target.Panel).PixelOffset);
((ILogicalScrollable)target).Offset = new Vector(0, 10);
minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index);
Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset);
Assert.Equal(10, minIndex);
Assert.Equal(0, ((IVirtualizingPanel)target.Panel).PixelOffset);
((ILogicalScrollable)target).Offset = new Vector(0, 11);
minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index);
Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset);
Assert.Equal(10, minIndex);
Assert.Equal(10, ((IVirtualizingPanel)target.Panel).PixelOffset);
}
[Fact]
public void Inserting_Items_Should_Update_Containers()
{
var target = CreateTarget();
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
((ILogicalScrollable)target).Offset = new Vector(0, 5);
var expected = Enumerable.Range(5, 10).Select(x => $"Item {x}").ToList();
var items = (ObservableCollection<string>)target.Items;
var actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual);
items.Insert(6, "Inserted");
expected.Insert(1, "Inserted");
expected.RemoveAt(expected.Count - 1);
actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual);
}
[Fact]
public void Removing_First_Materialized_Item_Should_Update_Containers()
{
var target = CreateTarget();
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList();
var items = (ObservableCollection<string>)target.Items;
var actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual);
items.RemoveAt(0);
expected = Enumerable.Range(1, 10).Select(x => $"Item {x}").ToList();
actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual);
}
[Fact]
public void Removing_Items_From_Middle_Should_Update_Containers_When_All_Items_Visible()
{
var target = CreateTarget();
target.ApplyTemplate();
target.Measure(new Size(100, 200));
target.Arrange(new Rect(0, 0, 100, 200));
var items = (ObservableCollection<string>)target.Items;
var actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(items, actual);
items.RemoveAt(2);
actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(items, actual);
items.RemoveAt(items.Count - 2);
actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(items, actual);
}
[Fact]
public void Removing_Last_Item_Should_Update_Containers_When_All_Items_Visible()
{
var target = CreateTarget();
target.ApplyTemplate();
target.Measure(new Size(100, 200));
target.Arrange(new Rect(0, 0, 100, 200));
((ILogicalScrollable)target).Offset = new Vector(0, 5);
var expected = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList();
var items = (ObservableCollection<string>)target.Items;
var actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual);
items.Remove(items.Last());
expected.Remove(expected.Last());
actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual);
}
[Fact]
public void Removing_Items_When_Scrolled_To_End_Should_Recyle_Containers_At_Top()
{
var target = CreateTarget(useAvaloniaList: true);
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
((ILogicalScrollable)target).Offset = new Vector(0, 10);
var expected = Enumerable.Range(10, 10).Select(x => $"Item {x}").ToList();
var items = (AvaloniaList<string>)target.Items;
var actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual);
items.RemoveRange(18, 2);
expected = Enumerable.Range(8, 10).Select(x => $"Item {x}").ToList();
actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual);
}
[Fact]
public void Removing_Items_When_Scrolled_To_Near_End_Should_Recycle_Containers_At_Bottom_And_Top()
{
var target = CreateTarget(useAvaloniaList: true);
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
((ILogicalScrollable)target).Offset = new Vector(0, 9);
var expected = Enumerable.Range(9, 10).Select(x => $"Item {x}").ToList();
var items = (AvaloniaList<string>)target.Items;
var actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual);
items.RemoveRange(15, 3);
expected = Enumerable.Range(7, 8).Select(x => $"Item {x}")
.Concat(Enumerable.Range(18, 2).Select(x => $"Item {x}"))
.ToList();
actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual);
}
[Fact]
public void Replacing_Items_Should_Update_Containers()
{
var target = CreateTarget();
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList();
var items = (ObservableCollection<string>)target.Items;
var actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual);
items[4] = expected[4] = "Replaced";
actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual);
}
[Fact]
public void Moving_Items_Should_Update_Containers()
{
var target = CreateTarget();
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList();
var items = (ObservableCollection<string>)target.Items;
var actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual);
items.Move(4, 8);
var i = expected[4];
expected.RemoveAt(4);
expected.Insert(8, i);
actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual);
}
[Fact]
public void Setting_Items_To_Null_Should_Remove_Containers()
{
var target = CreateTarget();
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList();
var items = (ObservableCollection<string>)target.Items;
var actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual);
target.Items = null;
Assert.Empty(target.Panel.Children);
}
public class Vertical
{
[Fact]
public void GetControlInDirection_Down_Should_Return_Existing_Container_If_Materialized()
{
var target = CreateTarget();
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
var from = target.Panel.Children[5];
var result = ((ILogicalScrollable)target).GetControlInDirection(
NavigationDirection.Down,
from);
Assert.Same(target.Panel.Children[6], result);
}
[Fact]
public void GetControlInDirection_Down_Should_Scroll_If_Necessary()
{
var target = CreateTarget();
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
var from = target.Panel.Children[9];
var result = ((ILogicalScrollable)target).GetControlInDirection(
NavigationDirection.Down,
from);
Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset);
Assert.Same(target.Panel.Children[9], result);
}
[Fact]
public void GetControlInDirection_Down_Should_Scroll_If_Partially_Visible()
{
using (UnitTestApplication.Start(TestServices.RealLayoutManager))
{
var target = CreateTarget();
var scroller = (ScrollContentPresenter)target.Parent;
scroller.Measure(new Size(100, 95));
scroller.Arrange(new Rect(0, 0, 100, 95));
var from = target.Panel.Children[8];
var result = ((ILogicalScrollable)target).GetControlInDirection(
NavigationDirection.Down,
from);
Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset);
Assert.Same(target.Panel.Children[8], result);
}
}
[Fact]
public void GetControlInDirection_Up_Should_Scroll_If_Partially_Visible_Item_Is_Currently_Shown()
{
using (UnitTestApplication.Start(TestServices.RealLayoutManager))
{
var target = CreateTarget();
var scroller = (ScrollContentPresenter)target.Parent;
scroller.Measure(new Size(100, 95));
scroller.Arrange(new Rect(0, 0, 100, 95));
((ILogicalScrollable)target).Offset = new Vector(0, 11);
var from = target.Panel.Children[1];
var result = ((ILogicalScrollable)target).GetControlInDirection(
NavigationDirection.Up,
from);
Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset);
Assert.Same(target.Panel.Children[0], result);
}
}
}
public class Horizontal
{
[Fact]
public void GetControlInDirection_Right_Should_Return_Existing_Container_If_Materialized()
{
var target = CreateTarget(orientation: Orientation.Horizontal);
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
var from = target.Panel.Children[5];
var result = ((ILogicalScrollable)target).GetControlInDirection(
NavigationDirection.Right,
from);
Assert.Same(target.Panel.Children[6], result);
}
[Fact]
public void GetControlInDirection_Right_Should_Scroll_If_Necessary()
{
var target = CreateTarget(orientation: Orientation.Horizontal);
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
var from = target.Panel.Children[9];
var result = ((ILogicalScrollable)target).GetControlInDirection(
NavigationDirection.Right,
from);
Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset);
Assert.Same(target.Panel.Children[9], result);
}
[Fact]
public void GetControlInDirection_Right_Should_Scroll_If_Partially_Visible()
{
using (UnitTestApplication.Start(TestServices.RealLayoutManager))
{
var target = CreateTarget(orientation: Orientation.Horizontal);
var scroller = (ScrollContentPresenter)target.Parent;
scroller.Measure(new Size(95, 100));
scroller.Arrange(new Rect(0, 0, 95, 100));
var from = target.Panel.Children[8];
var result = ((ILogicalScrollable)target).GetControlInDirection(
NavigationDirection.Right,
from);
Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset);
Assert.Same(target.Panel.Children[8], result);
}
}
[Fact]
public void GetControlInDirection_Left_Should_Scroll_If_Partially_Visible_Item_Is_Currently_Shown()
{
var target = CreateTarget(orientation: Orientation.Horizontal);
target.ApplyTemplate();
target.Measure(new Size(95, 100));
target.Arrange(new Rect(0, 0, 95, 100));
((ILogicalScrollable)target).Offset = new Vector(11, 0);
var from = target.Panel.Children[1];
var result = ((ILogicalScrollable)target).GetControlInDirection(
NavigationDirection.Left,
from);
Assert.Equal(new Vector(10, 0), ((ILogicalScrollable)target).Offset);
Assert.Same(target.Panel.Children[0], result);
}
}
public class WithContainers
{
[Fact]
public void Scrolling_Less_Than_A_Page_Should_Move_Recycled_Items()
{
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));
var containers = target.Panel.Children.ToList();
var scroller = (ScrollContentPresenter)target.Parent;
scroller.Offset = new Vector(0, 5);
var scrolledContainers = containers
.Skip(5)
.Take(5)
.Concat(containers.Take(5)).ToList();
Assert.Equal(new Vector(0, 5), ((ILogicalScrollable)target).Offset);
Assert.Equal(scrolledContainers, target.Panel.Children);
for (var i = 0; i < target.Panel.Children.Count; ++i)
{
Assert.Equal(items[i + 5], target.Panel.Children[i].DataContext);
}
scroller.Offset = new Vector(0, 0);
Assert.Equal(new Vector(0, 0), ((ILogicalScrollable)target).Offset);
Assert.Equal(containers, target.Panel.Children);
var dcs = target.Panel.Children.Select(x => x.DataContext).ToList();
for (var i = 0; i < target.Panel.Children.Count; ++i)
{
Assert.Equal(items[i], target.Panel.Children[i].DataContext);
}
}
[Fact]
public void Scrolling_More_Than_A_Page_Should_Recycle_Items()
{
var target = CreateTarget(itemCount: 50);
var items = (IList<string>)target.Items;
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
var containers = target.Panel.Children.ToList();
var scroller = (ScrollContentPresenter)target.Parent;
scroller.Offset = new Vector(0, 20);
Assert.Equal(new Vector(0, 20), ((ILogicalScrollable)target).Offset);
Assert.Equal(containers, target.Panel.Children);
for (var i = 0; i < target.Panel.Children.Count; ++i)
{
Assert.Equal(items[i + 20], target.Panel.Children[i].DataContext);
}
scroller.Offset = new Vector(0, 0);
Assert.Equal(new Vector(0, 0), ((ILogicalScrollable)target).Offset);
Assert.Equal(containers, target.Panel.Children);
for (var i = 0; i < target.Panel.Children.Count; ++i)
{
Assert.Equal(items[i], target.Panel.Children[i].DataContext);
}
}
}
private static ItemsPresenter CreateTarget(
Orientation orientation = Orientation.Vertical,
bool useContainers = true,
int itemCount = 20,
bool useAvaloniaList = false)
{
ItemsPresenter result;
var itemsSource = Enumerable.Range(0, itemCount).Select(x => $"Item {x}");
var items = useAvaloniaList ?
(IEnumerable)new AvaloniaList<string>(itemsSource) :
(IEnumerable)new ObservableCollection<string>(itemsSource);
var scroller = new ScrollContentPresenter
{
Content = result = new TestItemsPresenter(useContainers)
{
Items = items,
ItemsPanel = VirtualizingPanelTemplate(orientation),
ItemTemplate = ItemTemplate(),
VirtualizationMode = ItemVirtualizationMode.Simple,
}
};
scroller.UpdateChild();
return result;
}
private static IDataTemplate ItemTemplate()
{
return new FuncDataTemplate<string>(x => new Canvas
{
Width = 10,
Height = 10,
});
}
private static ITemplate<IPanel> VirtualizingPanelTemplate(
Orientation orientation = Orientation.Vertical)
{
return new FuncTemplate<IPanel>(() => new VirtualizingStackPanel
{
Orientation = orientation,
});
}
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;
}
}
}
}

132
tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs → tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs

@ -5,11 +5,12 @@ using System;
using System.Reactive.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class ScrollContentPresenterTests_IScrollable
public class ScrollContentPresenterTests_ILogicalScrollable
{
[Fact]
public void Measure_Should_Pass_Unchanged_Bounds_To_IScrollable()
@ -48,6 +49,27 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds);
}
[Fact]
public void Arrange_Should_Offset_IScrollable_Bounds_When_Logical_Scroll_Disabled()
{
var scrollable = new TestScrollable
{
IsLogicalScrollEnabled = false,
};
var target = new ScrollContentPresenter
{
Content = scrollable,
Offset = new Vector(25, 25),
};
target.UpdateChild();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Rect(-25, -25, 150, 150), scrollable.Bounds);
}
[Fact]
public void Arrange_Should_Not_Set_Viewport_And_Extent_With_IScrollable()
{
@ -169,12 +191,108 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Vector(50, 50), scrollable.Offset);
}
private class TestScrollable : Control, IScrollable
[Fact]
public void Toggling_IsLogicalScrollEnabled_Should_Update_State()
{
var scrollable = new TestScrollable
{
Extent = new Size(100, 100),
Offset = new Vector(50, 50),
Viewport = new Size(25, 25),
};
var target = new ScrollContentPresenter
{
Content = scrollable,
};
target.UpdateChild();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(scrollable.Extent, target.Extent);
Assert.Equal(scrollable.Offset, target.Offset);
Assert.Equal(scrollable.Viewport, target.Viewport);
Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds);
scrollable.IsLogicalScrollEnabled = false;
scrollable.InvalidateScroll();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Size(150, 150), target.Extent);
Assert.Equal(new Vector(0, 0), target.Offset);
Assert.Equal(new Size(100, 100), target.Viewport);
Assert.Equal(new Rect(0, 0, 150, 150), scrollable.Bounds);
scrollable.IsLogicalScrollEnabled = true;
scrollable.InvalidateScroll();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(scrollable.Extent, target.Extent);
Assert.Equal(scrollable.Offset, target.Offset);
Assert.Equal(scrollable.Viewport, target.Viewport);
Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds);
}
[Fact]
public void Changing_Content_Should_Update_State()
{
var logicalScrollable = new TestScrollable
{
Extent = new Size(100, 100),
Offset = new Vector(50, 50),
Viewport = new Size(25, 25),
};
var nonLogicalScrollable = new TestScrollable
{
IsLogicalScrollEnabled = false,
};
var target = new ScrollContentPresenter
{
Content = logicalScrollable,
};
target.UpdateChild();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(logicalScrollable.Extent, target.Extent);
Assert.Equal(logicalScrollable.Offset, target.Offset);
Assert.Equal(logicalScrollable.Viewport, target.Viewport);
Assert.Equal(new Rect(0, 0, 100, 100), logicalScrollable.Bounds);
target.Content = nonLogicalScrollable;
target.UpdateChild();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Size(150, 150), target.Extent);
Assert.Equal(new Vector(0, 0), target.Offset);
Assert.Equal(new Size(100, 100), target.Viewport);
Assert.Equal(new Rect(0, 0, 150, 150), nonLogicalScrollable.Bounds);
target.Content = logicalScrollable;
target.UpdateChild();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(logicalScrollable.Extent, target.Extent);
Assert.Equal(logicalScrollable.Offset, target.Offset);
Assert.Equal(logicalScrollable.Viewport, target.Viewport);
Assert.Equal(new Rect(0, 0, 100, 100), logicalScrollable.Bounds);
}
private class TestScrollable : Control, ILogicalScrollable
{
private Size _extent;
private Vector _offset;
private Size _viewport;
public bool IsLogicalScrollEnabled { get; set; } = true;
public Size AvailableSize { get; private set; }
public Action InvalidateScroll { get; set; }
@ -224,11 +342,21 @@ namespace Avalonia.Controls.UnitTests
}
}
public bool BringIntoView(IControl target, Rect targetRect)
{
throw new NotImplementedException();
}
protected override Size MeasureOverride(Size availableSize)
{
AvailableSize = availableSize;
return new Size(150, 150);
}
public IControl GetControlInDirection(NavigationDirection direction, IControl from)
{
throw new NotImplementedException();
}
}
}
}

16
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@ -184,23 +184,29 @@ namespace Avalonia.Controls.UnitTests
ApplyTemplate(target);
var carousel = (Carousel)target.Pages;
var dataContext = ((TextBlock)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext;
var container = (ContentPresenter)carousel.Presenter.Panel.Children.Single();
container.UpdateChild();
var dataContext = ((TextBlock)container.Child).DataContext;
Assert.Equal(items[0], dataContext);
target.SelectedIndex = 1;
dataContext = ((Button)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext;
container = (ContentPresenter)carousel.Presenter.Panel.Children.Single();
container.UpdateChild();
dataContext = ((Button)container.Child).DataContext;
Assert.Equal(items[1], dataContext);
target.SelectedIndex = 2;
dataContext = ((TextBlock)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext;
dataContext = ((TextBlock)carousel.Presenter.Panel.Children.Single()).DataContext;
Assert.Equal("Base", dataContext);
target.SelectedIndex = 3;
dataContext = ((TextBlock)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext;
container = (ContentPresenter)carousel.Presenter.Panel.Children[0];
container.UpdateChild();
dataContext = ((TextBlock)container.Child).DataContext;
Assert.Equal("Qux", dataContext);
target.SelectedIndex = 4;
dataContext = ((TextBlock)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext;
dataContext = ((TextBlock)carousel.Presenter.Panel.Children.Single()).DataContext;
Assert.Equal("Base", dataContext);
}

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

@ -462,6 +462,8 @@ namespace Avalonia.Controls.UnitTests
return new TextBlock { Text = node.Value };
}
public bool SupportsRecycling => false;
public InstancedBinding ItemsSelector(object item)
{
var obs = new ExpressionObserver(item, nameof(Node.Children));

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

@ -0,0 +1,170 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class VirtualizingStackPanelTests
{
public class Vertical
{
[Fact]
public void Measure_Invokes_Controller_UpdateControls()
{
var target = (IVirtualizingPanel)new VirtualizingStackPanel();
var controller = new Mock<IVirtualizingController>();
target.Controller = controller.Object;
target.Measure(new Size(100, 100));
controller.Verify(x => x.UpdateControls());
}
[Fact]
public void Arrange_Invokes_Controller_UpdateControls()
{
var target = (IVirtualizingPanel)new VirtualizingStackPanel();
var controller = new Mock<IVirtualizingController>();
target.Controller = controller.Object;
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 110, 110));
controller.Verify(x => x.UpdateControls());
}
[Fact]
public void Reports_IsFull_False_Until_Measure_Height_Is_Reached()
{
var target = (IVirtualizingPanel)new VirtualizingStackPanel();
target.Measure(new Size(100, 100));
Assert.Equal(new Size(0, 0), target.DesiredSize);
Assert.Equal(new Size(0, 0), target.Bounds.Size);
Assert.False(target.IsFull);
Assert.Equal(0, target.OverflowCount);
target.Children.Add(new Canvas { Width = 50, Height = 50 });
Assert.False(target.IsFull);
Assert.Equal(0, target.OverflowCount);
target.Children.Add(new Canvas { Width = 50, Height = 50 });
Assert.True(target.IsFull);
Assert.Equal(0, target.OverflowCount);
}
[Fact]
public void Reports_Overflow_After_Arrange()
{
var target = (IVirtualizingPanel)new VirtualizingStackPanel();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(new Size(0, 0), target.Bounds.Size);
target.Children.Add(new Canvas { Width = 50, Height = 50 });
target.Children.Add(new Canvas { Width = 50, Height = 50 });
target.Children.Add(new Canvas { Width = 50, Height = 50 });
target.Children.Add(new Canvas { Width = 50, Height = 50 });
Assert.Equal(0, target.OverflowCount);
target.Measure(new Size(100, 100));
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(2, target.OverflowCount);
}
[Fact]
public void Reports_Correct_Overflow_During_Arrange()
{
var target = (IVirtualizingPanel)new VirtualizingStackPanel();
var controller = new Mock<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, target.PixelOverflow);
Assert.Equal(0, target.OverflowCount);
called = true;
});
target.Controller = controller.Object;
target.Arrange(new Rect(target.DesiredSize));
Assert.True(called);
}
[Fact]
public void Reports_PixelOverflow_After_Arrange()
{
var target = (IVirtualizingPanel)new VirtualizingStackPanel();
target.Children.Add(new Canvas { Width = 50, Height = 50 });
target.Children.Add(new Canvas { Width = 50, Height = 52 });
target.Measure(new Size(100, 100));
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(2, target.PixelOverflow);
}
[Fact]
public void Reports_PixelOverflow_With_PixelOffset()
{
var target = (IVirtualizingPanel)new VirtualizingStackPanel();
target.Children.Add(new Canvas { Width = 50, Height = 50 });
target.Children.Add(new Canvas { Width = 50, Height = 52 });
target.PixelOffset = 2;
target.Measure(new Size(100, 100));
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(2, target.PixelOverflow);
}
[Fact]
public void PixelOffset_Can_Be_More_Than_Child_Without_Affecting_IsFull()
{
var target = (IVirtualizingPanel)new VirtualizingStackPanel();
target.Children.Add(new Canvas { Width = 50, Height = 50 });
target.Children.Add(new Canvas { Width = 50, Height = 52 });
target.PixelOffset = 55;
target.Measure(new Size(100, 100));
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(55, target.PixelOffset);
Assert.Equal(2, target.PixelOverflow);
Assert.True(target.IsFull);
}
[Fact]
public void Passes_Navigation_Request_To_ILogicalScrollable_Parent()
{
var presenter = new Mock<ILogical>().As<IControl>();
var scrollable = presenter.As<ILogicalScrollable>();
var target = (IVirtualizingPanel)new VirtualizingStackPanel();
var from = new Canvas();
scrollable.Setup(x => x.IsLogicalScrollEnabled).Returns(true);
((ISetLogicalParent)target).SetParent(presenter.Object);
((INavigableContainer)target).GetControl(NavigationDirection.Next, from);
scrollable.Verify(x => x.GetControlInDirection(NavigationDirection.Next, from));
}
}
}
}

44
tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs

@ -44,7 +44,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Equal(next, result);
}
@ -83,7 +83,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Equal(next, result);
}
@ -113,7 +113,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Equal(next, result);
}
@ -158,7 +158,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Equal(next, result);
}
@ -177,7 +177,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(top, FocusNavigationDirection.Down);
var result = KeyboardNavigationHandler.GetNext(top, NavigationDirection.Down);
Assert.Equal(next, result);
}
@ -224,7 +224,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Equal(next, result);
}
@ -262,7 +262,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Equal(next, result);
}
@ -300,7 +300,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Equal(next, result);
}
@ -339,7 +339,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Equal(next, result);
}
@ -378,7 +378,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Null(result);
}
@ -416,7 +416,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Null(result);
}
@ -455,7 +455,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Equal(next, result);
}
@ -494,7 +494,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Equal(next, result);
}
@ -525,7 +525,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Equal(next, result);
}
@ -570,7 +570,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Equal(next, result);
}
@ -615,7 +615,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Equal(next, result);
}
@ -635,7 +635,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Equal(top, result);
}
@ -674,7 +674,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Equal(next, result);
}
@ -713,7 +713,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Equal(next, result);
}
@ -752,7 +752,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Equal(next, result);
}
@ -790,7 +790,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Null(result);
}
@ -813,7 +813,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Null(result);
}

58
tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs

@ -42,7 +42,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Equal(next, result);
}
@ -79,7 +79,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Equal(next, result);
}
@ -123,7 +123,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Equal(next, result);
}
@ -152,7 +152,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Equal(next, result);
}
@ -195,7 +195,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Equal(next, result);
}
@ -213,7 +213,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(top, FocusNavigationDirection.Next);
var result = KeyboardNavigationHandler.GetNext(top, NavigationDirection.Next);
Assert.Equal(next, result);
}
@ -256,7 +256,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Equal(next, result);
}
@ -294,7 +294,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Equal(next, result);
}
@ -332,7 +332,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Equal(next, result);
}
@ -370,7 +370,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Equal(next, result);
}
@ -408,7 +408,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Null(result);
}
@ -446,7 +446,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Equal(next, result);
}
@ -486,7 +486,7 @@ namespace Avalonia.Input.UnitTests
KeyboardNavigation.SetTabOnceActiveElement(container, next);
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Equal(next, result);
}
@ -524,7 +524,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Equal(next, result);
}
@ -564,7 +564,7 @@ namespace Avalonia.Input.UnitTests
KeyboardNavigation.SetTabOnceActiveElement(container, next);
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Equal(next, result);
}
@ -601,7 +601,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous);
Assert.Equal(next, result);
}
@ -638,7 +638,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous);
Assert.Equal(next, result);
}
@ -667,7 +667,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous);
Assert.Equal(next, result);
}
@ -710,7 +710,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous);
Assert.Equal(next, result);
}
@ -753,7 +753,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous);
Assert.Equal(next, result);
}
@ -772,7 +772,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous);
Assert.Equal(top, result);
}
@ -810,7 +810,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous);
Assert.Equal(next, result);
}
@ -848,7 +848,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous);
Assert.Equal(next, result);
}
@ -886,7 +886,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous);
Assert.Equal(next, result);
}
@ -924,7 +924,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous);
Assert.Null(result);
}
@ -962,7 +962,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous);
Assert.Equal(next, result);
}
@ -1002,7 +1002,7 @@ namespace Avalonia.Input.UnitTests
KeyboardNavigation.SetTabOnceActiveElement(container, next);
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous);
Assert.Equal(next, result);
}
@ -1040,7 +1040,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous);
Assert.Equal(next, result);
}
@ -1063,7 +1063,7 @@ namespace Avalonia.Input.UnitTests
}
};
var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous);
Assert.Null(result);
}

8
tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs

@ -123,8 +123,8 @@ namespace Avalonia.Markup.UnitTests.Data
var target = new ExpressionSubject(
new ExpressionObserver(data, "DoubleValue"),
typeof(string),
DefaultValueConverter.Instance,
fallbackValue: "9.8");
"9.8",
DefaultValueConverter.Instance);
target.OnNext("foo");
@ -162,7 +162,7 @@ namespace Avalonia.Markup.UnitTests.Data
new ExpressionObserver(data, "DoubleValue"),
typeof(string),
converter.Object,
"foo");
converterParameter: "foo");
target.Subscribe(_ => { });
@ -178,7 +178,7 @@ namespace Avalonia.Markup.UnitTests.Data
new ExpressionObserver(data, "DoubleValue"),
typeof(string),
converter.Object,
"foo");
converterParameter: "foo");
target.OnNext("bar");

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

Loading…
Cancel
Save