Explorar o código

Merge branch 'master' into pr/553

Conflicts:
tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems
Steven Kirk %!s(int64=9) %!d(string=hai) anos
pai
achega
169ffca591
Modificáronse 100 ficheiros con 3170 adicións e 682 borrados
  1. 39 1
      Avalonia.sln
  2. 1 1
      nuget/template/Avalonia.Skia.Desktop.nuspec
  3. 1 1
      readme.md
  4. 1 2
      samples/BindingTest/App.xaml.cs
  5. 1 2
      samples/ControlCatalog.Desktop/Program.cs
  6. 1 0
      samples/ControlCatalog/Pages/MenuPage.xaml
  7. 1 2
      samples/TestApplication/Program.cs
  8. 6 0
      samples/VirtualizationTest/App.config
  9. 6 0
      samples/VirtualizationTest/App.xaml
  10. 16 0
      samples/VirtualizationTest/App.xaml.cs
  11. 51 0
      samples/VirtualizationTest/MainWindow.xaml
  12. 25 0
      samples/VirtualizationTest/MainWindow.xaml.cs
  13. 34 0
      samples/VirtualizationTest/Program.cs
  14. 36 0
      samples/VirtualizationTest/Properties/AssemblyInfo.cs
  15. 22 0
      samples/VirtualizationTest/ViewModels/ItemViewModel.cs
  16. 141 0
      samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs
  17. 180 0
      samples/VirtualizationTest/VirtualizationTest.csproj
  18. 26 0
      samples/VirtualizationTest/VirtualizationTest.v2.ncrunchproject
  19. 10 0
      samples/VirtualizationTest/packages.config
  20. 1 2
      samples/XamlTestApplication/Program.cs
  21. 34 21
      samples/XamlTestApplicationPcl/TestScrollable.cs
  22. 2 0
      samples/XamlTestApplicationPcl/Views/MainWindow.cs
  23. 1 0
      samples/XamlTestApplicationPcl/Views/MainWindow.xaml
  24. 0 1
      src/Android/Avalonia.Android/AndroidPlatform.cs
  25. 4 4
      src/Android/Avalonia.Android/Avalonia.Android.v2.ncrunchproject
  26. 2 2
      src/Avalonia.Base/AvaloniaObject.cs
  27. 51 0
      src/Avalonia.Base/Collections/AvaloniaList.cs
  28. 44 2
      src/Avalonia.Controls/AppBuilder.cs
  29. 0 51
      src/Avalonia.Controls/Application.cs
  30. 12 6
      src/Avalonia.Controls/Avalonia.Controls.csproj
  31. 1 1
      src/Avalonia.Controls/Canvas.cs
  32. 15 2
      src/Avalonia.Controls/Control.cs
  33. 21 1
      src/Avalonia.Controls/Design.cs
  34. 24 14
      src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
  35. 4 7
      src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs
  36. 107 79
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  37. 32 2
      src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
  38. 5 5
      src/Avalonia.Controls/Generators/ItemContainerInfo.cs
  39. 27 0
      src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs
  40. 3 3
      src/Avalonia.Controls/Generators/TreeContainerIndex.cs
  41. 9 4
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  42. 29 0
      src/Avalonia.Controls/IScrollable.cs
  43. 22 0
      src/Avalonia.Controls/ISetInheritanceParent.cs
  44. 22 0
      src/Avalonia.Controls/IVirtualizingController.cs
  45. 67 0
      src/Avalonia.Controls/IVirtualizingPanel.cs
  46. 21 0
      src/Avalonia.Controls/ItemVirtualizationMode.cs
  47. 23 1
      src/Avalonia.Controls/ItemsControl.cs
  48. 1 1
      src/Avalonia.Controls/LayoutTransformControl.cs
  49. 54 3
      src/Avalonia.Controls/ListBox.cs
  50. 7 0
      src/Avalonia.Controls/Menu.cs
  51. 7 0
      src/Avalonia.Controls/MenuItem.cs
  52. 1 16
      src/Avalonia.Controls/Mixins/SelectableMixin.cs
  53. 17 17
      src/Avalonia.Controls/Panel.cs
  54. 4 8
      src/Avalonia.Controls/Presenters/CarouselPresenter.cs
  55. 76 20
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  56. 2 0
      src/Avalonia.Controls/Presenters/IItemsPresenter.cs
  57. 198 0
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  58. 173 0
      src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs
  59. 504 0
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  60. 81 97
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  61. 44 10
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  62. 82 38
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  63. 1 1
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  64. 69 0
      src/Avalonia.Controls/Primitives/ILogicalScrollable.cs
  65. 0 54
      src/Avalonia.Controls/Primitives/IScrollable.cs
  66. 43 4
      src/Avalonia.Controls/Primitives/Popup.cs
  67. 25 0
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  68. 4 0
      src/Avalonia.Controls/Primitives/TabStrip.cs
  69. 2 2
      src/Avalonia.Controls/ScrollViewer.cs
  70. 38 13
      src/Avalonia.Controls/StackPanel.cs
  71. 0 48
      src/Avalonia.Controls/Templates/DataTemplateExtensions.cs
  72. 34 5
      src/Avalonia.Controls/Templates/FuncDataTemplate.cs
  73. 9 4
      src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs
  74. 3 0
      src/Avalonia.Controls/Templates/FuncTemplate`1.cs
  75. 6 0
      src/Avalonia.Controls/Templates/IDataTemplate.cs
  76. 4 2
      src/Avalonia.Controls/Templates/ITemplate`1.cs
  77. 12 7
      src/Avalonia.Controls/Utils/IEnumerableUtils.cs
  78. 229 0
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  79. 10 1
      src/Avalonia.Controls/Window.cs
  80. 9 9
      src/Avalonia.Controls/WrapPanel.cs
  81. 44 8
      src/Avalonia.DesignerSupport/DesignerApi.cs
  82. 53 11
      src/Avalonia.DesignerSupport/DesignerAssist.cs
  83. 2 0
      src/Avalonia.Diagnostics/ViewLocator.cs
  84. 2 2
      src/Avalonia.Input/Avalonia.Input.csproj
  85. 1 1
      src/Avalonia.Input/IKeyboardNavigationHandler.cs
  86. 1 1
      src/Avalonia.Input/INavigableContainer.cs
  87. 5 6
      src/Avalonia.Input/InputExtensions.cs
  88. 23 11
      src/Avalonia.Input/KeyboardNavigationHandler.cs
  89. 13 14
      src/Avalonia.Input/Navigation/DirectionalNavigation.cs
  90. 13 13
      src/Avalonia.Input/Navigation/TabNavigation.cs
  91. 12 2
      src/Avalonia.Input/NavigationDirection.cs
  92. 26 9
      src/Avalonia.Layout/LayoutManager.cs
  93. 2 1
      src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs
  94. 13 3
      src/Avalonia.SceneGraph/Rect.cs
  95. 2 2
      src/Avalonia.SceneGraph/Rendering/RendererMixin.cs
  96. 6 6
      src/Avalonia.SceneGraph/Visual.cs
  97. 2 2
      src/Avalonia.SceneGraph/VisualTree/IVisual.cs
  98. 10 12
      src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs
  99. 10 1
      src/Avalonia.Styling/Avalonia.Styling.csproj
  100. 0 0
      src/Avalonia.Styling/Controls/INameScope.cs

+ 39 - 1
Avalonia.sln

@@ -1,6 +1,6 @@
 Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio 14
-VisualStudioVersion = 14.0.25123.0
+VisualStudioVersion = 14.0.24720.0
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}"
 EndProject
@@ -15,6 +15,9 @@ EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Direct2D1", "src\Windows\Avalonia.Direct2D1\Avalonia.Direct2D1.csproj", "{3E908F67-5543-4879-A1DC-08EACE79B3CD}"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Designer", "src\Windows\Avalonia.Designer\Avalonia.Designer.csproj", "{EC42600F-049B-43FF-AED1-8314D61B2749}"
+	ProjectSection(ProjectDependencies) = postProject
+		{2B888490-D14A-4BCA-AB4B-48676FA93C9B} = {2B888490-D14A-4BCA-AB4B-48676FA93C9B}
+	EndProjectSection
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Input", "src\Avalonia.Input\Avalonia.Input.csproj", "{62024B2D-53EB-4638-B26B-85EEAA54866E}"
 EndProject
@@ -158,6 +161,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DesignerSupport.Te
 EndProject
 Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Avalonia.RenderTests", "tests\Avalonia.RenderTests\Avalonia.RenderTests.shproj", "{48840EDD-24BF-495D-911E-2EB12AE75D3B}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VirtualizationTest", "samples\VirtualizationTest\VirtualizationTest.csproj", "{FBCAF3D0-2808-4934-8E96-3F607594517B}"
+EndProject
 Global
 	GlobalSection(SharedMSBuildProjectFiles) = preSolution
 		src\Shared\RenderHelpers\RenderHelpers.projitems*{fb05ac90-89ba-4f2f-a924-f37875fb547c}*SharedItemsImports = 4
@@ -1909,6 +1914,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 +1997,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 - 1
nuget/template/Avalonia.Skia.Desktop.nuspec

@@ -13,7 +13,7 @@
     <copyright>Copyright 2015</copyright>
     <tags>Avalonia</tags>
     <dependencies>
-      <dependency id="SkiaSharp" version="1.49.2.1-beta"/>
+      <dependency id="SkiaSharp" version="1.49.3.0-beta"/>
       <dependency id="Avalonia" version="#VERSION#" />
     </dependencies>
   </metadata>

+ 1 - 1
readme.md

@@ -45,7 +45,7 @@ framework.
 As mentioned above, Avalonia is still in alpha and as such there's not much documentation yet. You can
 take a look at the [getting started page](docs/tutorial/gettingstarted.md) for an
 overview of how to get started but probably the best thing to do for now is to already know a little bit
-about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/Avalonia/Avalonia).
+about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia).
 
 There's also a high-level [architecture document](docs/spec/architecture.md) that is currently a little bit
 out of date, and I've also started writing blog posts on Avalonia at http://grokys.github.io/.

+ 1 - 2
samples/BindingTest/App.xaml.cs

@@ -19,8 +19,7 @@ namespace BindingTest
             InitializeLogging();
 
             AppBuilder.Configure<App>()
-                .UseWin32()
-                .UseDirect2D1()
+                .UsePlatformDetect()
                 .Start<MainWindow>();
         }
 

+ 1 - 2
samples/ControlCatalog.Desktop/Program.cs

@@ -17,8 +17,7 @@ namespace ControlCatalog
             // TODO: Make this work with GTK/Skia/Cairo depending on command-line args
             // again.
             AppBuilder.Configure<App>()
-                .UseWin32()
-                .UseDirect2D1()
+                .UsePlatformDetect()
                 .Start<MainWindow>();
         }
 

+ 1 - 0
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"/>

+ 1 - 2
samples/TestApplication/Program.cs

@@ -35,8 +35,7 @@ namespace TestApplication
             var app = new App();
 
             AppBuilder.Configure(app)
-                .UseWin32()
-                .UseDirect2D1()
+                .UsePlatformDetect()
                 .SetupWithoutStarting();
 
             app.Run();

+ 6 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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>

+ 1 - 2
samples/XamlTestApplication/Program.cs

@@ -21,8 +21,7 @@ namespace XamlTestApplication
             InitializeLogging();
 
             AppBuilder.Configure<XamlTestApp>()
-                .UseWin32()
-                .UseDirect2D1()
+                .UsePlatformDetect()
                 .Start<Views.MainWindow>();
         }
 

+ 34 - 21
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;
-                }
-            }
-        }
     }
 }

+ 2 - 0
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;

+ 1 - 0
samples/XamlTestApplicationPcl/Views/MainWindow.xaml

@@ -24,6 +24,7 @@
       <TabControl.Transition>
         <PageSlide Duration="0.25" />
       </TabControl.Transition>
+     
       <TabItem Header="Buttons">
         <ScrollViewer CanScrollHorizontally="False">
           <StackPanel Margin="10" Gap="4">

+ 0 - 1
src/Android/Avalonia.Android/AndroidPlatform.cs

@@ -38,7 +38,6 @@ namespace Avalonia.Android
                 .Bind<IWindowingPlatform>().ToConstant(this);
 
             SkiaPlatform.Initialize();
-            Application.RegisterPlatformCallback(() => { });
 
             _scalingFactor = global::Android.App.Application.Context.Resources.DisplayMetrics.ScaledDensity;
 

+ 4 - 4
src/Android/Avalonia.Android/Avalonia.Android.v2.ncrunchproject

@@ -7,7 +7,7 @@
   <AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking>
   <AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking>
   <AllowCodeAnalysis>false</AllowCodeAnalysis>
-  <IgnoreThisComponentCompletely>false</IgnoreThisComponentCompletely>
+  <IgnoreThisComponentCompletely>true</IgnoreThisComponentCompletely>
   <RunPreBuildEvents>false</RunPreBuildEvents>
   <RunPostBuildEvents>false</RunPostBuildEvents>
   <PreviouslyBuiltSuccessfully>true</PreviouslyBuiltSuccessfully>
@@ -17,9 +17,9 @@
   <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>

+ 2 - 2
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 - 0
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>

+ 44 - 2
src/Avalonia.Controls/AppBuilder.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.Reflection;
 
 namespace Avalonia.Controls
 {
@@ -98,23 +99,64 @@ namespace Avalonia.Controls
         /// </summary>
         /// <param name="initializer">The method to call to initialize the windowing subsystem.</param>
         /// <returns>An <see cref="AppBuilder"/> instance.</returns>
-        public AppBuilder WithWindowingSubsystem(Action initializer)
+        public AppBuilder UseWindowingSubsystem(Action initializer)
         {
             WindowingSubsystem = initializer;
             return this;
         }
 
+        /// <summary>
+        /// Specifies a windowing subsystem to use.
+        /// </summary>
+        /// <param name="dll">The dll in which to look for subsystem.</param>
+        /// <returns>An <see cref="AppBuilder"/> instance.</returns>
+        public AppBuilder UseWindowingSubsystem(string dll) => UseWindowingSubsystem(GetInitializer(dll));
+
         /// <summary>
         /// Specifies a rendering subsystem to use.
         /// </summary>
         /// <param name="initializer">The method to call to initialize the rendering subsystem.</param>
         /// <returns>An <see cref="AppBuilder"/> instance.</returns>
-        public AppBuilder WithRenderingSubsystem(Action initializer)
+        public AppBuilder UseRenderingSubsystem(Action initializer)
         {
             RenderingSubsystem = initializer;
             return this;
         }
 
+        /// <summary>
+        /// Specifies a rendering subsystem to use.
+        /// </summary>
+        /// <param name="dll">The dll in which to look for subsystem.</param>
+        /// <returns>An <see cref="AppBuilder"/> instance.</returns>
+        public AppBuilder UseRenderingSubsystem(string dll) => UseRenderingSubsystem(GetInitializer(dll));
+
+        static Action GetInitializer(string assemblyName) => () =>
+        {
+            var assembly = Assembly.Load(new AssemblyName(assemblyName));
+            var platformClassName = assemblyName.Replace("Avalonia.", string.Empty) + "Platform";
+            var platformClassFullName = assemblyName + "." + platformClassName;
+            var platformClass = assembly.GetType(platformClassFullName);
+            var init = platformClass.GetRuntimeMethod("Initialize", new Type[0]);
+            init.Invoke(null, null);
+        };
+
+        public AppBuilder UsePlatformDetect()
+        {
+            var platformId = (int)
+                ((dynamic) Type.GetType("System.Environment").GetRuntimeProperty("OSVersion").GetValue(null)).Platform;
+            if (platformId == 4 || platformId == 6)
+            {
+                UseRenderingSubsystem("Avalonia.Cairo");
+                UseWindowingSubsystem("Avalonia.Gtk");
+            }
+            else
+            {
+                UseRenderingSubsystem("Avalonia.Direct2D1");
+                UseWindowingSubsystem("Avalonia.Win32");
+            }
+            return this;
+        }
+
         /// <summary>
         /// Sets up the platform-speciic services for the <see cref="Application"/>.
         /// </summary>

+ 0 - 51
src/Avalonia.Controls/Application.cs

@@ -26,16 +26,12 @@ namespace Avalonia
     /// - A global set of <see cref="Styles"/>.
     /// - A <see cref="FocusManager"/>.
     /// - An <see cref="InputManager"/>.
-    /// - Loads and initializes rendering and windowing subsystems with
-    /// <see cref="InitializeSubsystems(int)"/> and <see cref="InitializeSubsystem(string)"/>.
     /// - Registers services needed by the rest of Avalonia in the <see cref="RegisterServices"/>
     /// method.
     /// - Tracks the lifetime of the application.
     /// </remarks>
     public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IApplicationLifecycle
     {
-        static Action _platformInitializationCallback;
-
         /// <summary>
         /// The application-global data templates.
         /// </summary>
@@ -121,11 +117,6 @@ namespace Avalonia
         /// </summary>
         IStyleHost IStyleHost.StylingParent => null;
 
-        public static void RegisterPlatformCallback(Action cb)
-        {
-            _platformInitializationCallback = cb;
-        }
-
         /// <summary>
         /// Initializes the application by loading XAML etc.
         /// </summary>
@@ -189,47 +180,5 @@ namespace Avalonia
                 .Bind<IRenderQueueManager>().ToTransient<RenderQueueManager>()
                 .Bind<IApplicationLifecycle>().ToConstant(this);
         }
-
-        /// <summary>
-        /// Initializes the rendering and windowing subsystems according to platform.
-        /// </summary>
-        /// <param name="platformID">The value of Environment.OSVersion.Platform.</param>
-        protected void InitializeSubsystems(int platformID)
-        {
-            if (_platformInitializationCallback != null)
-            {
-                _platformInitializationCallback();
-            }
-            else if (platformID == 4 || platformID == 6)
-            {
-                InitializeSubsystem("Avalonia.Cairo");
-                InitializeSubsystem("Avalonia.Gtk");
-            }
-            else
-            {
-                InitializeSubsystem("Avalonia.Direct2D1");
-                InitializeSubsystem("Avalonia.Win32");
-            }
-        }
-
-        /// <summary>
-        /// Initializes the rendering or windowing subsystem defined by the specified assemblt.
-        /// </summary>
-        /// <param name="assemblyName">The name of the assembly.</param>
-        protected static void InitializeSubsystem(string assemblyName)
-        {
-            var assembly = Assembly.Load(new AssemblyName(assemblyName));
-            var platformClassName = assemblyName.Replace("Avalonia.", string.Empty) + "Platform";
-            var platformClassFullName = assemblyName + "." + platformClassName;
-            var platformClass = assembly.GetType(platformClassFullName);
-            var init = platformClass.GetRuntimeMethod("Initialize", new Type[0]);
-            init.Invoke(null, null);
-        }
-
-        internal static void InitializeWin32Subsystem()
-        {
-            InitializeSubsystem("Avalonia.Direct2D1");
-            InitializeSubsystem("Avalonia.Win32");
-        }
     }
 }

+ 12 - 6
src/Avalonia.Controls/Avalonia.Controls.csproj

@@ -50,26 +50,31 @@
     <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="INameScope.cs" />
+    <Compile Include="IScrollable.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" />
-    <Compile Include="NameScopeEventArgs.cs" />
-    <Compile Include="NameScopeExtensions.cs" />
     <Compile Include="Platform\ITopLevelRenderer.cs" />
     <Compile Include="Platform\IWindowingPlatform.cs" />
     <Compile Include="Platform\PlatformManager.cs" />
     <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 +175,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" />

+ 1 - 1
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;

+ 15 - 2
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>

+ 21 - 1
src/Avalonia.Controls/Design.cs

@@ -1,4 +1,6 @@
 
+using System.Runtime.CompilerServices;
+
 namespace Avalonia.Controls
 {
     public static class Design
@@ -43,7 +45,25 @@ namespace Avalonia.Controls
         {
             return control.GetValue(DataContextProperty);
         }
-        
+
+        static readonly ConditionalWeakTable<object, Control> Substitutes = new ConditionalWeakTable<object, Control>();
+
+        public static readonly AttachedProperty<Control> PreviewWithProperty = AvaloniaProperty
+            .RegisterAttached<AvaloniaObject, Control>("PreviewWith", typeof (Design));
+
+        public static void SetPreviewWith(object target, Control control)
+        {
+            Substitutes.Remove(target);
+            Substitutes.Add(target, control);
+        }
+
+        public static Control GetPreviewWith(object target)
+        {
+            Control rv;
+            Substitutes.TryGetValue(target, out rv);
+            return rv;
+        }
+
         internal static void ApplyDesignerProperties(Control target, Control source)
         {
             if (source.IsSet(WidthProperty))

+ 24 - 14
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.

+ 4 - 7
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.

+ 107 - 79
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@@ -2,11 +2,11 @@
 // 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;
+using Avalonia.Data;
 
 namespace Avalonia.Controls.Generators
 {
@@ -15,7 +15,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 +29,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 +37,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 +51,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 +84,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 +132,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 +158,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 +184,40 @@ 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();
+                result.SetValue(ContentPresenter.ContentProperty, item, BindingPriority.Style);
+
+                if (ItemTemplate != null)
+                {
+                    result.SetValue(
+                        ContentPresenter.ContentTemplateProperty,
+                        ItemTemplate,
+                        BindingPriority.TemplatedParent);
+                }
             }
 
             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 +226,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);
         }
     }
 }

+ 32 - 2
src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs

@@ -5,6 +5,7 @@ using System;
 using System.Linq.Expressions;
 using System.Reflection;
 using Avalonia.Controls.Templates;
+using Avalonia.Data;
 
 namespace Avalonia.Controls.Generators
 {
@@ -62,10 +63,10 @@ namespace Avalonia.Controls.Generators
 
                 if (ContentTemplateProperty != null)
                 {
-                    result.SetValue(ContentTemplateProperty, ItemTemplate);
+                    result.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style);
                 }
 
-                result.SetValue(ContentProperty, item);
+                result.SetValue(ContentProperty, item, BindingPriority.Style);
 
                 if (!(item is IControl))
                 {
@@ -75,5 +76,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;
+        }
     }
 }

+ 5 - 5
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 - 0
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);
+        }
+    }
+}

+ 3 - 3
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)
             {

+ 9 - 4
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

@@ -78,7 +78,7 @@ namespace Avalonia.Controls.Generators
                 var template = GetTreeDataTemplate(item, ItemTemplate);
                 var result = new T();
 
-                result.SetValue(ContentProperty, template.Build(item));
+                result.SetValue(ContentProperty, template.Build(item), BindingPriority.Style);
 
                 var itemsSelector = template.ItemsSelector(item);
 
@@ -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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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,
+    }
+}

+ 23 - 1
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)
         {

+ 1 - 1
src/Avalonia.Controls/LayoutTransformControl.cs

@@ -139,7 +139,7 @@ namespace Avalonia.Controls
             if (null != TransformRoot)
             {
                 TransformRoot.RenderTransform = _matrixTransform;
-                TransformRoot.TransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute);
+                TransformRoot.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute);
             }
 
             ApplyLayoutTransform();

+ 54 - 3
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 - 0
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 - 0
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>

+ 1 - 16
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
                     {

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

+ 4 - 8
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;

+ 76 - 20
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,34 +217,86 @@ namespace Avalonia.Controls.Presenters
         /// </remarks>
         public void UpdateChild()
         {
-            var old = Child;
             var content = Content;
-            var result = this.MaterializeDataTemplate(content, ContentTemplate);
+            var oldChild = Child;
+            var newChild = content as IControl;
+
+            if (content != null && newChild == null)
+            {
+                // 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))
+                {
+                    newChild = oldChild;
+                }
+                else
+                {
+                    // 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());
+                    }
+                }
+            }
+            else
+            {
+                _dataTemplate = null;
+            }
+
+            // Remove the old child if we're not recycling it.
+            if (oldChild != null && newChild != oldChild)
+            {
+                VisualChildren.Remove(oldChild);
+            }
 
-            if (old != null)
+            // Set the DataContext if the data isn't a control.
+            if (!(content is IControl))
             {
-                VisualChildren.Remove(old);
+                DataContext = content;
             }
 
-            if (result != null)
+            // Update the Child.
+            if (newChild == null)
+            {
+                Child = null;
+            }
+            else if (newChild != oldChild)
             {
-                if (!(content is IControl))
+                ((ISetInheritanceParent)newChild).SetParent(this);
+
+                Child = newChild;
+
+                if (oldChild?.Parent == this)
                 {
-                    result.DataContext = content;
+                    LogicalChildren.Remove(oldChild);
                 }
 
-                Child = result;
-
-                if (result.Parent == null)
+                if (newChild.Parent == null)
                 {
-                    ((ISetLogicalParent)result).SetParent((ILogical)this.TemplatedParent ?? this);
+                    var templatedLogicalParent = TemplatedParent as ILogical;
+
+                    if (templatedLogicalParent != null)
+                    {
+                        ((ISetLogicalParent)newChild).SetParent(templatedLogicalParent);
+                    }
+                    else
+                    {
+                        LogicalChildren.Add(newChild);
+                    }
                 }
 
-                VisualChildren.Add(result);
-            }
-            else
-            {
-                Child = null;
+                VisualChildren.Add(newChild);
             }
 
             _createdChild = true;

+ 2 - 0
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 - 0
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();
+    }
+}

+ 173 - 0
src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs

@@ -0,0 +1,173 @@
+// 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)
+        {
+            if (Items != null)
+            {
+                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);
+                }
+            }
+        }
+    }
+}

+ 504 - 0
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@@ -0,0 +1,504 @@
+// 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);
+
+            var panel = VirtualizingPanel;
+
+            if (items != null)
+            {
+                switch (e.Action)
+                {
+                    case NotifyCollectionChangedAction.Add:
+                        CreateAndRemoveContainers();
+
+                        if (e.NewStartingIndex >= FirstIndex &&
+                            e.NewStartingIndex + e.NewItems.Count <= NextIndex)
+                        {
+                            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();
+                        CreateAndRemoveContainers();
+                        break;
+                }
+            }
+            else
+            {
+                Owner.ItemContainerGenerator.Clear();
+                VirtualizingPanel.Children.Clear();
+                FirstIndex = NextIndex = 0;
+            }
+
+            // If we are scrolled to view a partially visible last item but controls were added
+            // then we need to return to a non-offset scroll position.
+            if (panel.PixelOffset != 0 && FirstIndex + panel.Children.Count < ItemCount)
+            {
+                panel.PixelOffset = 0;
+                RecycleContainersForMove(1);
+            }
+
+            InvalidateScroll();
+        }
+
+        public override IControl GetControlInDirection(NavigationDirection direction, IControl from)
+        {
+            var generator = Owner.ItemContainerGenerator;
+            var panel = VirtualizingPanel;
+            var itemIndex = generator.IndexFromContainer(from);
+            var vertical = VirtualizingPanel.ScrollDirection == Orientation.Vertical;
+
+            if (itemIndex == -1)
+            {
+                return null;
+            }
+
+            var newItemIndex = -1;
+
+            switch (direction)
+            {
+                case NavigationDirection.First:
+                    newItemIndex = 0;
+                    break;
+
+                case NavigationDirection.Last:
+                    newItemIndex = ItemCount - 1;
+                    break;
+
+                case NavigationDirection.Up:
+                    if (vertical)
+                    {
+                        newItemIndex = itemIndex - 1;
+                    }
+
+                    break;
+                case NavigationDirection.Down:
+                    if (vertical)
+                    {
+                        newItemIndex = itemIndex + 1;
+                    }
+
+                    break;
+
+                case NavigationDirection.Left:
+                    if (!vertical)
+                    {
+                        newItemIndex = itemIndex - 1;
+                    }
+                    break;
+
+                case NavigationDirection.Right:
+                    if (!vertical)
+                    {
+                        newItemIndex = itemIndex + 1;
+                    }
+                    break;
+
+                case NavigationDirection.PageUp:
+                    newItemIndex = Math.Max(0, itemIndex - (int)ViewportValue);
+                    break;
+
+                case NavigationDirection.PageDown:
+                    newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue);
+                    break;
+            }
+
+            return 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);
+        }
+    }
+}

+ 81 - 97
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(1, 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();
         }
     }
 }

+ 44 - 10
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>

+ 82 - 38
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;
             }
 
@@ -218,26 +231,32 @@ namespace Avalonia.Controls.Presenters
         /// <inheritdoc/>
         protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
         {
-            if (Extent.Height > Viewport.Height)
+            if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width)
             {
-                var scrollable = Child as IScrollable;
+                var scrollable = Child as ILogicalScrollable;
+                bool isLogical = scrollable?.IsLogicalScrollEnabled == true;
+
+                double x = Offset.X;
+                double y = Offset.Y;
 
-                if (scrollable != null)
-                {                    
-                    var y = Offset.Y + (-e.Delta.Y * scrollable.ScrollSize.Height);
+                if (Extent.Height > Viewport.Height)
+                {
+                    double height = isLogical ? scrollable.ScrollSize.Height : 50;
+                    y += -e.Delta.Y * height;
                     y = Math.Max(y, 0);
                     y = Math.Min(y, Extent.Height - Viewport.Height);
-                    Offset = new Vector(Offset.X, y);
-                    e.Handled = true;
                 }
-                else
+
+                if (Extent.Width > Viewport.Width)
                 {
-                    var y = Offset.Y + (-e.Delta.Y * 50);
-                    y = Math.Max(y, 0);
-                    y = Math.Min(y, Extent.Height - Viewport.Height);
-                    Offset = new Vector(Offset.X, y);
-                    e.Handled = true;
+                    double width = isLogical ? scrollable.ScrollSize.Width : 50;
+                    x += -e.Delta.X * width;
+                    x = Math.Max(x, 0);
+                    x = Math.Min(x, Extent.Width - Viewport.Width);
                 }
+
+                Offset = new Vector(x, y);
+                e.Handled = true;
             }
         }
 
@@ -246,28 +265,53 @@ namespace Avalonia.Controls.Presenters
             e.Handled = BringDescendentIntoView(e.TargetObject, e.TargetRect);
         }
 
-        private void ChildChanged(IControl child)
+        private void ChildChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            UpdateScrollableSubscription((IControl)e.NewValue);
+
+            if (e.OldValue != null)
+            {
+                Offset = default(Vector);
+            }
+        }
+
+        private void UpdateScrollableSubscription(IControl child)
         {
-            var scrollable = child as IScrollable;
+            var scrollable = child as ILogicalScrollable;
 
-            _scrollableSubscription?.Dispose();
-            _scrollableSubscription = null;
+            _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;
+            }
         }
     }
-}
+}

+ 1 - 1
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@@ -60,7 +60,7 @@ namespace Avalonia.Controls.Primitives
                 if (info != null)
                 {
                     child.RenderTransform = new MatrixTransform(info.Bounds.Transform);
-                    child.TransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute);
+                    child.RenderTransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute);
                     child.Arrange(info.Bounds.Bounds);
                 }
                 else

+ 69 - 0
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);
+    }
+}

+ 0 - 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; }
-    }
-}

+ 43 - 4
src/Avalonia.Controls/Primitives/Popup.cs

@@ -10,6 +10,7 @@ using Avalonia.LogicalTree;
 using Avalonia.Metadata;
 using Avalonia.Rendering;
 using Avalonia.VisualTree;
+using Avalonia.Layout;
 
 namespace Avalonia.Controls.Primitives
 {
@@ -39,6 +40,18 @@ namespace Avalonia.Controls.Primitives
         public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
             AvaloniaProperty.Register<Popup, PlacementMode>(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
 
+        /// <summary>
+        /// Defines the <see cref="HorizontalOffset"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> HorizontalOffsetProperty =
+            AvaloniaProperty.Register<Popup, double>(nameof(HorizontalOffset));
+
+        /// <summary>
+        /// Defines the <see cref="VerticalOffset"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> VerticalOffsetProperty =
+            AvaloniaProperty.Register<Popup, double>(nameof(VerticalOffset));
+
         /// <summary>
         /// Defines the <see cref="PlacementTarget"/> property.
         /// </summary>
@@ -122,6 +135,24 @@ namespace Avalonia.Controls.Primitives
             set { SetValue(PlacementModeProperty, value); }
         }
 
+        /// <summary>
+        /// Gets or sets the Horizontal offset of the popup in relation to the <see cref="PlacementTarget"/>
+        /// </summary>
+        public double HorizontalOffset
+        {
+            get { return GetValue(HorizontalOffsetProperty); }
+            set { SetValue(HorizontalOffsetProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the Vertical offset of the popup in relation to the <see cref="PlacementTarget"/>
+        /// </summary>
+        public double VerticalOffset
+        {
+            get { return GetValue(VerticalOffsetProperty); }
+            set { SetValue(VerticalOffsetProperty, value); }
+        }
+
         /// <summary>
         /// Gets or sets the control that is used to determine the popup's position.
         /// </summary>
@@ -287,18 +318,26 @@ namespace Avalonia.Controls.Primitives
             if (target?.GetVisualRoot() == null)
             {
                 mode = PlacementMode.Pointer;
-            }
+            }            
 
             switch (mode)
             {
                 case PlacementMode.Pointer:
-                    return MouseDevice.Instance?.Position ?? default(Point);
+                    if (MouseDevice.Instance != null)
+                    {
+                        // Scales the Horizontal and Vertical offset to screen co-ordinates.
+                        var screenOffset = new Point(HorizontalOffset * (PopupRoot as ILayoutRoot).LayoutScaling, VerticalOffset * (PopupRoot as ILayoutRoot).LayoutScaling);
+                        return MouseDevice.Instance.Position + screenOffset;
+                    }
+
+                    return default(Point);
 
                 case PlacementMode.Bottom:
-                    return target?.PointToScreen(new Point(0, target.Bounds.Height)) ?? zero;
+
+                    return target?.PointToScreen(new Point(0 + HorizontalOffset, target.Bounds.Height + VerticalOffset)) ?? zero;
 
                 case PlacementMode.Right:
-                    return target?.PointToScreen(new Point(target.Bounds.Width, 0)) ?? zero;
+                    return target?.PointToScreen(new Point(target.Bounds.Width + HorizontalOffset, 0 + VerticalOffset)) ?? zero;
 
                 default:
                     throw new InvalidOperationException("Invalid value for Popup.PlacementMode");

+ 25 - 0
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;
 

+ 4 - 0
src/Avalonia.Controls/Primitives/TabStrip.cs

@@ -9,6 +9,9 @@ namespace Avalonia.Controls.Primitives
 {
     public class TabStrip : SelectingItemsControl
     {
+        private static readonly FuncTemplate<IPanel> DefaultPanel =
+            new FuncTemplate<IPanel>(() => new WrapPanel { Orientation = Orientation.Horizontal });
+
         private static IMemberSelector s_MemberSelector = new FuncMemberSelector<object, object>(SelectHeader);
 
         static TabStrip()
@@ -16,6 +19,7 @@ namespace Avalonia.Controls.Primitives
             MemberSelectorProperty.OverrideDefaultValue<TabStrip>(s_MemberSelector);
             SelectionModeProperty.OverrideDefaultValue<TabStrip>(SelectionMode.AlwaysSelected);
             FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false);
+            ItemsPanelProperty.OverrideDefaultValue<TabStrip>(DefaultPanel);
         }
 
         protected override IItemContainerGenerator CreateItemContainerGenerator()

+ 2 - 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.
@@ -370,7 +370,7 @@ namespace Avalonia.Controls
         private static double Max(double x, double y)
         {
             var result = Math.Max(x, y);
-            return double.IsNaN(result) ? 0 : Math.Round(result);
+            return double.IsNaN(result) ? 0 : result;
         }
 
         private static Vector ValidateOffset(AvaloniaObject o, Vector value)

+ 38 - 13
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);
+        }
     }
 }

+ 0 - 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>

+ 34 - 5
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>

+ 9 - 4
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)
         {
         }
 

+ 3 - 0
src/Avalonia.Controls/Templates/FuncTemplate`1.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 Avalonia.Styling;
 
 namespace Avalonia.Controls.Templates
 {
@@ -34,5 +35,7 @@ namespace Avalonia.Controls.Templates
         {
             return _func();
         }
+
+        object ITemplate.Build() => Build();
     }
 }

+ 6 - 0
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>

+ 4 - 2
src/Avalonia.Controls/Templates/ITemplate`1.cs

@@ -1,13 +1,15 @@
 // 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.Styling;
+
 namespace Avalonia.Controls
 {
     /// <summary>
     /// Creates a control.
     /// </summary>
     /// <typeparam name="TControl">The type of control.</typeparam>
-    public interface ITemplate<TControl> where TControl : IControl
+    public interface ITemplate<TControl> : ITemplate where TControl : IControl
     {
         /// <summary>
         /// Creates the control.
@@ -15,6 +17,6 @@ namespace Avalonia.Controls
         /// <returns>
         /// The created control.
         /// </returns>
-        TControl Build();
+        new TControl Build();
     }
 }

+ 12 - 7
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 - 0
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;
+        }
+    }
+}

+ 10 - 1
src/Avalonia.Controls/Window.cs

@@ -89,7 +89,16 @@ namespace Avalonia.Controls
         /// Initializes a new instance of the <see cref="Window"/> class.
         /// </summary>
         public Window()
-            : base(PlatformManager.CreateWindow())
+            : this(PlatformManager.CreateWindow())
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Window"/> class.
+        /// </summary>
+        /// <param name="impl">The window implementation.</param>
+        public Window(IWindowImpl impl)
+            : base(impl)
         {
             _maxPlatformClientSize = this.PlatformImpl.MaxClientSize;
         }

+ 9 - 9
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;
             }

+ 44 - 8
src/Avalonia.DesignerSupport/DesignerApi.cs

@@ -7,33 +7,42 @@ using System.Threading.Tasks;
 
 namespace Avalonia.DesignerSupport
 {
-    class DesignerApi
+    class DesignerApiDictionary
     {
-        private readonly Dictionary<string, object> _inner;
+        public Dictionary<string, object> Dictionary { get; set; }
 
-        public DesignerApi(Dictionary<string, object> inner)
+        public DesignerApiDictionary(Dictionary<string, object> dictionary)
         {
-            _inner = inner;
+            Dictionary = dictionary;
         }
 
-        object Get([CallerMemberName] string name = null)
+        protected object Get([CallerMemberName] string name = null)
         {
             object rv;
-            _inner.TryGetValue(name, out rv);
+            Dictionary.TryGetValue(name, out rv);
             return rv;
         }
 
-        void Set(object value, [CallerMemberName] string name = null)
+        protected void Set(object value, [CallerMemberName] string name = null)
         {
-            _inner[name] = value;
+            Dictionary[name] = value;
         }
+    }
 
+    class DesignerApi : DesignerApiDictionary
+    {
         public Action<string> UpdateXaml
         {
             get { return (Action<string>) Get(); }
             set {Set(value); }
         }
 
+        public Action<Dictionary<string, object>> UpdateXaml2
+        {
+            get { return (Action<Dictionary<string, object>>)Get(); }
+            set { Set(value); }
+        }
+
         public Action OnResize
         {
             get { return (Action) Get(); }
@@ -52,5 +61,32 @@ namespace Avalonia.DesignerSupport
             get { return (Action<double>) Get(); }
         }
 
+        public DesignerApi(Dictionary<string, object> dictionary) : base(dictionary)
+        {
+        }
+    }
+
+    class DesignerApiXamlFileInfo : DesignerApiDictionary
+    {
+        public string Xaml
+        {
+            get { return (string)Get(); }
+            set { Set(value); }
+        }
+
+        public string AssemblyPath
+        {
+            get { return (string) Get(); }
+            set { Set(value); }
+        }
+
+        public DesignerApiXamlFileInfo(Dictionary<string, object> dictionary) : base(dictionary)
+        {
+        }
+
+        public DesignerApiXamlFileInfo(): base(new Dictionary<string, object>())
+        {
+            
+        }
     }
 }

+ 53 - 11
src/Avalonia.DesignerSupport/DesignerAssist.cs

@@ -36,7 +36,7 @@ namespace Avalonia.DesignerSupport
         public static void Init(Dictionary<string, object> shared)
         {
             Design.IsDesignMode = true;
-            Api = new DesignerApi(shared) {UpdateXaml = UpdateXaml, SetScalingFactor = SetScalingFactor};
+            Api = new DesignerApi(shared) {UpdateXaml = UpdateXaml, UpdateXaml2 = UpdateXaml2, SetScalingFactor = SetScalingFactor};
             var plat = (IPclPlatformWrapper) Activator.CreateInstance(Assembly.Load(new AssemblyName("Avalonia.Win32"))
                 .DefinedTypes.First(typeof (IPclPlatformWrapper).GetTypeInfo().IsAssignableFrom).AsType());
             
@@ -58,10 +58,9 @@ namespace Avalonia.DesignerSupport
                     //Ignore, Assembly.DefinedTypes threw an exception, we can't do anything about that
                 }
             }
-
             AppBuilder.Configure(app == null ? new DesignerApp() : (Application) Activator.CreateInstance(app.AsType()))
-                .WithWindowingSubsystem(Application.InitializeWin32Subsystem)
-                .WithRenderingSubsystem(() => { })
+                .UseWindowingSubsystem("Avalonia.Win32")
+                .UseRenderingSubsystem("Avalonia.Direct2D1")
                 .SetupWithoutStarting();
         }
 
@@ -74,22 +73,65 @@ namespace Avalonia.DesignerSupport
 
         static Window s_currentWindow;
 
-        private static void UpdateXaml(string xaml)
+        private static void UpdateXaml(string xaml) => UpdateXaml2(new DesignerApiXamlFileInfo
+        {
+            Xaml = xaml
+        }.Dictionary);
+
+        private static void UpdateXaml2(Dictionary<string, object> dic)
         {
+            var xamlInfo = new DesignerApiXamlFileInfo(dic);
             Window window;
-            Control original;
+            Control control;
 
             using (PlatformManager.DesignerMode())
             {
                 var loader = new AvaloniaXamlLoader();
-                var stream = new MemoryStream(Encoding.UTF8.GetBytes(xaml));
+                var stream = new MemoryStream(Encoding.UTF8.GetBytes(xamlInfo.Xaml));
 
-                original = (Control)loader.Load(stream);
-                window = original as Window;
 
+                
+                Uri baseUri = null;
+                if (xamlInfo.AssemblyPath != null)
+                {
+                    //Fabricate fake Uri
+                    baseUri =
+                        new Uri("resm:Fake.xaml?assembly=" + Path.GetFileNameWithoutExtension(xamlInfo.AssemblyPath));
+                }
+
+                var loaded = loader.Load(stream, null, baseUri);
+                var styles = loaded as Styles;
+                if (styles != null)
+                {
+                    var substitute = Design.GetPreviewWith(styles) ??
+                                     styles.Select(Design.GetPreviewWith).FirstOrDefault(s => s != null);
+                    if (substitute != null)
+                    {
+                        substitute.Styles.AddRange(styles);
+                        control = substitute;
+                    }
+                    else
+                        control = new StackPanel
+                        {
+                            Children =
+                            {
+                                new TextBlock {Text = "Styles can't be previewed without Design.PreviewWith. Add"},
+                                new TextBlock {Text = "<Design.PreviewWith>"},
+                                new TextBlock {Text = "    <Border Padding=20><!-- YOUR CONTROL FOR PREVIEW HERE--></Border>"},
+                                new TextBlock {Text = "<Design.PreviewWith>"},
+                                new TextBlock {Text = "before setters in your first Style"}
+                            }
+                        };
+                }
+                if (loaded is Application)
+                    control = new TextBlock {Text = "Application can't be previewed in design view"};
+                else
+                    control = (Control) loaded;
+
+                window = control as Window;
                 if (window == null)
                 {
-                    window = new Window() {Content = original};
+                    window = new Window() {Content = (Control)control};
                 }
 
                 if (!window.IsSet(Window.SizeToContentProperty))
@@ -99,7 +141,7 @@ namespace Avalonia.DesignerSupport
             s_currentWindow?.Close();
             s_currentWindow = window;
             window.Show();
-            Design.ApplyDesignerProperties(window, original);
+            Design.ApplyDesignerProperties(window, control);
             Api.OnWindowCreated?.Invoke(window.PlatformImpl.Handle.Handle);
             Api.OnResize?.Invoke();
         }

+ 2 - 0
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");

+ 2 - 2
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" />

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

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

+ 5 - 6
src/Avalonia.Input/InputExtensions.cs

@@ -24,14 +24,14 @@ namespace Avalonia.Input
         public static IEnumerable<IInputElement> GetInputElementsAt(this IInputElement element, Point p)
         {
             Contract.Requires<ArgumentNullException>(element != null);
-            var transformedBounds = BoundsTracker.GetTransformedBounds((Visual)element);
-            var geometry = transformedBounds.GetTransformedBoundsGeometry();
 
             if (element.IsVisible &&
                 element.IsHitTestVisible &&
                 element.IsEnabledCore)
             {
-                if (element.VisualChildren.Any())
+                bool containsPoint = BoundsTracker.GetTransformedBounds((Visual)element).Contains(p);
+
+                if ((containsPoint || !element.ClipToBounds) && element.VisualChildren.Any())
                 {
                     foreach (var child in ZSort(element.VisualChildren.OfType<IInputElement>()))
                     {
@@ -42,7 +42,7 @@ namespace Avalonia.Input
                     }
                 }
 
-                if (geometry.FillContains(p))
+                if (containsPoint)
                 {
                     yield return element;
                 }
@@ -71,7 +71,6 @@ namespace Avalonia.Input
                 })
                 .OrderBy(x => x, null)
                 .Select(x => x.Element);
-                
         }
 
         private class ZOrderElement : IComparable<ZOrderElement>
@@ -95,4 +94,4 @@ namespace Avalonia.Input
             }
         }
     }
-}
+}

+ 23 - 11
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;
                 }
 

+ 13 - 14
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);

+ 13 - 13
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();
             }

+ 12 - 2
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,
     }
 }

+ 26 - 9
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()

+ 2 - 1
src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs

@@ -55,7 +55,8 @@ namespace Avalonia.Media.Imaging
         /// </summary>
         public IBitmapImpl PlatformImpl
         {
-            get; }
+            get;
+        }
 
         /// <summary>
         /// Saves the bitmap to a file.

+ 13 - 3
src/Avalonia.SceneGraph/Rect.cs

@@ -224,14 +224,24 @@ 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>
         public bool Contains(Point p)
         {
-            return p.X >= _x && p.X < _x + _width &&
-                   p.Y >= _y && p.Y < _y + _height;
+            return p.X >= _x && p.X <= _x + _width &&
+                   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>

+ 2 - 2
src/Avalonia.SceneGraph/Rendering/RendererMixin.cs

@@ -104,7 +104,7 @@ namespace Avalonia.Rendering
 
                 if (visual.RenderTransform != null)
                 {
-                    var origin = visual.TransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
+                    var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
                     var offset = Matrix.CreateTranslation(origin);
                     renderTransform = (-offset) * visual.RenderTransform.Value * (offset);
                 }
@@ -172,7 +172,7 @@ namespace Avalonia.Rendering
             }
             else
             {
-                var origin = visual.TransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
+                var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
                 var offset = Matrix.CreateTranslation(visual.Bounds.Position + origin);
                 var m = (-offset) * visual.RenderTransform.Value * (offset);
                 return visual.Bounds.TransformToAABB(m);

+ 6 - 6
src/Avalonia.SceneGraph/Visual.cs

@@ -70,10 +70,10 @@ namespace Avalonia
             AvaloniaProperty.Register<Visual, Transform>(nameof(RenderTransform));
 
         /// <summary>
-        /// Defines the <see cref="TransformOrigin"/> property.
+        /// Defines the <see cref="RenderTransformOrigin"/> property.
         /// </summary>
-        public static readonly StyledProperty<RelativePoint> TransformOriginProperty =
-            AvaloniaProperty.Register<Visual, RelativePoint>(nameof(TransformOrigin), defaultValue: RelativePoint.Center);
+        public static readonly StyledProperty<RelativePoint> RenderTransformOriginProperty =
+            AvaloniaProperty.Register<Visual, RelativePoint>(nameof(RenderTransformOrigin), defaultValue: RelativePoint.Center);
 
         /// <summary>
         /// Defines the <see cref="IVisual.VisualParent"/> property.
@@ -196,10 +196,10 @@ namespace Avalonia
         /// <summary>
         /// Gets the transform origin of the scene graph node.
         /// </summary>
-        public RelativePoint TransformOrigin
+        public RelativePoint RenderTransformOrigin
         {
-            get { return GetValue(TransformOriginProperty); }
-            set { SetValue(TransformOriginProperty, value); }
+            get { return GetValue(RenderTransformOriginProperty); }
+            set { SetValue(RenderTransformOriginProperty, value); }
         }
 
         /// <summary>

+ 2 - 2
src/Avalonia.SceneGraph/VisualTree/IVisual.cs

@@ -77,9 +77,9 @@ namespace Avalonia.VisualTree
         Transform RenderTransform { get; set; }
 
         /// <summary>
-        /// Gets or sets the transform origin of the scene graph node.
+        /// Gets or sets the render transform origin of the scene graph node.
         /// </summary>
-        RelativePoint TransformOrigin { get; set; }
+        RelativePoint RenderTransformOrigin { get; set; }
 
         /// <summary>
         /// Gets the scene graph node's child nodes.

+ 10 - 12
src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs

@@ -38,20 +38,18 @@ namespace Avalonia.VisualTree
         /// </summary>
         public Matrix Transform { get; }
 
-        public Geometry GetTransformedBoundsGeometry()
+        public bool Contains(Point point)
         {
-            StreamGeometry geometry = new StreamGeometry();
-            using (var context = geometry.Open())
+            if (Transform.HasInverse)
             {
-                context.SetFillRule(FillRule.EvenOdd);
-                context.BeginFigure(Bounds.TopLeft * Transform, true);
-                context.LineTo(Bounds.TopRight * Transform);
-                context.LineTo(Bounds.BottomRight * Transform);
-                context.LineTo(Bounds.BottomLeft * Transform);
-                context.LineTo(Bounds.TopLeft * Transform);
-                context.EndFigure(true);
+                Point trPoint = point * Transform.Invert();
+
+                return Bounds.Contains(trPoint);
+            }
+            else
+            {
+                return Bounds.Contains(point);
             }
-            return geometry;
         }
     }
-}
+}

+ 10 - 1
src/Avalonia.Styling/Avalonia.Styling.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>
@@ -43,12 +43,17 @@
     <Compile Include="..\Shared\SharedAssemblyInfo.cs">
       <Link>Properties\SharedAssemblyInfo.cs</Link>
     </Compile>
+    <Compile Include="Controls\NameScopeEventArgs.cs" />
+    <Compile Include="Controls\NameScopeExtensions.cs" />
     <Compile Include="LogicalTree\ILogical.cs" />
     <Compile Include="LogicalTree\LogicalExtensions.cs" />
     <Compile Include="LogicalTree\LogicalTreeAttachmentEventArgs.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Styling\ActivatedSubject.cs" />
     <Compile Include="Styling\ActivatedValue.cs" />
+    <Compile Include="Controls\INameScope.cs" />
+    <Compile Include="Styling\ITemplate.cs" />
+    <Compile Include="Controls\NameScope.cs" />
     <Compile Include="Styling\TemplateSelector.cs" />
     <Compile Include="Styling\DescendentSelector.cs" />
     <Compile Include="Styling\ChildSelector.cs" />
@@ -92,6 +97,10 @@
     <None Include="Styling\packages.config" />
   </ItemGroup>
   <ItemGroup>
+    <ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj">
+      <Project>{d211e587-d8bc-45b9-95a4-f297c8fa5200}</Project>
+      <Name>Avalonia.Animation</Name>
+    </ProjectReference>
     <ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj">
       <Project>{B09B78D8-9B26-48B0-9149-D64A2F120F3F}</Project>
       <Name>Avalonia.Base</Name>

+ 0 - 0
src/Avalonia.Controls/INameScope.cs → src/Avalonia.Styling/Controls/INameScope.cs


Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio