Browse Source

Merge branch 'master' into headless-platform

Dan Walmsley 6 years ago
parent
commit
2101a7f75a
100 changed files with 1516 additions and 895 deletions
  1. 2 1
      .editorconfig
  2. 8 0
      .gitignore
  3. 2 1
      azure-pipelines.yml
  4. 1 1
      build-native.sh
  5. 1 1
      build/SharedVersion.props
  6. 1 1
      build/SkiaSharp.props
  7. 16 4
      native/Avalonia.Native/src/OSX/AvnString.mm
  8. 11 10
      native/Avalonia.Native/src/OSX/KeyTransform.mm
  9. 10 1
      native/Avalonia.Native/src/OSX/clipboard.mm
  10. 25 12
      nukebuild/Build.cs
  11. 0 3
      readme.md
  12. 1 1
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  13. 24 6
      samples/ControlCatalog.NetCore/Program.cs
  14. 1 1
      samples/ControlCatalog/DecoratedWindow.xaml.cs
  15. 21 5
      samples/ControlCatalog/MainView.xaml
  16. 1 0
      samples/ControlCatalog/MainWindow.xaml
  17. 0 4
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
  18. 1 1
      samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
  19. 26 0
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  20. 81 0
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
  21. 14 1
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  22. 46 6
      samples/ControlCatalog/Pages/ListBoxPage.xaml.cs
  23. 9 0
      samples/ControlCatalog/Pages/PointersPage.cs
  24. 4 1
      samples/ControlCatalog/Pages/ScreenPage.cs
  25. 33 0
      samples/ControlCatalog/Pages/TabStripPage.xaml
  26. 45 0
      samples/ControlCatalog/Pages/TabStripPage.xaml.cs
  27. 23 10
      samples/ControlCatalog/Pages/TreeViewPage.xaml
  28. 84 9
      samples/ControlCatalog/Pages/TreeViewPage.xaml.cs
  29. 18 11
      samples/ControlCatalog/SideBar.xaml
  30. 27 0
      samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
  31. 1 2
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  32. 2 3
      samples/RenderDemo/SideBar.xaml
  33. 1 0
      samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
  34. 0 5
      src/Android/Avalonia.Android/AndroidPlatform.cs
  35. 1 1
      src/Android/Avalonia.Android/Avalonia.Android.csproj
  36. 0 112
      src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs
  37. 2 0
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  38. 2 1
      src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
  39. 39 64
      src/Avalonia.Base/AvaloniaObject.cs
  40. 2 0
      src/Avalonia.Base/Data/BindingOperations.cs
  41. 1 1
      src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs
  42. 46 0
      src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs
  43. 31 0
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  44. 7 2
      src/Avalonia.Base/IPriorityValueOwner.cs
  45. 18 16
      src/Avalonia.Base/PriorityValue.cs
  46. 150 0
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  47. 80 126
      src/Avalonia.Base/Utilities/DeferredSetter.cs
  48. 106 0
      src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs
  49. 28 124
      src/Avalonia.Base/ValueStore.cs
  50. 5 5
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  51. 1 0
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  52. 1 2
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  53. 1 1
      src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs
  54. 1 1
      src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs
  55. 1 27
      src/Avalonia.Controls/AutoCompleteBox.cs
  56. 14 3
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  57. 2 3
      src/Avalonia.Controls/ComboBox.cs
  58. 35 7
      src/Avalonia.Controls/ContextMenu.cs
  59. 1 20
      src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs
  60. 0 19
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs
  61. 1 0
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  62. 2 10
      src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
  63. 3 14
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  64. 4 10
      src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
  65. 10 5
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  66. 1 0
      src/Avalonia.Controls/GridSplitter.cs
  67. 9 0
      src/Avalonia.Controls/IScrollAnchorProvider.cs
  68. 1 1
      src/Avalonia.Controls/Image.cs
  69. 0 15
      src/Avalonia.Controls/ItemsControl.cs
  70. 1 1
      src/Avalonia.Controls/LayoutTransformControl.cs
  71. 19 2
      src/Avalonia.Controls/ListBox.cs
  72. 26 21
      src/Avalonia.Controls/Menu.cs
  73. 2 3
      src/Avalonia.Controls/MenuBase.cs
  74. 1 1
      src/Avalonia.Controls/MenuItem.cs
  75. 1 0
      src/Avalonia.Controls/Mixins/ContentControlMixin.cs
  76. 1 1
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  77. 1 1
      src/Avalonia.Controls/Panel.cs
  78. 17 2
      src/Avalonia.Controls/PlacementMode.cs
  79. 1 1
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  80. 3 1
      src/Avalonia.Controls/Platform/IPopupImpl.cs
  81. 2 0
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  82. 1 23
      src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
  83. 27 0
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  84. 0 1
      src/Avalonia.Controls/Platform/IWindowingPlatform.cs
  85. 2 1
      src/Avalonia.Controls/Platform/InProcessDragSource.cs
  86. 30 55
      src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs
  87. 0 5
      src/Avalonia.Controls/Platform/PlatformManager.cs
  88. 1 1
      src/Avalonia.Controls/Presenters/CarouselPresenter.cs
  89. 6 7
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  90. 1 1
      src/Avalonia.Controls/Presenters/ItemContainerSync.cs
  91. 1 0
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  92. 1 1
      src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs
  93. 3 6
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  94. 0 15
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  95. 8 11
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  96. 0 42
      src/Avalonia.Controls/Primitives/AdornerDecorator.cs
  97. 1 1
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  98. 26 0
      src/Avalonia.Controls/Primitives/IPopupHost.cs
  99. 38 0
      src/Avalonia.Controls/Primitives/OverlayLayer.cs
  100. 149 0
      src/Avalonia.Controls/Primitives/OverlayPopupHost.cs

+ 2 - 1
.editorconfig

@@ -131,13 +131,14 @@ csharp_space_between_method_declaration_name_and_open_parenthesis = false
 csharp_space_between_method_declaration_parameter_list_parentheses = false
 csharp_space_between_parentheses = false
 csharp_space_between_square_brackets = false
+space_within_single_line_array_initializer_braces = true
 
 # Wrapping preferences
 csharp_wrap_before_ternary_opsigns = false
 
 # Xaml files
 [*.xaml]
-indent_size = 4
+indent_size = 2
 
 # Xml project files
 [*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]

+ 8 - 0
.gitignore

@@ -198,3 +198,11 @@ info.plist
 build-intermediate
 obj-Direct2D1/
 obj-Skia/
+
+##################
+# Vim
+##################
+.vim
+coc-settings.json
+.ccls-cache
+.ccls

+ 2 - 1
azure-pipelines.yml

@@ -102,7 +102,7 @@ jobs:
 
 - job: Windows
   pool:
-    vmImage: 'vs2017-win2016'
+    vmImage: 'windows-2019'
   steps:
   - task: CmdLine@2
     displayName: 'Install Nuke'
@@ -134,3 +134,4 @@ jobs:
       pathToPublish: '$(Build.SourcesDirectory)/artifacts/zip'
       artifactName: 'Samples'
     condition: succeeded()
+

+ 1 - 1
build-native.sh

@@ -2,4 +2,4 @@
   <ItemGroup>
     <PackageReference Include="System.ValueTuple" Version="4.5.0" />
   </ItemGroup>
-</Project>
+</Project>

+ 1 - 1
build/SharedVersion.props

@@ -2,7 +2,7 @@
   xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <PropertyGroup>
     <Product>Avalonia</Product>
-    <Version>0.8.1</Version>
+    <Version>0.8.999</Version>
     <Copyright>Copyright 2019 &#169; The AvaloniaUI Project</Copyright>
     <PackageLicenseUrl>https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md</PackageLicenseUrl>
     <PackageProjectUrl>https://github.com/AvaloniaUI/Avalonia/</PackageProjectUrl>

+ 1 - 1
build/SkiaSharp.props

@@ -1,6 +1,6 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
     <PackageReference Include="SkiaSharp" Version="1.68.0" />
-    <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.68.0.1" />
+    <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.68.0.2" />
   </ItemGroup>
 </Project>

+ 16 - 4
native/Avalonia.Native/src/OSX/AvnString.mm

@@ -11,14 +11,26 @@
 class AvnStringImpl : public virtual ComSingleObject<IAvnString, &IID_IAvnString>
 {
 private:
-    NSString* _string;
+    int _length;
+    const char* _cstring;
     
 public:
     FORWARD_IUNKNOWN()
     
     AvnStringImpl(NSString* string)
+    { 
+        auto cstring = [string cStringUsingEncoding:NSUTF8StringEncoding];
+        _length = (int)[string lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
+        
+        _cstring = (const char*)malloc(_length + 5);
+        
+        memset((void*)_cstring, 0, _length + 5);
+        memcpy((void*)_cstring, (void*)cstring, _length);
+    }
+    
+    virtual ~AvnStringImpl()
     {
-        _string = string;
+        free((void*)_cstring);
     }
     
     virtual HRESULT Pointer(void**retOut) override
@@ -30,7 +42,7 @@ public:
                 return E_POINTER;
             }
             
-            *retOut = (void*)_string.UTF8String;
+            *retOut = (void*)_cstring;
             
             return S_OK;
         }
@@ -43,7 +55,7 @@ public:
             return E_POINTER;
         }
         
-        *retOut = (int)_string.length;
+        *retOut = _length;
         
         return S_OK;
     }

+ 11 - 10
native/Avalonia.Native/src/OSX/KeyTransform.mm

@@ -26,7 +26,7 @@ const int kVK_ANSI_3 = 0x14;
 const int kVK_ANSI_4 = 0x15;
 const int kVK_ANSI_6 = 0x16;
 const int kVK_ANSI_5 = 0x17;
-//const int kVK_ANSI_Equal = 0x18;
+const int kVK_ANSI_Equal = 0x18;
 const int kVK_ANSI_9 = 0x19;
 const int kVK_ANSI_7 = 0x1A;
 const int kVK_ANSI_Minus = 0x1B;
@@ -45,11 +45,11 @@ const int kVK_ANSI_K = 0x28;
 const int kVK_ANSI_Semicolon = 0x29;
 const int kVK_ANSI_Backslash = 0x2A;
 const int kVK_ANSI_Comma = 0x2B;
-//const int kVK_ANSI_Slash = 0x2C;
+const int kVK_ANSI_Slash = 0x2C;
 const int kVK_ANSI_N = 0x2D;
 const int kVK_ANSI_M = 0x2E;
 const int kVK_ANSI_Period = 0x2F;
-//const int kVK_ANSI_Grave = 0x32;
+const int kVK_ANSI_Grave = 0x32;
 const int kVK_ANSI_KeypadDecimal = 0x41;
 const int kVK_ANSI_KeypadMultiply = 0x43;
 const int kVK_ANSI_KeypadPlus = 0x45;
@@ -57,7 +57,7 @@ const int kVK_ANSI_KeypadClear = 0x47;
 const int kVK_ANSI_KeypadDivide = 0x4B;
 const int kVK_ANSI_KeypadEnter = 0x4C;
 const int kVK_ANSI_KeypadMinus = 0x4E;
-//const int kVK_ANSI_KeypadEquals = 0x51;
+const int kVK_ANSI_KeypadEquals = 0x51;
 const int kVK_ANSI_Keypad0 = 0x52;
 const int kVK_ANSI_Keypad1 = 0x53;
 const int kVK_ANSI_Keypad2 = 0x54;
@@ -121,7 +121,7 @@ const int kVK_UpArrow = 0x7E;
 //const int kVK_JIS_Underscore = 0x5E;
 //const int kVK_JIS_KeypadComma = 0x5F;
 //const int kVK_JIS_Eisu = 0x66;
-//const int kVK_JIS_Kana = 0x68;
+const int kVK_JIS_Kana = 0x68;
 
  std::map<int, AvnKey> s_KeyMap =
  {
@@ -148,7 +148,7 @@ const int kVK_UpArrow = 0x7E;
     {kVK_ANSI_4, D4},
     {kVK_ANSI_6, D6},
     {kVK_ANSI_5, D5},
-    //{kVK_ANSI_Equal, ?},
+    {kVK_ANSI_Equal, OemPlus},
     {kVK_ANSI_9, D9},
     {kVK_ANSI_7, D7},
     {kVK_ANSI_Minus, OemMinus},
@@ -167,11 +167,11 @@ const int kVK_UpArrow = 0x7E;
     {kVK_ANSI_Semicolon, OemSemicolon},
     {kVK_ANSI_Backslash, OemBackslash},
     {kVK_ANSI_Comma, OemComma},
-    //{kVK_ANSI_Slash, ?},
+    {kVK_ANSI_Slash, Oem2},
     {kVK_ANSI_N, N},
     {kVK_ANSI_M, M},
     {kVK_ANSI_Period, OemPeriod},
-    //{kVK_ANSI_Grave, ?},
+    {kVK_ANSI_Grave, OemTilde},
     {kVK_ANSI_KeypadDecimal, Decimal},
     {kVK_ANSI_KeypadMultiply, Multiply},
     {kVK_ANSI_KeypadPlus, OemPlus},
@@ -179,7 +179,7 @@ const int kVK_UpArrow = 0x7E;
     {kVK_ANSI_KeypadDivide, Divide},
     {kVK_ANSI_KeypadEnter, AvnKeyEnter},
     {kVK_ANSI_KeypadMinus, OemMinus},
-    //{kVK_ANSI_KeypadEquals, ?},
+    {kVK_ANSI_KeypadEquals, OemPlus},
     {kVK_ANSI_Keypad0, NumPad0},
     {kVK_ANSI_Keypad1, NumPad1},
     {kVK_ANSI_Keypad2, NumPad2},
@@ -237,5 +237,6 @@ const int kVK_UpArrow = 0x7E;
     {kVK_LeftArrow, Left},
     {kVK_RightArrow, Right},
     {kVK_DownArrow, Down},
-    {kVK_UpArrow, Up}
+    {kVK_UpArrow, Up},
+    {kVK_JIS_Kana, AvnKeyKanaMode},
 };

+ 10 - 1
native/Avalonia.Native/src/OSX/clipboard.mm

@@ -8,6 +8,13 @@ class Clipboard : public ComSingleObject<IAvnClipboard, &IID_IAvnClipboard>
 {
 public:
     FORWARD_IUNKNOWN()
+    
+    Clipboard()
+    {
+        NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
+        [pasteBoard stringForType:NSPasteboardTypeString];
+    }
+    
     virtual HRESULT GetText (IAvnString**ppv) override
     {
         @autoreleasepool
@@ -39,7 +46,9 @@ public:
     {
         @autoreleasepool
         {
-            [[NSPasteboard generalPasteboard] clearContents];
+            NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
+            [pasteBoard clearContents];
+            [pasteBoard setString:@"" forType:NSPasteboardTypeString];
         }
         
         return S_OK;

+ 25 - 12
nukebuild/Build.cs

@@ -89,6 +89,29 @@ partial class Build : NukeBuild
 
     }
 
+    IReadOnlyCollection<Output> MsBuildCommon(
+        string projectFile,
+        Configure<MSBuildSettings> configurator = null)
+    {
+        return MSBuild(projectFile, c =>
+        {
+            // This is required for VS2019 image on Azure Pipelines
+            if (Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure)
+            {
+                var javaSdk = Environment.GetEnvironmentVariable("JAVA_HOME_8_X64");
+                if (javaSdk != null)
+                    c = c.AddProperty("JavaSdkDirectory", javaSdk);
+            }
+
+            c = c.AddProperty("PackageVersion", Parameters.Version)
+                .AddProperty("iOSRoslynPathHackRequired", "true")
+                .SetToolPath(MsBuildExe.Value)
+                .SetConfiguration(Parameters.Configuration)
+                .SetVerbosity(MSBuildVerbosity.Minimal);
+            c = configurator?.Invoke(c) ?? c;
+            return c;
+        });
+    }
     Target Clean => _ => _.Executes(() =>
     {
         DeleteDirectories(Parameters.BuildDirs);
@@ -105,13 +128,8 @@ partial class Build : NukeBuild
         .Executes(() =>
         {
             if (Parameters.IsRunningOnWindows)
-                MSBuild(Parameters.MSBuildSolution, c => c
+                MsBuildCommon(Parameters.MSBuildSolution, c => c
                     .SetArgumentConfigurator(a => a.Add("/r"))
-                    .SetConfiguration(Parameters.Configuration)
-                    .SetVerbosity(MSBuildVerbosity.Minimal)
-                    .AddProperty("PackageVersion", Parameters.Version)
-                    .AddProperty("iOSRoslynPathHackRequired", "true")
-                    .SetToolPath(MsBuildExe.Value)
                     .AddTargets("Build")
                 );
 
@@ -237,12 +255,7 @@ partial class Build : NukeBuild
         {
             if (Parameters.IsRunningOnWindows)
 
-                MSBuild(Parameters.MSBuildSolution, c => c
-                    .SetConfiguration(Parameters.Configuration)
-                    .SetVerbosity(MSBuildVerbosity.Minimal)
-                    .AddProperty("PackageVersion", Parameters.Version)
-                    .AddProperty("iOSRoslynPathHackRequired", "true")
-                    .SetToolPath(MsBuildExe.Value)
+                MsBuildCommon(Parameters.MSBuildSolution, c => c
                     .AddTargets("Pack"));
             else
                 DotNetPack(Parameters.MSBuildSolution, c =>

+ 0 - 3
readme.md

@@ -32,9 +32,6 @@ Install-Package Avalonia.Desktop
 
 ## Bleeding Edge Builds
 
-Try out the latest build of Avalonia available for download here:
-https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master/artifacts
-
 or use nightly build feeds as described here:
 https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed
 

+ 1 - 1
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@@ -16,7 +16,7 @@
     <AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
     <GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
     <AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk>
-    <TargetFrameworkVersion>v4.4</TargetFrameworkVersion>
+    <TargetFrameworkVersion>v8.0</TargetFrameworkVersion>
     <AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

+ 24 - 6
samples/ControlCatalog.NetCore/Program.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Diagnostics;
+using System.Globalization;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
@@ -32,10 +33,19 @@ namespace ControlCatalog.NetCore
             }
 
             var builder = BuildAvaloniaApp();
+
+            double GetScaling()
+            {
+                var idx = Array.IndexOf(args, "--scaling");
+                if (idx != 0 && args.Length > idx + 1 &&
+                    double.TryParse(args[idx + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out var scaling))
+                    return scaling;
+                return 1;
+            }
             if (args.Contains("--fbdev"))
             {
-                System.Threading.ThreadPool.QueueUserWorkItem(_ => ConsoleSilencer());
-                return builder.StartLinuxFramebuffer(args);
+                SilenceConsole();
+                return builder.StartLinuxFbDev(args, scaling: GetScaling());
             }
             else if (args.Contains("--vnc"))
             {
@@ -83,6 +93,11 @@ namespace ControlCatalog.NetCore
                     })
                     .StartWithClassicDesktopLifetime(args);
             }
+            else if (args.Contains("--drm"))
+            {
+                SilenceConsole();
+                return builder.StartLinuxDrm(args, scaling: GetScaling());
+            }
             else
                 return builder.StartWithClassicDesktopLifetime(args);
         }
@@ -102,11 +117,14 @@ namespace ControlCatalog.NetCore
                 .UseSkia()
                 .UseReactiveUI();
 
-        static void ConsoleSilencer()
+        static void SilenceConsole()
         {
-            Console.CursorVisible = false;
-            while (true)
-                Console.ReadKey(true);
+            new Thread(() =>
+            {
+                Console.CursorVisible = false;
+                while (true)
+                    Console.ReadKey(true);
+            }) {IsBackground = true}.Start();
         }
     }
 }

+ 1 - 1
samples/ControlCatalog/DecoratedWindow.xaml.cs

@@ -34,7 +34,7 @@ namespace ControlCatalog
             SetupSide("Left", StandardCursorType.LeftSide, WindowEdge.West);
             SetupSide("Right", StandardCursorType.RightSide, WindowEdge.East);
             SetupSide("Top", StandardCursorType.TopSide, WindowEdge.North);
-            SetupSide("Bottom", StandardCursorType.BottomSize, WindowEdge.South);
+            SetupSide("Bottom", StandardCursorType.BottomSide, WindowEdge.South);
             SetupSide("TopLeft", StandardCursorType.TopLeftCorner, WindowEdge.NorthWest);
             SetupSide("TopRight", StandardCursorType.TopRightCorner, WindowEdge.NorthEast);
             SetupSide("BottomLeft", StandardCursorType.BottomLeftCorner, WindowEdge.SouthWest);

+ 21 - 5
samples/ControlCatalog/MainView.xaml

@@ -6,10 +6,13 @@
         Foreground="{DynamicResource ThemeForegroundBrush}"
         FontSize="{DynamicResource FontSizeNormal}">
   <Grid>
-    <ComboBox x:Name="Themes" SelectedIndex="0" Width="100" Margin="8" HorizontalAlignment="Right" VerticalAlignment="Bottom">
-      <ComboBoxItem>Light</ComboBoxItem>
-      <ComboBoxItem>Dark</ComboBoxItem>
-    </ComboBox>
+    <Grid.Styles>
+        <Style Selector="TextBlock.h2">
+            <Setter Property="TextWrapping" Value="Wrap"/>
+            <Setter Property="MaxWidth" Value="400"/>
+            <Setter Property="HorizontalAlignment" Value="Left"/>
+        </Style>
+    </Grid.Styles>  
     <TabControl Classes="sidebar" Name="Sidebar">
       <TabItem Header="AutoCompleteBox"><pages:AutoCompleteBoxPage/></TabItem>
       <TabItem Header="Border"><pages:BorderPage/></TabItem>
@@ -21,11 +24,17 @@
       <TabItem Header="CheckBox"><pages:CheckBoxPage/></TabItem>
       <TabItem Header="ComboBox"><pages:ComboBoxPage/></TabItem>
       <TabItem Header="ContextMenu"><pages:ContextMenuPage/></TabItem>
-      <TabItem Header="DataGrid"><pages:DataGridPage/></TabItem>
+      <!-- DataGrid is our special snowflake -->  
+      <TabItem Header="DataGrid" 
+               ScrollViewer.VerticalScrollBarVisibility="Disabled"
+               ScrollViewer.HorizontalScrollBarVisibility="Disabled">
+          <pages:DataGridPage/>
+      </TabItem>
       <TabItem Header="DatePicker"><pages:DatePickerPage/></TabItem>
       <TabItem Header="Drag+Drop"><pages:DragAndDropPage/></TabItem>
       <TabItem Header="Expander"><pages:ExpanderPage/></TabItem>
       <TabItem Header="Image"><pages:ImagePage/></TabItem>
+      <TabItem Header="ItemsRepeater"><pages:ItemsRepeaterPage/></TabItem>
       <TabItem Header="LayoutTransformControl"><pages:LayoutTransformControlPage/></TabItem>
       <TabItem Header="ListBox"><pages:ListBoxPage/></TabItem>
       <TabItem Header="Menu"><pages:MenuPage/></TabItem>
@@ -36,10 +45,17 @@
       <TabItem Header="RadioButton"><pages:RadioButtonPage/></TabItem>
       <TabItem Header="Slider"><pages:SliderPage/></TabItem>
       <TabItem Header="TabControl"><pages:TabControlPage/></TabItem>
+      <TabItem Header="TabStrip"><pages:TabStripPage/></TabItem>
       <TabItem Header="TextBox"><pages:TextBoxPage/></TabItem>
       <TabItem Header="ToolTip"><pages:ToolTipPage/></TabItem>
       <TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>
       <TabItem Header="Viewbox"><pages:ViewboxPage/></TabItem>
+    <TabControl.Tag>
+        <ComboBox x:Name="Themes" SelectedIndex="0" Width="100" Margin="8" HorizontalAlignment="Right" VerticalAlignment="Bottom">
+            <ComboBoxItem>Light</ComboBoxItem>
+            <ComboBoxItem>Dark</ComboBoxItem>
+        </ComboBox>
+    </TabControl.Tag>
     </TabControl>
   </Grid>
 </UserControl>

+ 1 - 0
samples/ControlCatalog/MainWindow.xaml

@@ -1,4 +1,5 @@
 <Window xmlns="https://github.com/avaloniaui" MinWidth="500" MinHeight="300"
+        Width="1024" Height="800"
         xmlns:pages="clr-namespace:ControlCatalog.Pages"
         Title="Avalonia Control Gallery"
         Icon="/Assets/test_icon.ico"

+ 0 - 4
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml

@@ -37,10 +37,6 @@
 
       <StackPanel Orientation="Vertical">
         
-        <TextBlock Text="ValueMemeberSelector"/>
-        <AutoCompleteBox Width="200"
-                         Margin="0,0,0,8"
-                         ValueMemberSelector="Capital"/>
         <TextBlock Text="ValueMemberBinding"/>
         <AutoCompleteBox Width="200"
                          Margin="0,0,0,8"

+ 1 - 1
samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs

@@ -29,7 +29,7 @@ namespace ControlCatalog.Pages
             DataObject dragData = new DataObject();
             dragData.Set(DataFormats.Text, $"You have dragged text {++DragCount} times");
 
-            var result = await DragDrop.DoDragDrop(dragData, DragDropEffects.Copy);
+            var result = await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Copy);
             switch(result)
             {
                 case DragDropEffects.Copy:

+ 26 - 0
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@@ -0,0 +1,26 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             x:Class="ControlCatalog.Pages.ItemsRepeaterPage">
+  <DockPanel>
+    <StackPanel DockPanel.Dock="Top" Spacing="4" Margin="0 0 0 16">
+      <TextBlock Classes="h1">ItemsRepeater</TextBlock>
+      <TextBlock Classes="h2">A data-driven collection control that incorporates a flexible layout system, custom views, and virtualization.</TextBlock>
+    </StackPanel>
+    <StackPanel DockPanel.Dock="Right" Margin="8 0" Spacing="4">
+      <ComboBox SelectedIndex="0" SelectionChanged="LayoutChanged">
+        <ComboBoxItem>Stack - Vertical</ComboBoxItem>
+        <ComboBoxItem>Stack - Horizontal</ComboBoxItem>
+        <ComboBoxItem>UniformGrid - Vertical</ComboBoxItem>
+        <ComboBoxItem>UniformGrid - Horizontal</ComboBoxItem>
+      </ComboBox>
+      <Button Command="{Binding AddItem}">Add Item</Button>
+    </StackPanel>
+    <Border BorderThickness="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}" Margin="0 0 0 16">
+      <ScrollViewer Name="scroller"
+                    HorizontalScrollBarVisibility="Auto"
+                    VerticalScrollBarVisibility="Auto">
+        <ItemsRepeater Name="repeater" Background="Transparent" Items="{Binding Items}"/>
+      </ScrollViewer>
+    </Border>
+  </DockPanel>
+</UserControl>

+ 81 - 0
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs

@@ -0,0 +1,81 @@
+using System;
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Layout;
+using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
+
+namespace ControlCatalog.Pages
+{
+    public class ItemsRepeaterPage : UserControl
+    {
+        private ItemsRepeater _repeater;
+        private ScrollViewer _scroller;
+
+        public ItemsRepeaterPage()
+        {
+            this.InitializeComponent();
+            _repeater = this.FindControl<ItemsRepeater>("repeater");
+            _scroller = this.FindControl<ScrollViewer>("scroller");
+            _repeater.PointerPressed += RepeaterClick;
+            DataContext = new ItemsRepeaterPageViewModel();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        private void LayoutChanged(object sender, SelectionChangedEventArgs e)
+        {
+            if (_repeater == null)
+            {
+                return;
+            }
+
+            var comboBox = (ComboBox)sender;
+
+            switch (comboBox.SelectedIndex)
+            {
+                case 0:
+                    _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
+                    _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
+                    _repeater.Layout = new StackLayout { Orientation = Orientation.Vertical };
+                    break;
+                case 1:
+                    _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
+                    _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
+                    _repeater.Layout = new StackLayout { Orientation = Orientation.Horizontal };
+                    break;
+                case 2:
+                    _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
+                    _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
+                    _repeater.Layout = new UniformGridLayout
+                    {
+                        Orientation = Orientation.Vertical,
+                        MinItemWidth = 200,
+                        MinItemHeight = 200,
+                    };
+                    break;
+                case 3:
+                    _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
+                    _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
+                    _repeater.Layout = new UniformGridLayout
+                    {
+                        Orientation = Orientation.Horizontal,
+                        MinItemWidth = 200,
+                        MinItemHeight = 200,
+                    };
+                    break;
+            }
+        }
+
+        private void RepeaterClick(object sender, PointerPressedEventArgs e)
+        {
+            var item = (e.Source as TextBlock)?.DataContext as string;
+            ((ItemsRepeaterPageViewModel)DataContext).SelectedItem = item;
+        }
+    }
+}

+ 14 - 1
samples/ControlCatalog/Pages/ListBoxPage.xaml

@@ -9,7 +9,20 @@
               Margin="0,16,0,0"
               HorizontalAlignment="Center"
               Spacing="16">
-      <ListBox Items="{Binding}" Width="250" Height="350"></ListBox>
+      <StackPanel Orientation="Vertical" Spacing="8">
+        <ListBox Items="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350"></ListBox>
+
+        <Button Command="{Binding AddItemCommand}">Add</Button>
+
+        <Button Command="{Binding RemoveItemCommand}">Remove</Button>
+
+        <ComboBox SelectedIndex="{Binding SelectionMode, Mode=TwoWay}">
+          <ComboBoxItem>Single</ComboBoxItem>
+          <ComboBoxItem>Multiple</ComboBoxItem>
+          <ComboBoxItem>Toggle</ComboBoxItem>
+          <ComboBoxItem>AlwaysSelected</ComboBoxItem>
+        </ComboBox>
+      </StackPanel>
     </StackPanel>
   </StackPanel>
 </UserControl>

+ 46 - 6
samples/ControlCatalog/Pages/ListBoxPage.xaml.cs

@@ -1,9 +1,9 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.Linq;
+using System.Reactive;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
+using ReactiveUI;
 
 namespace ControlCatalog.Pages
 {
@@ -11,9 +11,8 @@ namespace ControlCatalog.Pages
     {
         public ListBoxPage()
         {
-            this.InitializeComponent();
-            DataContext = Enumerable.Range(1, 10).Select(i => $"Item {i}" )
-                .ToArray();
+            InitializeComponent();
+            DataContext = new PageViewModel();
         }
 
         private void InitializeComponent()
@@ -21,5 +20,46 @@ namespace ControlCatalog.Pages
             AvaloniaXamlLoader.Load(this);
         }
 
+        private class PageViewModel : ReactiveObject
+        {
+            private int _counter;
+            private SelectionMode _selectionMode;
+
+            public PageViewModel()
+            {
+                Items = new ObservableCollection<string>(Enumerable.Range(1, 10).Select(i => GenerateItem()));
+                SelectedItems = new ObservableCollection<string>();
+
+                AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
+
+                RemoveItemCommand = ReactiveCommand.Create(() =>
+                {
+                    while (SelectedItems.Count > 0)
+                    {
+                        Items.Remove(SelectedItems[0]);
+                    }
+                });
+            }
+
+            public ObservableCollection<string> Items { get; }
+
+            public ObservableCollection<string> SelectedItems { get; }
+
+            public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
+
+            public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
+
+            public SelectionMode SelectionMode
+            {
+                get => _selectionMode;
+                set
+                {
+                    SelectedItems.Clear();
+                    this.RaiseAndSetIfChanged(ref _selectionMode, value);
+                }
+            }
+
+            private string GenerateItem() => $"Item {_counter++}";
+        }
     }
 }

+ 9 - 0
samples/ControlCatalog/Pages/PointersPage.cs

@@ -69,16 +69,25 @@ namespace ControlCatalog.Pages
         {
             UpdatePointer(e);
             e.Pointer.Capture(this);
+            e.Handled = true;
             base.OnPointerPressed(e);
         }
 
         protected override void OnPointerMoved(PointerEventArgs e)
         {
             UpdatePointer(e);
+            e.Handled = true;
             base.OnPointerMoved(e);
         }
 
         protected override void OnPointerReleased(PointerReleasedEventArgs e)
+        {
+            _pointers.Remove(e.Pointer);
+            e.Handled = true;
+            InvalidateVisual();
+        }
+
+        protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
         {
             _pointers.Remove(e.Pointer);
             InvalidateVisual();

+ 4 - 1
samples/ControlCatalog/Pages/ScreenPage.cs

@@ -22,7 +22,10 @@ namespace ControlCatalog.Pages
         public override void Render(DrawingContext context)
         {
             base.Render(context);
-            Window w = (Window)VisualRoot;
+            if (!(VisualRoot is Window w))
+            {
+                return;                
+            }
             var screens = w.Screens.All;
             var scaling = ((IRenderRoot)w).RenderScaling;
 

+ 33 - 0
samples/ControlCatalog/Pages/TabStripPage.xaml

@@ -0,0 +1,33 @@
+<UserControl xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             x:Class="ControlCatalog.Pages.TabStripPage"
+             xmlns="https://github.com/avaloniaui">
+    <StackPanel Orientation="Vertical" Spacing="4">
+        <TextBlock Classes="h1">TabStrip</TextBlock>
+        <TextBlock Classes="h2">A control which displays a selectable strip of tabs</TextBlock>
+
+        <Separator Margin="0 16"/>
+        
+        <TextBlock Classes="h1">Defined in XAML</TextBlock>
+        <TabStrip>
+            <TabStripItem>Item 1</TabStripItem>
+            <TabStripItem>Item 2</TabStripItem>
+            <TabStripItem IsEnabled="False">Disabled</TabStripItem>
+        </TabStrip>
+
+        <Separator Margin="0 16"/>
+
+        <TextBlock Classes="h1">Dynamically generated</TextBlock>
+        <TabStrip Items="{Binding}">
+            <TabStrip.Styles>
+                <Style Selector="TabStripItem">
+                    <Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
+                </Style>
+            </TabStrip.Styles>
+            <TabStrip.ItemTemplate>
+                <DataTemplate>
+                    <TextBlock Text="{Binding Header}"/>
+                </DataTemplate>
+            </TabStrip.ItemTemplate>
+        </TabStrip>
+    </StackPanel>
+</UserControl>

+ 45 - 0
samples/ControlCatalog/Pages/TabStripPage.xaml.cs

@@ -0,0 +1,45 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+
+namespace ControlCatalog.Pages
+{
+    public class TabStripPage : UserControl
+    {
+        public TabStripPage()
+        {
+            InitializeComponent();
+
+            DataContext = new[]
+            {
+                new TabStripItemViewModel
+                {
+                    Header = "Item 1",
+                },
+                new TabStripItemViewModel
+                {
+                    Header = "Item 2",
+                },
+                new TabStripItemViewModel
+                {
+                    Header = "Disabled",
+                    IsEnabled = false,
+                },
+            };
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        private class TabStripItemViewModel
+        {
+            public string Header { get; set; }
+            public bool IsEnabled { get; set; } = true;
+        }
+    }
+}

+ 23 - 10
samples/ControlCatalog/Pages/TreeViewPage.xaml

@@ -6,16 +6,29 @@
     <TextBlock Classes="h2">Displays a hierachical tree of data.</TextBlock>
 
     <StackPanel Orientation="Horizontal"
-              Margin="0,16,0,0"
-              HorizontalAlignment="Center"
-              Spacing="16">
-      <TreeView SelectionMode="Multiple" Items="{Binding}" Width="250" Height="350">
-        <TreeView.ItemTemplate>
-          <TreeDataTemplate ItemsSource="{Binding Children}">
-            <TextBlock Text="{Binding Header}"/>
-          </TreeDataTemplate>
-        </TreeView.ItemTemplate>
-      </TreeView>
+                Margin="0,16,0,0"
+                HorizontalAlignment="Center"
+                Spacing="16">
+      <StackPanel Orientation="Vertical" Spacing="8">
+        <TreeView Items="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
+          <TreeView.ItemTemplate>
+            <TreeDataTemplate ItemsSource="{Binding Children}">
+              <TextBlock Text="{Binding Header}"/>
+            </TreeDataTemplate>
+          </TreeView.ItemTemplate>
+        </TreeView>
+
+        <Button Command="{Binding AddItemCommand}">Add</Button>
+
+        <Button Command="{Binding RemoveItemCommand}">Remove</Button>
+
+        <ComboBox SelectedIndex="{Binding SelectionMode, Mode=TwoWay}">
+          <ComboBoxItem>Single</ComboBoxItem>
+          <ComboBoxItem>Multiple</ComboBoxItem>
+          <ComboBoxItem>Toggle</ComboBoxItem>
+          <ComboBoxItem>AlwaysSelected</ComboBoxItem>
+        </ComboBox>
+      </StackPanel>
     </StackPanel>
   </StackPanel>
 </UserControl>

+ 84 - 9
samples/ControlCatalog/Pages/TreeViewPage.xaml.cs

@@ -1,8 +1,9 @@
-using System.Collections;
-using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.Linq;
+using System.Reactive;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
+using ReactiveUI;
 
 namespace ControlCatalog.Pages
 {
@@ -10,8 +11,8 @@ namespace ControlCatalog.Pages
     {
         public TreeViewPage()
         {
-            this.InitializeComponent();
-            DataContext = new Node().Children;
+            InitializeComponent();
+            DataContext = new PageViewModel();
         }
 
         private void InitializeComponent()
@@ -19,22 +20,96 @@ namespace ControlCatalog.Pages
             AvaloniaXamlLoader.Load(this);
         }
 
-        public class Node
+        private class PageViewModel : ReactiveObject
         {
-            private IList<Node> _children;
+            private SelectionMode _selectionMode;
+
+            public PageViewModel()
+            {
+                Node root = new Node();
+                Items = root.Children;
+                SelectedItems = new ObservableCollection<Node>();
+
+                AddItemCommand = ReactiveCommand.Create(() =>
+                {
+                    Node parentItem = SelectedItems.Count > 0 ? SelectedItems[0] : root;
+                    parentItem.AddNewItem();
+                });
+
+                RemoveItemCommand = ReactiveCommand.Create(() =>
+                {
+                    while (SelectedItems.Count > 0)
+                    {
+                        Node lastItem = SelectedItems[0];
+                        RecursiveRemove(Items, lastItem);
+                        SelectedItems.Remove(lastItem);
+                    }
+
+                    bool RecursiveRemove(ObservableCollection<Node> items, Node selectedItem)
+                    {
+                        if (items.Remove(selectedItem))
+                        {
+                            return true;
+                        }
+
+                        foreach (Node item in items)
+                        {
+                            if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem))
+                            {
+                                return true;
+                            }
+                        }
+
+                        return false;
+                    }
+                });
+            }
+
+            public ObservableCollection<Node> Items { get; }
+
+            public ObservableCollection<Node> SelectedItems { get; }
+
+            public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
+
+            public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
+
+            public SelectionMode SelectionMode
+            {
+                get => _selectionMode;
+                set
+                {
+                    SelectedItems.Clear();
+                    this.RaiseAndSetIfChanged(ref _selectionMode, value);
+                }
+            }
+        }
+
+        private class Node
+        {
+            private int _counter;
+            private ObservableCollection<Node> _children;
+
             public string Header { get; private set; }
-            public IList<Node> Children
+
+            public bool AreChildrenInitialized => _children != null;
+
+            public ObservableCollection<Node> Children
             {
                 get
                 {
                     if (_children == null)
                     {
-                        _children = Enumerable.Range(1, 10).Select(i => new Node() {Header = $"Item {i}"})
-                            .ToArray();
+                        _children = new ObservableCollection<Node>(Enumerable.Range(1, 10).Select(i => CreateNewNode()));
                     }
                     return _children;
                 }
             }
+
+            public void AddNewItem() => Children.Add(CreateNewNode());
+
+            public override string ToString() => Header;
+
+            private Node CreateNewNode() => new Node {Header = $"Item {_counter++}"};
         }
     }
 }

+ 18 - 11
samples/ControlCatalog/SideBar.xaml

@@ -24,23 +24,28 @@
                             Name="PART_ScrollViewer"
                             HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
                             VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
-                            Background="{TemplateBinding Background}">
+                            Background="{TemplateBinding Background}"
+                            DockPanel.Dock="Left">
                             <ItemsPresenter
                                 Name="PART_ItemsPresenter"                          
                                 Items="{TemplateBinding Items}"
                                 ItemsPanel="{TemplateBinding ItemsPanel}"
-                                ItemTemplate="{TemplateBinding ItemTemplate}"
-                                MemberSelector="{TemplateBinding MemberSelector}">
+                                ItemTemplate="{TemplateBinding ItemTemplate}">
                             </ItemsPresenter>
                         </ScrollViewer>
-                        <ContentPresenter
-                            Name="PART_SelectedContentHost"
-                            Margin="{TemplateBinding Padding}"                           
-                            HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
-                            VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
-                            Content="{TemplateBinding SelectedContent}"
-                            ContentTemplate="{TemplateBinding SelectedContentTemplate}">
-                        </ContentPresenter>
+                        <ContentControl Content="{TemplateBinding Tag}" HorizontalContentAlignment="Right" DockPanel.Dock="Bottom"/>
+                        <ScrollViewer
+                            HorizontalScrollBarVisibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=SelectedItem.(ScrollViewer.HorizontalScrollBarVisibility)}"
+                            VerticalScrollBarVisibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=SelectedItem.(ScrollViewer.VerticalScrollBarVisibility)}">
+                            <ContentPresenter
+                                    Name="PART_SelectedContentHost"
+                                    Margin="{TemplateBinding Padding}"                           
+                                    HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
+                                    VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
+                                    Content="{TemplateBinding SelectedContent}"
+                                    ContentTemplate="{TemplateBinding SelectedContentTemplate}">
+                            </ContentPresenter>
+                        </ScrollViewer>
                     </DockPanel>
                 </Border>
             </ControlTemplate>
@@ -59,6 +64,8 @@
                 <DoubleTransition Property="Opacity" Duration="0:0:0.150"/>
             </Transitions>
         </Setter>
+        <Setter Property="(ScrollViewer.HorizontalScrollBarVisibility)" Value="Auto"/>
+        <Setter Property="(ScrollViewer.VerticalScrollBarVisibility)" Value="Auto"/>
     </Style>
     <Style Selector="TabControl.sidebar > TabItem:pointerover">
         <Setter Property="Opacity" Value="1"/>

+ 27 - 0
samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs

@@ -0,0 +1,27 @@
+using System.Collections.ObjectModel;
+using System.Linq;
+using ReactiveUI;
+
+namespace ControlCatalog.ViewModels
+{
+    public class ItemsRepeaterPageViewModel : ReactiveObject
+    {
+        private int newItemIndex = 1;
+
+        public ItemsRepeaterPageViewModel()
+        {
+            Items = new ObservableCollection<string>(
+                Enumerable.Range(1, 100000).Select(i => $"Item {i}"));
+        }
+
+        public ObservableCollection<string> Items { get; }
+
+        public string SelectedItem { get; set; }
+
+        public void AddItem()
+        {
+            var index = SelectedItem != null ? Items.IndexOf(SelectedItem) : -1;
+            Items.Insert(index + 1, $"New Item {newItemIndex++}");
+        }
+    }
+}

+ 1 - 2
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@@ -1,11 +1,10 @@
 using System.Reactive;
 using Avalonia.Controls.Notifications;
-using Avalonia.Diagnostics.ViewModels;
 using ReactiveUI;
 
 namespace ControlCatalog.ViewModels
 {
-    class MainWindowViewModel : ViewModelBase
+    class MainWindowViewModel : ReactiveObject
     {
         private IManagedNotificationManager _notificationManager;
 

+ 2 - 3
samples/RenderDemo/SideBar.xaml

@@ -20,8 +20,7 @@
                                 Name="PART_ItemsPresenter"                          
                                 Items="{TemplateBinding Items}"
                                 ItemsPanel="{TemplateBinding ItemsPanel}"
-                                ItemTemplate="{TemplateBinding ItemTemplate}"
-                                MemberSelector="{TemplateBinding MemberSelector}">
+                                ItemTemplate="{TemplateBinding ItemTemplate}">
                             </ItemsPresenter>
                         </ScrollViewer>
                         <ContentPresenter
@@ -63,4 +62,4 @@
     <Style Selector="TabControl.sidebar > TabItem:selected /template/ ContentPresenter#PART_ContentPresenter">
         <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush2}"/>
     </Style>
-</Styles>
+</Styles>

+ 1 - 0
samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs

@@ -9,6 +9,7 @@ using Avalonia.Controls;
 using Avalonia.Controls.Primitives;
 using ReactiveUI.Legacy;
 using ReactiveUI;
+using Avalonia.Layout;
 
 namespace VirtualizationDemo.ViewModels
 {

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

@@ -71,10 +71,5 @@ namespace Avalonia.Android
         {
             throw new NotSupportedException();
         }
-
-        public IPopupImpl CreatePopup()
-        {
-            return new PopupImpl();
-        }
     }
 }

+ 1 - 1
src/Android/Avalonia.Android/Avalonia.Android.csproj

@@ -1,6 +1,6 @@
 <Project Sdk="MSBuild.Sdk.Extras">
   <PropertyGroup>
-    <TargetFramework>monoandroid44</TargetFramework>
+    <TargetFramework>monoandroid80</TargetFramework>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
   <ItemGroup>

+ 0 - 112
src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs

@@ -1,112 +0,0 @@
-using System;
-using Android.Content;
-using Android.Graphics;
-using Android.Runtime;
-using Android.Views;
-using Avalonia.Controls;
-using Avalonia.Platform;
-
-namespace Avalonia.Android.Platform.SkiaPlatform
-{
-    class PopupImpl : TopLevelImpl, IPopupImpl
-    {
-        private PixelPoint _position;
-        private bool _isAdded;
-        Action IWindowBaseImpl.Activated { get; set; }
-        public Action<PixelPoint> PositionChanged { get; set; }
-        public Action Deactivated { get; set; }
-
-        public PopupImpl() : base(ActivityTracker.Current, true)
-        {
-        }
-
-        private Size _clientSize = new Size(1, 1);
-
-        public void Resize(Size value)
-        {
-            if (View == null)
-                return;
-            _clientSize = value;
-            UpdateParams();
-        }
-
-        public void SetMinMaxSize(Size minSize, Size maxSize)
-        {
-        }
-
-        public IScreenImpl Screen { get; }
-
-        public PixelPoint Position
-        {
-            get { return _position; }
-            set
-            {
-                _position = value;
-                PositionChanged?.Invoke(_position);
-                UpdateParams();
-            }
-        }
-
-        WindowManagerLayoutParams CreateParams() => new WindowManagerLayoutParams(0,
-            WindowManagerFlags.NotTouchModal, Format.Translucent)
-        {
-            Gravity = GravityFlags.Left | GravityFlags.Top,
-            WindowAnimations = 0,
-            X = (int) _position.X,
-            Y = (int) _position.Y,
-            Width = Math.Max(1, (int) _clientSize.Width),
-            Height = Math.Max(1, (int) _clientSize.Height)
-        };
-
-        void UpdateParams()
-        {
-            if (_isAdded)
-                ActivityTracker.Current?.WindowManager?.UpdateViewLayout(View, CreateParams());
-        }
-
-        public override void Show()
-        {
-            if (_isAdded)
-                return;
-            ActivityTracker.Current.WindowManager.AddView(View, CreateParams());
-            _isAdded = true;
-        }
-
-        public override void Hide()
-        {
-            if (_isAdded)
-            {
-                var wm = View.Context.ApplicationContext.GetSystemService(Context.WindowService)
-                    .JavaCast<IWindowManager>();
-                wm.RemoveView(View);
-                _isAdded = false;
-            }
-        }
-
-        public override void Dispose()
-        {
-            Hide();
-            base.Dispose();
-        }
-
-
-        public void Activate()
-        {
-        }
-
-        public void BeginMoveDrag()
-        {
-            //Not supported
-        }
-
-        public void BeginResizeDrag(WindowEdge edge)
-        {
-            //Not supported
-        }
-
-        public void SetTopmost(bool value)
-        {
-            //Not supported
-        }
-    }
-}

+ 2 - 0
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -191,6 +191,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             }
         }
 
+        public IPopupImpl CreatePopup() => null;
+
         ILockedFramebuffer IFramebufferPlatformSurface.Lock()=>new AndroidFramebuffer(_view.Holder.Surface);
     }
 }

+ 2 - 1
src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj

@@ -16,7 +16,7 @@
     <AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
     <GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
     <AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk>
-    <TargetFrameworkVersion>v4.4</TargetFrameworkVersion>
+    <TargetFrameworkVersion>v8.0</TargetFrameworkVersion>
     <AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@@ -150,6 +150,7 @@
   </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
   <Import Project="..\..\..\build\Serilog.props" />
+  <Import Project="..\..\..\build\Base.props" />
   <Import Project="..\..\..\build\Rx.props" />
   <Import Project="..\..\..\build\System.Memory.props" />
   <Import Project="..\..\..\build\AndroidWorkarounds.props" />

+ 39 - 64
src/Avalonia.Base/AvaloniaObject.cs

@@ -163,6 +163,37 @@ namespace Avalonia
             SetValue(property, AvaloniaProperty.UnsetValue);
         }
 
+        /// <summary>
+        /// Compares two objects using reference equality.
+        /// </summary>
+        /// <param name="obj">The object to compare.</param>
+        /// <remarks>
+        /// Overriding Equals and GetHashCode on an AvaloniaObject is disallowed for two reasons:
+        /// 
+        /// - AvaloniaObjects are by their nature mutable
+        /// - The presence of attached properties means that the semantics of equality are
+        ///   difficult to define
+        /// 
+        /// See https://github.com/AvaloniaUI/Avalonia/pull/2747 for the discussion that prompted
+        /// this.
+        /// </remarks>
+        public sealed override bool Equals(object obj) => base.Equals(obj);
+
+        /// <summary>
+        /// Gets the hash code for the object.
+        /// </summary>
+        /// <remarks>
+        /// Overriding Equals and GetHashCode on an AvaloniaObject is disallowed for two reasons:
+        /// 
+        /// - AvaloniaObjects are by their nature mutable
+        /// - The presence of attached properties means that the semantics of equality are
+        ///   difficult to define
+        /// 
+        /// See https://github.com/AvaloniaUI/Avalonia/pull/2747 for the discussion that prompted
+        /// this.
+        /// </remarks>
+        public sealed override int GetHashCode() => base.GetHashCode();
+
         /// <summary>
         /// Gets a <see cref="AvaloniaProperty"/> value.
         /// </summary>
@@ -466,7 +497,7 @@ namespace Avalonia
         /// <param name="oldValue">The old property value.</param>
         /// <param name="newValue">The new property value.</param>
         /// <param name="priority">The priority of the binding that produced the value.</param>
-        protected void RaisePropertyChanged(
+        protected internal void RaisePropertyChanged(
             AvaloniaProperty property,
             object oldValue,
             object newValue,
@@ -508,45 +539,6 @@ namespace Avalonia
             }
         }
 
-        /// <summary>
-        /// A callback type for encapsulating complex logic for setting direct properties.
-        /// </summary>
-        /// <typeparam name="T">The type of the property.</typeparam>
-        /// <param name="value">The value to which to set the property.</param>
-        /// <param name="field">The backing field for the property.</param>
-        /// <param name="notifyWrapper">A wrapper for the property-changed notification.</param>
-        protected delegate void SetAndRaiseCallback<T>(T value, ref T field, Action<Action> notifyWrapper);
-
-        /// <summary>
-        /// Sets the backing field for a direct avalonia property, raising the 
-        /// <see cref="PropertyChanged"/> event if the value has changed.
-        /// </summary>
-        /// <typeparam name="T">The type of the property.</typeparam>
-        /// <param name="property">The property.</param>
-        /// <param name="field">The backing field.</param>
-        /// <param name="setterCallback">A callback called to actually set the value to the backing field.</param>
-        /// <param name="value">The value.</param>
-        /// <returns>
-        /// True if the value changed, otherwise false.
-        /// </returns>
-        protected bool SetAndRaise<T>(
-            AvaloniaProperty<T> property,
-            ref T field,
-            SetAndRaiseCallback<T> setterCallback,
-            T value)
-        {
-            Contract.Requires<ArgumentNullException>(setterCallback != null);
-            return Values.Setter.SetAndNotify(
-                property,
-                ref field,
-                (object update, ref T backing, Action<Action> notify) =>
-                {
-                    setterCallback((T)update, ref backing, notify);
-                    return true;
-                },
-                value);
-        }
-
         /// <summary>
         /// Sets the backing field for a direct avalonia property, raising the 
         /// <see cref="PropertyChanged"/> event if the value has changed.
@@ -561,32 +553,15 @@ namespace Avalonia
         protected bool SetAndRaise<T>(AvaloniaProperty<T> property, ref T field, T value)
         {
             VerifyAccess();
-            return SetAndRaise(
-                property,
-                ref field,
-                (T val, ref T backing, Action<Action> notifyWrapper)
-                    => SetAndRaiseCore(property, ref backing, val, notifyWrapper),
-                value);
-        }
 
-        /// <summary>
-        /// Default assignment logic for SetAndRaise.
-        /// </summary>
-        /// <typeparam name="T">The type of the property.</typeparam>
-        /// <param name="property">The property.</param>
-        /// <param name="field">The backing field.</param>
-        /// <param name="value">The value.</param>
-        /// <param name="notifyWrapper">A wrapper for the property-changed notification.</param>
-        /// <returns>
-        /// True if the value changed, otherwise false.
-        /// </returns>
-        private bool SetAndRaiseCore<T>(AvaloniaProperty property, ref T field, T value, Action<Action> notifyWrapper)
-        {
-            var old = field;
-            field = value;
+            if (EqualityComparer<T>.Default.Equals(field, value))
+            {
+                return false;
+            }
+
+            DeferredSetter<T> setter = Values.GetDirectDeferredSetter(property);
 
-            notifyWrapper(() => RaisePropertyChanged(property, old, value, BindingPriority.LocalValue));
-            return true;
+            return setter.SetAndNotify(this, property, ref field, value);
         }
 
         /// <summary>

+ 2 - 0
src/Avalonia.Base/Data/BindingOperations.cs

@@ -10,6 +10,8 @@ namespace Avalonia.Data
 {
     public static class BindingOperations
     {
+        public static readonly object DoNothing = new object();
+
         /// <summary>
         /// Applies an <see cref="InstancedBinding"/> a property on an <see cref="IAvaloniaObject"/>.
         /// </summary>

+ 1 - 1
src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs

@@ -31,7 +31,7 @@ namespace Avalonia.Data.Converters
         {
             if (value == null)
             {
-                return AvaloniaProperty.UnsetValue;
+                return targetType.IsValueType ? AvaloniaProperty.UnsetValue : null;
             }
 
             if (typeof(ICommand).IsAssignableFrom(targetType) && value is Delegate d && d.Method.GetParameters().Length <= 1)

+ 46 - 0
src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs

@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+
+namespace Avalonia.Data.Converters
+{
+    /// <summary>
+    /// A multi-value converter which calls <see cref="string.Format(string, object)"/>
+    /// </summary>
+    public class StringFormatMultiValueConverter : IMultiValueConverter
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StringFormatMultiValueConverter"/> class.
+        /// </summary>
+        /// <param name="format">The format string.</param>
+        /// <param name="inner">
+        /// An optional inner converter to be called before the format takes place.
+        /// </param>
+        public StringFormatMultiValueConverter(string format, IMultiValueConverter inner)
+        {
+            Contract.Requires<ArgumentNullException>(format != null);
+
+            Format = format;
+            Inner = inner;
+        }
+
+        /// <summary>
+        /// Gets an inner value converter which will be called before the string format takes place.
+        /// </summary>
+        public IMultiValueConverter Inner { get; }
+
+        /// <summary>
+        /// Gets the format string.
+        /// </summary>
+        public string Format { get; }
+
+        /// <inheritdoc/>
+        public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
+        {
+            return Inner == null
+                       ? string.Format(culture, Format, values.ToArray())
+                       : string.Format(culture, Format, Inner.Convert(values, targetType, parameter, culture));
+        }
+    }
+}

+ 31 - 0
src/Avalonia.Base/Data/Core/BindingExpression.cs

@@ -114,6 +114,11 @@ namespace Avalonia.Data.Core
         /// <inheritdoc/>
         public void OnNext(object value)
         {
+            if (value == BindingOperations.DoNothing)
+            {
+                return;
+            }
+
             using (_inner.Subscribe(_ => { }))
             {
                 var type = _inner.ResultType;
@@ -126,6 +131,11 @@ namespace Avalonia.Data.Core
                         ConverterParameter,
                         CultureInfo.CurrentCulture);
 
+                    if (converted == BindingOperations.DoNothing)
+                    {
+                        return;
+                    }
+
                     if (converted == AvaloniaProperty.UnsetValue)
                     {
                         converted = TypeUtilities.Default(type);
@@ -186,6 +196,11 @@ namespace Avalonia.Data.Core
         /// <inheritdoc/>
         private object ConvertValue(object value)
         {
+            if (value == BindingOperations.DoNothing)
+            {
+                return value;
+            }
+
             var notification = value as BindingNotification;
 
             if (notification == null)
@@ -196,6 +211,11 @@ namespace Avalonia.Data.Core
                     ConverterParameter,
                     CultureInfo.CurrentCulture);
 
+                if (converted == BindingOperations.DoNothing)
+                {
+                    return converted;
+                }
+
                 notification = converted as BindingNotification;
 
                 if (notification?.ErrorType == BindingErrorType.None)
@@ -327,7 +347,18 @@ namespace Avalonia.Data.Core
 
             public void OnNext(object value)
             {
+                if (value == BindingOperations.DoNothing)
+                {
+                    return;
+                }
+
                 var converted = _owner.ConvertValue(value);
+
+                if (converted == BindingOperations.DoNothing)
+                {
+                    return;
+                }
+
                 _owner._value = new WeakReference<object>(converted);
                 _owner.PublishNext(converted);
             }

+ 7 - 2
src/Avalonia.Base/IPriorityValueOwner.cs

@@ -29,6 +29,13 @@ namespace Avalonia
         /// <param name="notification">The notification.</param>
         void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification);
 
+        /// <summary>
+        /// Returns deferred setter for given non-direct property.
+        /// </summary>
+        /// <param name="property">Property.</param>
+        /// <returns>Deferred setter for given property.</returns>
+        DeferredSetter<object> GetNonDirectDeferredSetter(AvaloniaProperty property);
+
         /// <summary>
         /// Logs a binding error.
         /// </summary>
@@ -40,7 +47,5 @@ namespace Avalonia
         /// Ensures that the current thread is the UI thread.
         /// </summary>
         void VerifyAccess();
-
-        DeferredSetter<object> Setter { get; }
     }
 }

+ 18 - 16
src/Avalonia.Base/PriorityValue.cs

@@ -30,7 +30,9 @@ namespace Avalonia
         private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
 
         private readonly Func<object, object> _validate;
+        private readonly SetAndNotifyCallback<(object, int)> _setAndNotifyCallback;
         private (object value, int priority) _value;
+        private DeferredSetter<object> _setter;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="PriorityValue"/> class.
@@ -50,6 +52,7 @@ namespace Avalonia
             _valueType = valueType;
             _value = (AvaloniaProperty.UnsetValue, int.MaxValue);
             _validate = validate;
+            _setAndNotifyCallback = SetAndNotify;
         }
 
         /// <summary>
@@ -242,22 +245,22 @@ namespace Avalonia
         /// <param name="priority">The priority level that the value came from.</param>
         private void UpdateValue(object value, int priority)
         {
-            Owner.Setter.SetAndNotify(Property,
-                ref _value,
-                UpdateCore,
-                (value, priority));
-        }
+            var newValue = (value, priority);
+
+            if (newValue == _value)
+            {
+                return;
+            }
 
-        private bool UpdateCore(
-            object update,
-            ref (object value, int priority) backing,
-            Action<Action> notify)
-            => UpdateCore(((object, int))update, ref backing, notify);
+            if (_setter == null)
+            {
+                _setter = Owner.GetNonDirectDeferredSetter(Property);
+            }
+
+            _setter.SetAndNotifyCallback(Property, _setAndNotifyCallback, ref _value, newValue);
+        }
 
-        private bool UpdateCore(
-            (object value, int priority) update,
-            ref (object value, int priority) backing,
-            Action<Action> notify)
+        private void SetAndNotify(AvaloniaProperty property, ref (object value, int priority) backing, (object value, int priority) update)
         {
             var val = update.value;
             var notification = val as BindingNotification;
@@ -286,7 +289,7 @@ namespace Avalonia
 
                 if (notification == null || notification.HasValue)
                 {
-                    notify(() => Owner?.Changed(Property, ValuePriority, old, Value));
+                    Owner?.Changed(Property, ValuePriority, old, Value);
                 }
 
                 if (notification != null)
@@ -305,7 +308,6 @@ namespace Avalonia
                     val,
                     val?.GetType());
             }
-            return true;
         }
     }
 }

+ 150 - 0
src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs

@@ -0,0 +1,150 @@
+// 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;
+
+namespace Avalonia.Utilities
+{
+    /// <summary>
+    /// Stores values with <see cref="AvaloniaProperty"/> as key.
+    /// </summary>
+    /// <typeparam name="TValue">Stored value type.</typeparam>
+    internal sealed class AvaloniaPropertyValueStore<TValue>
+    {
+        private Entry[] _entries;
+
+        public AvaloniaPropertyValueStore()
+        {
+            // The last item in the list is always int.MaxValue
+            _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } };
+        }
+
+        private (int, bool) TryFindEntry(int propertyId)
+        {
+            if (_entries.Length <= 12)
+            {
+                // For small lists, we use an optimized linear search. Since the last item in the list
+                // is always int.MaxValue, we can skip a conditional branch in each iteration.
+                // By unrolling the loop, we can skip another unconditional branch in each iteration.
+
+                if (_entries[0].PropertyId >= propertyId)
+                    return (0, _entries[0].PropertyId == propertyId);
+                if (_entries[1].PropertyId >= propertyId)
+                    return (1, _entries[1].PropertyId == propertyId);
+                if (_entries[2].PropertyId >= propertyId)
+                    return (2, _entries[2].PropertyId == propertyId);
+                if (_entries[3].PropertyId >= propertyId)
+                    return (3, _entries[3].PropertyId == propertyId);
+                if (_entries[4].PropertyId >= propertyId)
+                    return (4, _entries[4].PropertyId == propertyId);
+                if (_entries[5].PropertyId >= propertyId)
+                    return (5, _entries[5].PropertyId == propertyId);
+                if (_entries[6].PropertyId >= propertyId)
+                    return (6, _entries[6].PropertyId == propertyId);
+                if (_entries[7].PropertyId >= propertyId)
+                    return (7, _entries[7].PropertyId == propertyId);
+                if (_entries[8].PropertyId >= propertyId)
+                    return (8, _entries[8].PropertyId == propertyId);
+                if (_entries[9].PropertyId >= propertyId)
+                    return (9, _entries[9].PropertyId == propertyId);
+                if (_entries[10].PropertyId >= propertyId)
+                    return (10, _entries[10].PropertyId == propertyId);
+            }
+            else
+            {
+                int low = 0;
+                int high = _entries.Length;
+                int id;
+
+                while (high - low > 3)
+                {
+                    int pivot = (high + low) / 2;
+                    id = _entries[pivot].PropertyId;
+
+                    if (propertyId == id)
+                        return (pivot, true);
+
+                    if (propertyId <= id)
+                        high = pivot;
+                    else
+                        low = pivot + 1;
+                }
+
+                do
+                {
+                    id = _entries[low].PropertyId;
+
+                    if (id == propertyId)
+                        return (low, true);
+
+                    if (id > propertyId)
+                        break;
+
+                    ++low;
+                }
+                while (low < high);
+            }
+
+            return (0, false);
+        }
+
+        public bool TryGetValue(AvaloniaProperty property, out TValue value)
+        {
+            (int index, bool found) = TryFindEntry(property.Id);
+            if (!found)
+            {
+                value = default;
+                return false;
+            }
+
+            value = _entries[index].Value;
+            return true;
+        }
+
+        public void AddValue(AvaloniaProperty property, TValue value)
+        {
+            Entry[] entries = new Entry[_entries.Length + 1];
+
+            for (int i = 0; i < _entries.Length; ++i)
+            {
+                if (_entries[i].PropertyId > property.Id)
+                {
+                    if (i > 0)
+                    {
+                        Array.Copy(_entries, 0, entries, 0, i);
+                    }
+
+                    entries[i] = new Entry { PropertyId = property.Id, Value = value };
+                    Array.Copy(_entries, i, entries, i + 1, _entries.Length - i);
+                    break;
+                }
+            }
+
+            _entries = entries;
+        }
+
+        public void SetValue(AvaloniaProperty property, TValue value)
+        {
+            _entries[TryFindEntry(property.Id).Item1].Value = value;
+        }
+
+        public Dictionary<AvaloniaProperty, TValue> ToDictionary()
+        {
+            var dict = new Dictionary<AvaloniaProperty, TValue>(_entries.Length - 1);
+
+            for (int i = 0; i < _entries.Length - 1; ++i)
+            {
+                dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
+            }
+
+            return dict;
+        }
+
+        private struct Entry
+        {
+            internal int PropertyId;
+            internal TValue Value;
+        }
+    }
+}

+ 80 - 126
src/Avalonia.Base/Utilities/DeferredSetter.cs

@@ -1,168 +1,122 @@
-using System;
-using System.Collections.Generic;
+// 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.Utilities
 {
+    /// <summary>
+    /// Callback invoked when deferred setter wants to set a value.
+    /// </summary>
+    /// <typeparam name="TValue">Value type.</typeparam>
+    /// <param name="property">Property being set.</param>
+    /// <param name="backing">Backing field reference.</param>
+    /// <param name="value">New value.</param>
+    internal delegate void SetAndNotifyCallback<TValue>(AvaloniaProperty property, ref TValue backing, TValue value);
+
     /// <summary>
     /// A utility class to enable deferring assignment until after property-changed notifications are sent.
     /// Used to fix #855.
     /// </summary>
     /// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
-    class DeferredSetter<TSetRecord>
+    internal sealed class DeferredSetter<TSetRecord>
     {
-        private struct NotifyDisposable : IDisposable
+        private readonly SingleOrQueue<TSetRecord> _pendingValues;
+        private bool _isNotifying;
+
+        public DeferredSetter()
         {
-            private readonly SettingStatus status;
+            _pendingValues = new SingleOrQueue<TSetRecord>();
+        }
 
-            internal NotifyDisposable(SettingStatus status)
-            {
-                this.status = status;
-                status.Notifying = true;
-            }
+        private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty<TSetRecord> property, ref TSetRecord backing, TSetRecord value)
+        {
+            var old = backing;
 
-            public void Dispose()
-            {
-                status.Notifying = false;
-            }
+            backing = value;
+
+            source.RaisePropertyChanged(property, old, value);
         }
 
-        /// <summary>
-        /// Information on current setting/notification status of a property.
-        /// </summary>
-        private class SettingStatus
+        public bool SetAndNotify(
+            AvaloniaObject source,
+            AvaloniaProperty<TSetRecord> property,
+            ref TSetRecord backing,
+            TSetRecord value)
         {
-            public bool Notifying { get; set; }
-
-            private SingleOrQueue<TSetRecord> pendingValues;
-            
-            public SingleOrQueue<TSetRecord> PendingValues
+            if (!_isNotifying)
             {
-                get
+                using (new NotifyDisposable(this))
                 {
-                    return pendingValues ?? (pendingValues = new SingleOrQueue<TSetRecord>());
+                    SetAndRaisePropertyChanged(source, property, ref backing, value);
                 }
-            }
-        }
 
-        private Dictionary<AvaloniaProperty, SettingStatus> _setRecords;
-        private Dictionary<AvaloniaProperty, SettingStatus> SetRecords
-            => _setRecords ?? (_setRecords = new Dictionary<AvaloniaProperty, SettingStatus>());
+                if (!_pendingValues.Empty)
+                {
+                    using (new NotifyDisposable(this))
+                    {
+                        while (!_pendingValues.Empty)
+                        {
+                            SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue());
+                        }
+                    }
+                }
 
-        private SettingStatus GetOrCreateStatus(AvaloniaProperty property)
-        {
-            if (!SetRecords.TryGetValue(property, out var status))
-            {
-                status = new SettingStatus();
-                SetRecords.Add(property, status);
+                return true;
             }
 
-            return status;
+            _pendingValues.Enqueue(value);
+
+            return false;
         }
 
-        /// <summary>
-        /// Mark the property as currently notifying.
-        /// </summary>
-        /// <param name="property">The property to mark as notifying.</param>
-        /// <returns>Returns a disposable that when disposed, marks the property as done notifying.</returns>
-        private NotifyDisposable MarkNotifying(AvaloniaProperty property)
+        public bool SetAndNotifyCallback<TValue>(AvaloniaProperty property, SetAndNotifyCallback<TValue> setAndNotifyCallback, ref TValue backing, TValue value)
+            where TValue : TSetRecord
         {
-            Contract.Requires<InvalidOperationException>(!IsNotifying(property));
-
-            SettingStatus status = GetOrCreateStatus(property);
-
-            return new NotifyDisposable(status);
-        }
+            if (!_isNotifying)
+            {
+                using (new NotifyDisposable(this))
+                {
+                    setAndNotifyCallback(property, ref backing, value);
+                }
 
-        /// <summary>
-        /// Check if the property is currently notifying listeners.
-        /// </summary>
-        /// <param name="property">The property.</param>
-        /// <returns>If the property is currently notifying listeners.</returns>
-        private bool IsNotifying(AvaloniaProperty property)
-            => SetRecords.TryGetValue(property, out var value) && value.Notifying;
+                if (!_pendingValues.Empty)
+                {
+                    using (new NotifyDisposable(this))
+                    {
+                        while (!_pendingValues.Empty)
+                        {
+                            setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue());
+                        }
+                    }
+                }
 
-        /// <summary>
-        /// Add a pending assignment for the property.
-        /// </summary>
-        /// <param name="property">The property.</param>
-        /// <param name="value">The value to assign.</param>
-        private void AddPendingSet(AvaloniaProperty property, TSetRecord value)
-        {
-            Contract.Requires<InvalidOperationException>(IsNotifying(property));
+                return true;
+            }
 
-            GetOrCreateStatus(property).PendingValues.Enqueue(value);
-        }
+            _pendingValues.Enqueue(value);
 
-        /// <summary>
-        /// Checks if there are any pending assignments for the property.
-        /// </summary>
-        /// <param name="property">The property to check.</param>
-        /// <returns>If the property has any pending assignments.</returns>
-        private bool HasPendingSet(AvaloniaProperty property)
-        {
-            return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty;
+            return false;
         }
 
         /// <summary>
-        /// Gets the first pending assignment for the property.
+        /// Disposable that marks the property as currently notifying.
+        /// When disposed, marks the property as done notifying.
         /// </summary>
-        /// <param name="property">The property to check.</param>
-        /// <returns>The first pending assignment for the property.</returns>
-        private TSetRecord GetFirstPendingSet(AvaloniaProperty property)
+        private readonly struct NotifyDisposable : IDisposable
         {
-            return GetOrCreateStatus(property).PendingValues.Dequeue();
-        }
-
-        public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback);
+            private readonly DeferredSetter<TSetRecord> _setter;
 
-        /// <summary>
-        /// Set the property and notify listeners while ensuring we don't get into a stack overflow as happens with #855 and #824
-        /// </summary>
-        /// <param name="property">The property to set.</param>
-        /// <param name="backing">The backing field for the property</param>
-        /// <param name="setterCallback">
-        /// A callback that actually sets the property.
-        /// The first parameter is the value to set, and the second is a wrapper that takes a callback that sends the property-changed notification.
-        /// </param>
-        /// <param name="value">The value to try to set.</param>
-        public bool SetAndNotify<TValue>(
-            AvaloniaProperty property,
-            ref TValue backing,
-            SetterDelegate<TValue> setterCallback,
-            TSetRecord value)
-        {
-            Contract.Requires<ArgumentNullException>(setterCallback != null);
-            if (!IsNotifying(property))
+            internal NotifyDisposable(DeferredSetter<TSetRecord> setter)
             {
-                bool updated = false;
-                if (!object.Equals(value, backing))
-                {
-                    updated = setterCallback(value, ref backing, notification =>
-                    {
-                        using (MarkNotifying(property))
-                        {
-                            notification();
-                        }
-                    });
-                }
-                while (HasPendingSet(property))
-                {
-                    updated |= setterCallback(GetFirstPendingSet(property), ref backing, notification =>
-                    {
-                        using (MarkNotifying(property))
-                        {
-                            notification();
-                        }
-                    });
-                }
-
-                return updated;
+                _setter = setter;
+                _setter._isNotifying = true;
             }
-            else if(!object.Equals(value, backing))
+
+            public void Dispose()
             {
-                AddPendingSet(property, value);
+                _setter._isNotifying = false;
             }
-            return false;
         }
     }
 }

+ 106 - 0
src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs

@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace Avalonia.Utilities
+{
+    /// <summary>
+    /// A task-like operation that is guaranteed to finish continuations synchronously,
+    /// can be used for parametrized one-shot events
+    /// </summary>
+    public struct SynchronousCompletionAsyncResult<T> : INotifyCompletion
+    {
+        private readonly SynchronousCompletionAsyncResultSource<T> _source;
+        private readonly T _result;
+        private readonly bool _isValid;
+        internal SynchronousCompletionAsyncResult(SynchronousCompletionAsyncResultSource<T> source)
+        {
+            _source = source;
+            _result = default;
+            _isValid = true;
+        }
+
+        public SynchronousCompletionAsyncResult(T result)
+        {
+            _result = result;
+            _source = null;
+            _isValid = true;
+        }
+
+        static void ThrowNotInitialized() =>
+            throw new InvalidOperationException("This SynchronousCompletionAsyncResult was not initialized");
+
+        public bool IsCompleted
+        {
+            get
+            {
+                if (!_isValid)
+                    ThrowNotInitialized();
+                return _source == null || _source.IsCompleted;
+            }
+        }
+
+        public T GetResult()
+        {
+            if (!_isValid)
+                ThrowNotInitialized();
+            return _source == null ? _result : _source.Result;
+        }
+
+
+        public void OnCompleted(Action continuation)
+        {
+            if (!_isValid)
+                ThrowNotInitialized();
+            if (_source == null)
+                continuation();
+            else
+                _source.OnCompleted(continuation);
+        }
+
+        public SynchronousCompletionAsyncResult<T> GetAwaiter() => this;
+    }
+
+    /// <summary>
+    /// Source for incomplete SynchronousCompletionAsyncResult
+    /// </summary>
+    /// <typeparam name="T"></typeparam>
+    public class SynchronousCompletionAsyncResultSource<T>
+    {
+        private T _result;
+        internal bool IsCompleted { get; private set; }
+        public SynchronousCompletionAsyncResult<T> AsyncResult => new SynchronousCompletionAsyncResult<T>(this);
+        
+        internal T Result => IsCompleted ?
+            _result :
+            throw new InvalidOperationException("Asynchronous operation is not yet completed");
+        
+        private List<Action> _continuations;
+
+        internal void OnCompleted(Action continuation)
+        {
+            if(_continuations==null)
+                _continuations = new List<Action>();
+            _continuations.Add(continuation);
+        }
+
+        public void SetResult(T result)
+        {
+            if (IsCompleted)
+                throw new InvalidOperationException("Asynchronous operation is already completed");
+            _result = result;
+            IsCompleted = true;
+            if(_continuations!=null)
+                foreach (var c in _continuations)
+                    c();
+            _continuations = null;
+        }
+
+        public void TrySetResult(T result)
+        {
+            if(IsCompleted)
+                return;
+            SetResult(result);
+        }
+    }
+}

+ 28 - 124
src/Avalonia.Base/ValueStore.cs

@@ -7,21 +7,15 @@ namespace Avalonia
 {
     internal class ValueStore : IPriorityValueOwner
     {
-        private struct Entry
-        {
-            internal int PropertyId;
-            internal object Value;
-        }
-
+        private readonly AvaloniaPropertyValueStore<object> _propertyValues;
+        private readonly AvaloniaPropertyValueStore<object> _deferredSetters;
         private readonly AvaloniaObject _owner;
-        private Entry[] _entries;
 
         public ValueStore(AvaloniaObject owner)
         {
             _owner = owner;
-
-            // The last item in the list is always int.MaxValue
-            _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = null } };
+            _propertyValues = new AvaloniaPropertyValueStore<object>();
+            _deferredSetters = new AvaloniaPropertyValueStore<object>();
         }
 
         public IDisposable AddBinding(
@@ -31,7 +25,7 @@ namespace Avalonia
         {
             PriorityValue priorityValue;
 
-            if (TryGetValue(property, out var v))
+            if (_propertyValues.TryGetValue(property, out var v))
             {
                 priorityValue = v as PriorityValue;
 
@@ -39,13 +33,13 @@ namespace Avalonia
                 {
                     priorityValue = CreatePriorityValue(property);
                     priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
-                    SetValueInternal(property, priorityValue);
+                    _propertyValues.SetValue(property, priorityValue);
                 }
             }
             else
             {
                 priorityValue = CreatePriorityValue(property);
-                AddValueInternal(property, priorityValue);
+                _propertyValues.AddValue(property, priorityValue);
             }
 
             return priorityValue.Add(source, (int)priority);
@@ -55,7 +49,7 @@ namespace Avalonia
         {
             PriorityValue priorityValue;
 
-            if (TryGetValue(property, out var v))
+            if (_propertyValues.TryGetValue(property, out var v))
             {
                 priorityValue = v as PriorityValue;
 
@@ -63,7 +57,7 @@ namespace Avalonia
                 {
                     if (priority == (int)BindingPriority.LocalValue)
                     {
-                        SetValueInternal(property, Validate(property, value));
+                        _propertyValues.SetValue(property, Validate(property, value));
                         Changed(property, priority, v, value);
                         return;
                     }
@@ -71,7 +65,7 @@ namespace Avalonia
                     {
                         priorityValue = CreatePriorityValue(property);
                         priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
-                        SetValueInternal(property, priorityValue);
+                        _propertyValues.SetValue(property, priorityValue);
                     }
                 }
             }
@@ -84,14 +78,14 @@ namespace Avalonia
 
                 if (priority == (int)BindingPriority.LocalValue)
                 {
-                    AddValueInternal(property, Validate(property, value));
+                    _propertyValues.AddValue(property, Validate(property, value));
                     Changed(property, priority, AvaloniaProperty.UnsetValue, value);
                     return;
                 }
                 else
                 {
                     priorityValue = CreatePriorityValue(property);
-                    AddValueInternal(property, priorityValue);
+                    _propertyValues.AddValue(property, priorityValue);
                 }
             }
 
@@ -110,14 +104,9 @@ namespace Avalonia
 
         public IDictionary<AvaloniaProperty, object> GetSetValues()
         {
-            var dict = new Dictionary<AvaloniaProperty, object>(_entries.Length - 1);
-            for (int i = 0; i < _entries.Length - 1; ++i)
-            {
-                dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
-            }
-
-            return dict;
+            return _propertyValues.ToDictionary();
         }
+
         public void LogError(AvaloniaProperty property, Exception e)
         {
             _owner.LogBindingError(property, e);
@@ -127,7 +116,7 @@ namespace Avalonia
         {
             var result = AvaloniaProperty.UnsetValue;
 
-            if (TryGetValue(property, out var value))
+            if (_propertyValues.TryGetValue(property, out var value))
             {
                 result = (value is PriorityValue priorityValue) ? priorityValue.Value : value;
             }
@@ -137,12 +126,12 @@ namespace Avalonia
 
         public bool IsAnimating(AvaloniaProperty property)
         {
-            return TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
+            return _propertyValues.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
         }
 
         public bool IsSet(AvaloniaProperty property)
         {
-            if (TryGetValue(property, out var value))
+            if (_propertyValues.TryGetValue(property, out var value))
             {
                 return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue;
             }
@@ -152,7 +141,7 @@ namespace Avalonia
 
         public void Revalidate(AvaloniaProperty property)
         {
-            if (TryGetValue(property, out var value))
+            if (_propertyValues.TryGetValue(property, out var value))
             {
                 (value as PriorityValue)?.Revalidate();
             }
@@ -189,113 +178,28 @@ namespace Avalonia
             return value;
         }
 
-        private DeferredSetter<object> _deferredSetter;
-
-        public DeferredSetter<object> Setter
+        private DeferredSetter<T> GetDeferredSetter<T>(AvaloniaProperty property)
         {
-            get
+            if (_deferredSetters.TryGetValue(property, out var deferredSetter))
             {
-                return _deferredSetter ??
-                    (_deferredSetter = new DeferredSetter<object>());
+                return (DeferredSetter<T>)deferredSetter;
             }
-        }
 
-        private bool TryGetValue(AvaloniaProperty property, out object value)
-        {
-            (int index, bool found) = TryFindEntry(property.Id);
-            if (!found)
-            {
-                value = null;
-                return false;
-            }
-
-            value = _entries[index].Value;
-            return true;
-        }
-
-        private void AddValueInternal(AvaloniaProperty property, object value)
-        {
-            Entry[] entries = new Entry[_entries.Length + 1];
-
-            for (int i = 0; i < _entries.Length; ++i)
-            {
-                if (_entries[i].PropertyId > property.Id)
-                {
-                    if (i > 0)
-                    {
-                        Array.Copy(_entries, 0, entries, 0, i);
-                    }
+            var newDeferredSetter = new DeferredSetter<T>();
 
-                    entries[i] = new Entry { PropertyId = property.Id, Value = value };
-                    Array.Copy(_entries, i, entries, i + 1, _entries.Length - i);
-                    break;
-                }
-            }
+            _deferredSetters.AddValue(property, newDeferredSetter);
 
-            _entries = entries;
+            return newDeferredSetter;
         }
 
-        private void SetValueInternal(AvaloniaProperty property, object value)
+        public DeferredSetter<object> GetNonDirectDeferredSetter(AvaloniaProperty property)
         {
-            _entries[TryFindEntry(property.Id).Item1].Value = value;
+            return GetDeferredSetter<object>(property);
         }
 
-        private (int, bool) TryFindEntry(int propertyId)
+        public DeferredSetter<T> GetDirectDeferredSetter<T>(AvaloniaProperty<T> property)
         {
-            if (_entries.Length <= 12)
-            {
-                // For small lists, we use an optimized linear search. Since the last item in the list
-                // is always int.MaxValue, we can skip a conditional branch in each iteration.
-                // By unrolling the loop, we can skip another unconditional branch in each iteration.
-
-                if (_entries[0].PropertyId >= propertyId) return (0, _entries[0].PropertyId == propertyId);
-                if (_entries[1].PropertyId >= propertyId) return (1, _entries[1].PropertyId == propertyId);
-                if (_entries[2].PropertyId >= propertyId) return (2, _entries[2].PropertyId == propertyId);
-                if (_entries[3].PropertyId >= propertyId) return (3, _entries[3].PropertyId == propertyId);
-                if (_entries[4].PropertyId >= propertyId) return (4, _entries[4].PropertyId == propertyId);
-                if (_entries[5].PropertyId >= propertyId) return (5, _entries[5].PropertyId == propertyId);
-                if (_entries[6].PropertyId >= propertyId) return (6, _entries[6].PropertyId == propertyId);
-                if (_entries[7].PropertyId >= propertyId) return (7, _entries[7].PropertyId == propertyId);
-                if (_entries[8].PropertyId >= propertyId) return (8, _entries[8].PropertyId == propertyId);
-                if (_entries[9].PropertyId >= propertyId) return (9, _entries[9].PropertyId == propertyId);
-                if (_entries[10].PropertyId >= propertyId) return (10, _entries[10].PropertyId == propertyId);
-            }
-            else
-            {
-                int low = 0;
-                int high = _entries.Length;
-                int id;
-
-                while (high - low > 3)
-                {
-                    int pivot = (high + low) / 2;
-                    id = _entries[pivot].PropertyId;
-
-                    if (propertyId == id)
-                        return (pivot, true);
-
-                    if (propertyId <= id)
-                        high = pivot;
-                    else
-                        low = pivot + 1;
-                }
-
-                do
-                {
-                    id = _entries[low].PropertyId;
-
-                    if (id == propertyId)
-                        return (low, true);
-
-                    if (id > propertyId)
-                        break;
-
-                    ++low;
-                }
-                while (low < high);
-            }
-
-            return (0, false);
+            return GetDeferredSetter<T>(property);
         }
     }
 }

+ 5 - 5
src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs

@@ -75,9 +75,9 @@ namespace Avalonia.Build.Tasks
                     .First(c => c.Parameters.Count == 1));
 
             var runtimeHelpers = typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.XamlIlRuntimeHelpers");
-            var rootServiceProviderField = asm.MainModule.ImportReference(
-                typeSystem.GetTypeReference(runtimeHelpers).Resolve().Fields
-                    .First(x => x.Name == "RootServiceProviderV1"));
+            var createRootServiceProviderMethod = asm.MainModule.ImportReference(
+                typeSystem.GetTypeReference(runtimeHelpers).Resolve().Methods
+                    .First(x => x.Name == "CreateRootServiceProviderV2"));
             
             var loaderDispatcherDef = new TypeDefinition("CompiledAvaloniaXaml", "!XamlLoader",
                 TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
@@ -211,7 +211,7 @@ namespace Avalonia.Build.Tasks
                             trampoline.Parameters.Add(new ParameterDefinition(classTypeDefinition));
                             classTypeDefinition.Methods.Add(trampoline);
 
-                            var regularStart = Instruction.Create(OpCodes.Ldsfld, rootServiceProviderField);
+                            var regularStart = Instruction.Create(OpCodes.Call, createRootServiceProviderMethod);
                             
                             trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldsfld, designLoaderField));
                             trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Brfalse, regularStart));
@@ -307,7 +307,7 @@ namespace Avalonia.Build.Tasks
                                     i.Add(Instruction.Create(OpCodes.Newobj, parameterlessConstructor));
                                 else
                                 {
-                                    i.Add(Instruction.Create(OpCodes.Ldsfld, rootServiceProviderField));
+                                    i.Add(Instruction.Create(OpCodes.Call, createRootServiceProviderMethod));
                                     i.Add(Instruction.Create(OpCodes.Call, compiledBuildMethod));
                                 }
 

+ 1 - 0
src/Avalonia.Controls.DataGrid/DataGrid.cs

@@ -24,6 +24,7 @@ using System.Linq;
 using Avalonia.Input.Platform;
 using System.ComponentModel.DataAnnotations;
 using Avalonia.Controls.Utils;
+using Avalonia.Layout;
 
 namespace Avalonia.Controls
 {

+ 1 - 2
src/Avalonia.Controls.DataGrid/Themes/Default.xaml

@@ -195,7 +195,6 @@
     <Setter Property="GridLinesVisibility" Value="Vertical" />
     <Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource ThemeBorderLightBrush}" />
     <Setter Property="VerticalGridLinesBrush" Value="{DynamicResource ThemeBorderLightBrush}" />
-    <Setter Property="HeadersVisibility" Value="Column" />
     <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderDarkBrush}"/>
     <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}" />
     <Setter Property="DropLocationIndicatorTemplate">
@@ -230,4 +229,4 @@
       </ControlTemplate>
     </Setter>
   </Style>
-</Styles>
+</Styles>

+ 1 - 1
src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs

@@ -6,7 +6,7 @@ using System;
 namespace Avalonia.Controls.ApplicationLifetimes
 {
     /// <summary>
-    /// Contains the arguments for the <see cref="IClassicDesktopStyleApplicationLifetime.Exit"/> event.
+    /// Contains the arguments for the <see cref="IControlledApplicationLifetime.Exit"/> event.
     /// </summary>
     public class ControlledApplicationLifetimeExitEventArgs : EventArgs
     {

+ 1 - 1
src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs

@@ -8,7 +8,7 @@ using System.Linq;
 namespace Avalonia.Controls.ApplicationLifetimes
 {
     /// <summary>
-    /// Contains the arguments for the <see cref="IClassicDesktopStyleApplicationLifetime.Startup"/> event.
+    /// Contains the arguments for the <see cref="IControlledApplicationLifetime.Startup"/> event.
     /// </summary>
     public class ControlledApplicationLifetimeStartupEventArgs : EventArgs
     {

+ 1 - 27
src/Avalonia.Controls/AutoCompleteBox.cs

@@ -345,7 +345,6 @@ namespace Avalonia.Controls
         /// </summary>
         private IDisposable _collectionChangeSubscription;
 
-        private IMemberSelector _valueMemberSelector;
         private Func<string, CancellationToken, Task<IEnumerable<object>>> _asyncPopulator;
         private CancellationTokenSource _populationCancellationTokenSource;
 
@@ -541,12 +540,6 @@ namespace Avalonia.Controls
                 o => o.Items,
                 (o, v) => o.Items = v);
 
-        public static readonly DirectProperty<AutoCompleteBox, IMemberSelector> ValueMemberSelectorProperty =
-            AvaloniaProperty.RegisterDirect<AutoCompleteBox, IMemberSelector>(
-                nameof(ValueMemberSelector),
-                o => o.ValueMemberSelector,
-                (o, v) => o.ValueMemberSelector = v);
-
         public static readonly DirectProperty<AutoCompleteBox, Func<string, CancellationToken, Task<IEnumerable<object>>>> AsyncPopulatorProperty =
             AvaloniaProperty.RegisterDirect<AutoCompleteBox, Func<string, CancellationToken, Task<IEnumerable<object>>>>(
                 nameof(AsyncPopulator),
@@ -795,7 +788,7 @@ namespace Avalonia.Controls
                 var template =
                     new FuncDataTemplate(
                         typeof(object),
-                        o =>
+                        (o, _) =>
                         {
                             var control = new ContentControl();
                             control.Bind(ContentControl.ContentProperty, value);
@@ -958,20 +951,6 @@ namespace Avalonia.Controls
             }
         }
 
-        /// <summary>
-        /// Gets or sets the MemberSelector that is used to get values for
-        /// display in the text portion of the
-        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
-        /// </summary>
-        /// <value>The MemberSelector that is used to get values for display in
-        /// the text portion of the
-        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.</value>
-        public IMemberSelector ValueMemberSelector
-        {
-            get { return _valueMemberSelector; }
-            set { SetAndRaise(ValueMemberSelectorProperty, ref _valueMemberSelector, value); }
-        }
-
         /// <summary>
         /// Gets or sets the selected item in the drop-down.
         /// </summary>
@@ -1841,11 +1820,6 @@ namespace Avalonia.Controls
                 return _valueBindingEvaluator.GetDynamicValue(value) ?? String.Empty;
             }
 
-            if (_valueMemberSelector != null)
-            {
-                value = _valueMemberSelector.Select(value);
-            }
-
             return value == null ? String.Empty : value.ToString();
         }
 

+ 14 - 3
src/Avalonia.Controls/Calendar/CalendarItem.cs

@@ -4,6 +4,7 @@
 // All other rights reserved.
 
 using System;
+using System.Collections.Generic;
 using System.Diagnostics;
 using System.Globalization;
 using Avalonia.Data;
@@ -193,6 +194,9 @@ namespace Avalonia.Controls.Primitives
         {
             if (MonthView != null)
             {
+                var childCount = Calendar.RowsPerMonth + Calendar.RowsPerMonth * Calendar.ColumnsPerMonth;
+                var children = new List<IControl>(childCount);
+
                 for (int i = 0; i < Calendar.RowsPerMonth; i++)
                 {
                     if (_dayTitleTemplate != null)
@@ -201,7 +205,7 @@ namespace Avalonia.Controls.Primitives
                         cell.DataContext = string.Empty;
                         cell.SetValue(Grid.RowProperty, 0);
                         cell.SetValue(Grid.ColumnProperty, i);
-                        MonthView.Children.Add(cell);
+                        children.Add(cell);
                     }
                 }
 
@@ -222,13 +226,18 @@ namespace Avalonia.Controls.Primitives
                         cell.PointerEnter += Cell_MouseEnter;
                         cell.PointerLeave += Cell_MouseLeave;
                         cell.Click += Cell_Click;
-                        MonthView.Children.Add(cell);
+                        children.Add(cell);
                     }
                 }
+                
+                MonthView.Children.AddRange(children);
             }
 
             if (YearView != null)
             {
+                var childCount = Calendar.RowsPerYear * Calendar.ColumnsPerYear;
+                var children = new List<IControl>(childCount);
+
                 CalendarButton month;
                 for (int i = 0; i < Calendar.RowsPerYear; i++)
                 {
@@ -246,9 +255,11 @@ namespace Avalonia.Controls.Primitives
                         month.CalendarLeftMouseButtonUp += Month_CalendarButtonMouseUp;
                         month.PointerEnter += Month_MouseEnter;
                         month.PointerLeave += Month_MouseLeave;
-                        YearView.Children.Add(month);
+                        children.Add(month);
                     }
                 }
+
+                YearView.Children.AddRange(children);
             }
         }
 

+ 2 - 3
src/Avalonia.Controls/ComboBox.cs

@@ -202,7 +202,7 @@ namespace Avalonia.Controls
         {
             if (!e.Handled)
             {
-                if (_popup?.PopupRoot != null && ((IVisual)e.Source).GetVisualRoot() == _popup?.PopupRoot)
+                if (_popup?.IsInsidePopup((IVisual)e.Source) == true)
                 {
                     if (UpdateSelectionFromEventSource(e.Source))
                     {
@@ -333,8 +333,7 @@ namespace Avalonia.Controls
             }
             else
             {
-                var selector = MemberSelector;
-                SelectionBoxItem = selector != null ? selector.Select(item) : item;
+                SelectionBoxItem = item;
             }
         }
 

+ 35 - 7
src/Avalonia.Controls/ContextMenu.cs

@@ -1,12 +1,13 @@
 using System;
 using System.ComponentModel;
 using System.Linq;
-using System.Reactive.Linq;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Platform;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Layout;
 using Avalonia.LogicalTree;
 
 namespace Avalonia.Controls
@@ -90,9 +91,16 @@ namespace Avalonia.Controls
         /// <param name="control">The control.</param>
         public void Open(Control control)
         {
+            if (control == null)
+                throw new ArgumentNullException(nameof(control));
+            if (IsOpen)
+            {
+                return;
+            }
+
             if (_popup == null)
             {
-                _popup = new Popup()
+                _popup = new Popup
                 {
                     PlacementMode = PlacementMode.Pointer,
                     PlacementTarget = control,
@@ -107,7 +115,14 @@ namespace Avalonia.Controls
             ((ISetLogicalParent)_popup).SetParent(control);
             _popup.Child = this;
             _popup.IsOpen = true;
+
             IsOpen = true;
+
+            RaiseEvent(new RoutedEventArgs
+            {
+                RoutedEvent = MenuOpenedEvent,
+                Source = this,
+            });
         }
 
         /// <summary>
@@ -115,13 +130,15 @@ namespace Avalonia.Controls
         /// </summary>
         public override void Close()
         {
+            if (!IsOpen)
+            {
+                return;
+            }
+
             if (_popup != null && _popup.IsVisible)
             {
                 _popup.IsOpen = false;
             }
-
-            SelectedIndex = -1;
-            IsOpen = false;
         }
 
         protected override IItemContainerGenerator CreateItemContainerGenerator()
@@ -129,6 +146,18 @@ namespace Avalonia.Controls
             return new MenuItemContainerGenerator(this);
         }
 
+        private void CloseCore()
+        {
+            SelectedIndex = -1;
+            IsOpen = false;
+
+            RaiseEvent(new RoutedEventArgs
+            {
+                RoutedEvent = MenuClosedEvent,
+                Source = this,
+            });
+        }
+
         private void PopupOpened(object sender, EventArgs e)
         {
             Focus();
@@ -145,8 +174,7 @@ namespace Avalonia.Controls
                     i.IsSubMenuOpen = false;
                 }
 
-                contextMenu.IsOpen = false;
-                contextMenu.SelectedIndex = -1;
+                contextMenu.CloseCore();
             }
         }
 

+ 1 - 20
src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs

@@ -8,7 +8,7 @@ using JetBrains.Annotations;
 
 namespace Avalonia.Controls.Embedding
 {
-    public class EmbeddableControlRoot : TopLevel, IStyleable, IFocusScope, INameScope, IDisposable
+    public class EmbeddableControlRoot : TopLevel, IStyleable, IFocusScope, IDisposable
     {
         public EmbeddableControlRoot(IEmbeddableWindowImpl impl) : base(impl)
         {
@@ -51,25 +51,6 @@ namespace Avalonia.Controls.Embedding
             return rv;
         }
 
-        private readonly NameScope _nameScope = new NameScope();
-        public event EventHandler<NameScopeEventArgs> Registered
-        {
-            add { _nameScope.Registered += value; }
-            remove { _nameScope.Registered -= value; }
-        }
-
-        public event EventHandler<NameScopeEventArgs> Unregistered
-        {
-            add { _nameScope.Unregistered += value; }
-            remove { _nameScope.Unregistered -= value; }
-        }
-
-        public void Register(string name, object element) => _nameScope.Register(name, element);
-
-        public object Find(string name) => _nameScope.Find(name);
-
-        public void Unregister(string name) => _nameScope.Unregister(name);
-
         Type IStyleable.StyleKey => typeof(EmbeddableControlRoot);
         public void Dispose() => PlatformImpl?.Dispose();
     }

+ 0 - 19
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs

@@ -30,25 +30,6 @@ namespace Avalonia.Controls.Embedding.Offscreen
                 init.EndInit();
             }
         }
-        
-        private readonly NameScope _nameScope = new NameScope();
-        public event EventHandler<NameScopeEventArgs> Registered
-        {
-            add { _nameScope.Registered += value; }
-            remove { _nameScope.Registered -= value; }
-        }
-
-        public event EventHandler<NameScopeEventArgs> Unregistered
-        {
-            add { _nameScope.Unregistered += value; }
-            remove { _nameScope.Unregistered -= value; }
-        }
-
-        public void Register(string name, object element) => _nameScope.Register(name, element);
-
-        public object Find(string name) => _nameScope.Find(name);
-
-        public void Unregister(string name) => _nameScope.Unregister(name);
 
         Type IStyleable.StyleKey => typeof(EmbeddableControlRoot);
         public void Dispose()

+ 1 - 0
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

@@ -61,5 +61,6 @@ namespace Avalonia.Controls.Embedding.Offscreen
 
         public Action Closed { get; set; }
         public abstract IMouseDevice MouseDevice { get; }
+        public IPopupImpl CreatePopup() => null;
     }
 }

+ 2 - 10
src/Avalonia.Controls/Generators/IItemContainerGenerator.cs

@@ -49,12 +49,8 @@ namespace Avalonia.Controls.Generators
         /// The index of the item of data in the control's items.
         /// </param>
         /// <param name="item">The item.</param>
-        /// <param name="selector">An optional member selector.</param>
         /// <returns>The created controls.</returns>
-        ItemContainerInfo Materialize(
-            int index,
-            object item,
-            IMemberSelector selector);
+        ItemContainerInfo Materialize(int index, object item);
 
         /// <summary>
         /// Removes a set of created containers.
@@ -84,11 +80,7 @@ namespace Avalonia.Controls.Generators
         /// <returns>The removed containers.</returns>
         IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count);
 
-        bool TryRecycle(
-            int oldIndex,
-            int newIndex,
-            object item,
-            IMemberSelector selector);
+        bool TryRecycle(int oldIndex, int newIndex, object item);
 
         /// <summary>
         /// Clears all created containers and returns the removed controls.

+ 3 - 14
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@@ -54,13 +54,9 @@ namespace Avalonia.Controls.Generators
         public virtual Type ContainerType => null;
 
         /// <inheritdoc/>
-        public ItemContainerInfo Materialize(
-            int index,
-            object item,
-            IMemberSelector selector)
+        public ItemContainerInfo Materialize(int index, object item)
         {
-            var i = selector != null ? selector.Select(item) : item;
-            var container = new ItemContainerInfo(CreateContainer(i), item, index);
+            var container = new ItemContainerInfo(CreateContainer(item), item, index);
 
             _containers.Add(container.Index, container);
             Materialized?.Invoke(this, new ItemContainerEventArgs(container));
@@ -138,14 +134,7 @@ namespace Avalonia.Controls.Generators
         }
 
         /// <inheritdoc/>
-        public virtual bool TryRecycle(
-            int oldIndex,
-            int newIndex,
-            object item,
-            IMemberSelector selector)
-        {
-            return false;
-        }
+        public virtual bool TryRecycle(int oldIndex, int newIndex, object item) => false;
 
         /// <inheritdoc/>
         public virtual IEnumerable<ItemContainerInfo> Clear()

+ 4 - 10
src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs

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

+ 10 - 5
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

@@ -92,7 +92,6 @@ namespace Avalonia.Controls.Generators
                     result.DataContext = item;
                 }
 
-                NameScope.SetNameScope((Control)(object)result, new NameScope());
                 Index.Add(item, result);
 
                 return result;
@@ -118,16 +117,22 @@ namespace Avalonia.Controls.Generators
             return base.RemoveRange(startingIndex, count);
         }
 
-        public override bool TryRecycle(int oldIndex, int newIndex, object item, IMemberSelector selector)
+        public override bool TryRecycle(int oldIndex, int newIndex, object item) => false;
+
+        class WrapperTreeDataTemplate : ITreeDataTemplate
         {
-            return false;
+            private readonly IDataTemplate _inner;
+            public WrapperTreeDataTemplate(IDataTemplate inner) => _inner = inner;
+            public IControl Build(object param) => _inner.Build(param);
+            public bool SupportsRecycling => _inner.SupportsRecycling;
+            public bool Match(object data) => _inner.Match(data);
+            public InstancedBinding ItemsSelector(object item) => null;
         }
 
         private ITreeDataTemplate GetTreeDataTemplate(object item, IDataTemplate primary)
         {
             var template = Owner.FindDataTemplate(item, primary) ?? FuncDataTemplate.Default;
-            var treeTemplate = template as ITreeDataTemplate ??
-                new FuncTreeDataTemplate(typeof(object), template.Build, x => null);
+            var treeTemplate = template as ITreeDataTemplate ?? new WrapperTreeDataTemplate(template);
             return treeTemplate;
         }
     }

+ 1 - 0
src/Avalonia.Controls/GridSplitter.cs

@@ -6,6 +6,7 @@ using System.Collections.Generic;
 using System.Linq;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
+using Avalonia.Layout;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Controls

+ 9 - 0
src/Avalonia.Controls/IScrollAnchorProvider.cs

@@ -0,0 +1,9 @@
+namespace Avalonia.Controls
+{
+    public interface IScrollAnchorProvider
+    {
+        IControl CurrentAnchor { get; }
+        void RegisterAnchorCandidate(IControl element);
+        void UnregisterAnchorCandidate(IControl element);
+    }
+}

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

@@ -96,7 +96,7 @@ namespace Avalonia.Controls
                 }
             }
 
-            return result.Constrain(availableSize);
+            return result;
         }
 
         /// <inheritdoc/>

+ 0 - 15
src/Avalonia.Controls/ItemsControl.cs

@@ -54,12 +54,6 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty =
             AvaloniaProperty.Register<ItemsControl, IDataTemplate>(nameof(ItemTemplate));
 
-        /// <summary>
-        /// Defines the <see cref="MemberSelector"/> property.
-        /// </summary>
-        public static readonly StyledProperty<IMemberSelector> MemberSelectorProperty =
-            AvaloniaProperty.Register<ItemsControl, IMemberSelector>(nameof(MemberSelector));
-
         private IEnumerable _items = new AvaloniaList<object>();
         private int _itemCount;
         private IItemContainerGenerator _itemContainerGenerator;
@@ -144,15 +138,6 @@ namespace Avalonia.Controls
             set { SetValue(ItemTemplateProperty, value); }
         }
 
-        /// <summary>
-        /// Selects a member from <see cref="Items"/> to use as the list item.
-        /// </summary>
-        public IMemberSelector MemberSelector
-        {
-            get { return GetValue(MemberSelectorProperty); }
-            set { SetValue(MemberSelectorProperty, value); }
-        }
-
         /// <summary>
         /// Gets the items presenter control.
         /// </summary>

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

@@ -45,7 +45,7 @@ namespace Avalonia.Controls
         }
 
         /// <summary>
-        /// Utilize the <see cref="RenderTransformProperty"/> for layout transforms.
+        /// Utilize the <see cref="Visual.RenderTransformProperty"/> for layout transforms.
         /// </summary>
         public bool UseRenderTransform
         {

+ 19 - 2
src/Avalonia.Controls/ListBox.cs

@@ -68,7 +68,13 @@ namespace Avalonia.Controls
         /// <inheritdoc/>
         public new IList SelectedItems => base.SelectedItems;
 
-        /// <inheritdoc/>
+        /// <summary>
+        /// Gets or sets the selection mode.
+        /// </summary>
+        /// <remarks>
+        /// Note that the selection mode only applies to selections made via user interaction.
+        /// Multiple selections can be made programatically regardless of the value of this property.
+        /// </remarks>
         public new SelectionMode SelectionMode
         {
             get { return base.SelectionMode; }
@@ -84,6 +90,16 @@ namespace Avalonia.Controls
             set { SetValue(VirtualizationModeProperty, value); }
         }
 
+        /// <summary>
+        /// Selects all items in the <see cref="ListBox"/>.
+        /// </summary>
+        public new void SelectAll() => base.SelectAll();
+
+        /// <summary>
+        /// Deselects all items in the <see cref="ListBox"/>.
+        /// </summary>
+        public new void UnselectAll() => base.UnselectAll();
+
         /// <inheritdoc/>
         protected override IItemContainerGenerator CreateItemContainerGenerator()
         {
@@ -118,7 +134,8 @@ namespace Avalonia.Controls
                     e.Source,
                     true,
                     (e.InputModifiers & InputModifiers.Shift) != 0,
-                    (e.InputModifiers & InputModifiers.Control) != 0);
+                    (e.InputModifiers & InputModifiers.Control) != 0,
+                    e.MouseButton == MouseButton.Right);
             }
         }
 

+ 26 - 21
src/Avalonia.Controls/Menu.cs

@@ -5,6 +5,7 @@ using Avalonia.Controls.Platform;
 using Avalonia.Controls.Templates;
 using Avalonia.Input;
 using Avalonia.Interactivity;
+using Avalonia.Layout;
 
 namespace Avalonia.Controls
 {
@@ -40,37 +41,41 @@ namespace Avalonia.Controls
         /// <inheritdoc/>
         public override void Close()
         {
-            if (IsOpen)
+            if (!IsOpen)
             {
-                foreach (var i in ((IMenu)this).SubItems)
-                {
-                    i.Close();
-                }
-
-                IsOpen = false;
-                SelectedIndex = -1;
+                return;
+            }
 
-                RaiseEvent(new RoutedEventArgs
-                {
-                    RoutedEvent = MenuClosedEvent,
-                    Source = this,
-                });
+            foreach (var i in ((IMenu)this).SubItems)
+            {
+                i.Close();
             }
+
+            IsOpen = false;
+            SelectedIndex = -1;
+
+            RaiseEvent(new RoutedEventArgs
+            {
+                RoutedEvent = MenuClosedEvent,
+                Source = this,
+            });
         }
 
         /// <inheritdoc/>
         public override void Open()
         {
-            if (!IsOpen)
+            if (IsOpen)
             {
-                IsOpen = true;
-
-                RaiseEvent(new RoutedEventArgs
-                {
-                    RoutedEvent = MenuOpenedEvent,
-                    Source = this,
-                });
+                return;
             }
+
+            IsOpen = true;
+
+            RaiseEvent(new RoutedEventArgs
+            {
+                RoutedEvent = MenuOpenedEvent,
+                Source = this,
+            });
         }
 
         /// <inheritdoc/>

+ 2 - 3
src/Avalonia.Controls/MenuBase.cs

@@ -7,7 +7,6 @@ using System.Linq;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Platform;
 using Avalonia.Controls.Primitives;
-using Avalonia.Controls.Templates;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.LogicalTree;
@@ -31,13 +30,13 @@ namespace Avalonia.Controls
         /// Defines the <see cref="MenuOpened"/> event.
         /// </summary>
         public static readonly RoutedEvent<RoutedEventArgs> MenuOpenedEvent =
-            RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(MenuOpened), RoutingStrategies.Bubble);
+            RoutedEvent.Register<MenuBase, RoutedEventArgs>(nameof(MenuOpened), RoutingStrategies.Bubble);
 
         /// <summary>
         /// Defines the <see cref="MenuClosed"/> event.
         /// </summary>
         public static readonly RoutedEvent<RoutedEventArgs> MenuClosedEvent =
-            RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(MenuClosed), RoutingStrategies.Bubble);
+            RoutedEvent.Register<MenuBase, RoutedEventArgs>(nameof(MenuClosed), RoutingStrategies.Bubble);
 
         private bool _isOpen;
 

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

@@ -224,7 +224,7 @@ namespace Avalonia.Controls
         public bool IsTopLevel => Parent is Menu;
 
         /// <inheritdoc/>
-        bool IMenuItem.IsPointerOverSubMenu => _popup.PopupRoot?.IsPointerOver ?? false;
+        bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false; 
 
         /// <inheritdoc/>
         IMenuElement IMenuItem.Parent => Parent as IMenuElement;

+ 1 - 0
src/Avalonia.Controls/Mixins/ContentControlMixin.cs

@@ -150,6 +150,7 @@ namespace Avalonia.Controls.Mixins
                 if (oldValue is IControl child)
                 {
                     logicalChildren.Remove(child);
+                    ((ISetInheritanceParent)child).SetParent(child.Parent);
                 }
 
                 child = newValue as IControl;

+ 1 - 1
src/Avalonia.Controls/Notifications/WindowNotificationManager.cs

@@ -150,7 +150,7 @@ namespace Avalonia.Controls.Notifications
         private void Install(Window host)
         {
             var adornerLayer = host.GetVisualDescendants()
-                .OfType<AdornerDecorator>()
+                .OfType<VisualLayerManager>()
                 .FirstOrDefault()
                 ?.AdornerLayer;
 

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

@@ -112,7 +112,7 @@ namespace Avalonia.Controls
                 case NotifyCollectionChangedAction.Add:
                     controls = e.NewItems.OfType<Control>().ToList();
                     LogicalChildren.InsertRange(e.NewStartingIndex, controls);
-                    VisualChildren.AddRange(e.NewItems.OfType<Visual>());
+                    VisualChildren.InsertRange(e.NewStartingIndex, e.NewItems.OfType<Visual>());
                     break;
 
                 case NotifyCollectionChangedAction.Move:

+ 17 - 2
src/Avalonia.Controls/PlacementMode.cs

@@ -23,6 +23,21 @@ namespace Avalonia.Controls
         /// <summary>
         /// The popup is placed at the top right of its target.
         /// </summary>
-        Right
+        Right,
+        
+        /// <summary>
+        /// The popup is placed at the top left of its target.
+        /// </summary>
+        Left,
+        
+        /// <summary>
+        /// The popup is placed at the top left of its target.
+        /// </summary>
+        Top,
+        
+        /// <summary>
+        /// The popup is placed according to anchor and gravity rules
+        /// </summary>
+        AnchorAndGravity
     }
-}
+}

+ 1 - 1
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@@ -396,7 +396,7 @@ namespace Avalonia.Controls.Platform
 
         protected internal virtual void WindowDeactivated(object sender, EventArgs e)
         {
-            Menu.Close();
+            Menu?.Close();
         }
 
         protected void Click(IMenuItem item)

+ 3 - 1
src/Avalonia.Controls/Platform/IPopupImpl.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 Avalonia.Controls.Primitives.PopupPositioning;
+
 namespace Avalonia.Platform
 {
     /// <summary>
@@ -8,6 +10,6 @@ namespace Avalonia.Platform
     /// </summary>
     public interface IPopupImpl : IWindowBaseImpl
     {
-
+        IPopupPositioner PopupPositioner { get; }
     }
 }

+ 2 - 0
src/Avalonia.Controls/Platform/ITopLevelImpl.cs

@@ -107,5 +107,7 @@ namespace Avalonia.Platform
         /// </summary>
         [CanBeNull]
         IMouseDevice MouseDevice { get; }
+
+        IPopupImpl CreatePopup();
     }
 }

+ 1 - 23
src/Avalonia.Controls/Platform/IWindowBaseImpl.cs

@@ -15,21 +15,10 @@ namespace Avalonia.Platform
         /// </summary>
         void Hide();
 
-        /// <summary>
-        /// Starts moving a window with left button being held. Should be called from left mouse button press event handler.
-        /// </summary>
-        void BeginMoveDrag();
-
-        /// <summary>
-        /// Starts resizing a window. This function is used if an application has window resizing controls. 
-        /// Should be called from left mouse button press event handler
-        /// </summary>
-        void BeginResizeDrag(WindowEdge edge);
-
         /// <summary>
         /// Gets the position of the window in device pixels.
         /// </summary>
-        PixelPoint Position { get; set; }
+        PixelPoint Position { get; }
         
         /// <summary>
         /// Gets or sets a method called when the window's position changes.
@@ -61,17 +50,6 @@ namespace Avalonia.Platform
         /// </summary>
         Size MaxClientSize { get; }
 
-        /// <summary>
-        /// Sets the client size of the top level.
-        /// </summary>
-        void Resize(Size clientSize);
-
-        /// <summary>
-        /// Minimum width of the window.
-        /// </summary>
-        /// 
-        void SetMinMaxSize(Size minSize, Size maxSize);
-
         /// <summary>
         /// Sets whether this window appears on top of all other windows
         /// </summary>

+ 27 - 0
src/Avalonia.Controls/Platform/IWindowImpl.cs

@@ -57,5 +57,32 @@ namespace Avalonia.Platform
         /// Return true to prevent the underlying implementation from closing.
         /// </summary>
         Func<bool> Closing { get; set; }
+        
+        /// <summary>
+        /// Starts moving a window with left button being held. Should be called from left mouse button press event handler.
+        /// </summary>
+        void BeginMoveDrag();
+
+        /// <summary>
+        /// Starts resizing a window. This function is used if an application has window resizing controls. 
+        /// Should be called from left mouse button press event handler
+        /// </summary>
+        void BeginResizeDrag(WindowEdge edge);
+        
+        /// <summary>
+        /// Sets the client size of the top level.
+        /// </summary>
+        void Resize(Size clientSize);
+        
+        /// <summary>
+        /// Sets the client size of the top level.
+        /// </summary>
+        void Move(PixelPoint point);
+        
+        /// <summary>
+        /// Minimum width of the window.
+        /// </summary>
+        /// 
+        void SetMinMaxSize(Size minSize, Size maxSize);
     }
 }

+ 0 - 1
src/Avalonia.Controls/Platform/IWindowingPlatform.cs

@@ -4,6 +4,5 @@ namespace Avalonia.Platform
     {
         IWindowImpl CreateWindow();
         IEmbeddableWindowImpl CreateEmbeddableWindow();
-        IPopupImpl CreatePopup();
     }
 }

+ 2 - 1
src/Avalonia.Controls/Platform/InProcessDragSource.cs

@@ -33,9 +33,10 @@ namespace Avalonia.Platform
             _dragDrop = AvaloniaLocator.Current.GetService<IDragDropDevice>();
         }
 
-        public async Task<DragDropEffects> DoDragDrop(IDataObject data, DragDropEffects allowedEffects)
+        public async Task<DragDropEffects> DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects)
         {
             Dispatcher.UIThread.VerifyAccess();
+            triggerEvent.Pointer.Capture(null);
             if (_draggedData == null)
             {
                 _draggedData = data;

+ 30 - 55
src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs

@@ -9,94 +9,69 @@ using Avalonia.Threading;
 
 namespace Avalonia.Controls.Platform
 {
-    public class InternalPlatformThreadingInterface : IPlatformThreadingInterface, IRenderTimer
+    public class InternalPlatformThreadingInterface : IPlatformThreadingInterface
     {
         public InternalPlatformThreadingInterface()
         {
             TlsCurrentThreadIsLoopThread = true;
-            StartTimer(
-                DispatcherPriority.Render,
-                new TimeSpan(0, 0, 0, 0, 66),
-                () => Tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount)));
         }
 
         private readonly AutoResetEvent _signaled = new AutoResetEvent(false);
-        private readonly AutoResetEvent _queued = new AutoResetEvent(false);
 
-        private readonly Queue<Action> _actions = new Queue<Action>();
 
         public void RunLoop(CancellationToken cancellationToken)
         {
-            var handles = new[] {_signaled, _queued};
             while (true)
             {
-                if (0 == WaitHandle.WaitAny(handles))
-                    Signaled?.Invoke(null);
-                else
-                {
-                    while (true)
-                    {
-                        Action item;
-                        lock (_actions)
-                            if (_actions.Count == 0)
-                                break;
-                            else
-                                item = _actions.Dequeue();
-                        item();
-                    }
-                }
+                Signaled?.Invoke(null);
+                _signaled.WaitOne();
             }
         }
 
-        public void Send(Action cb)
-        {
-            lock (_actions)
-            {
-                _actions.Enqueue(cb);
-                _queued.Set();
-            }
-        }
 
-        class WatTimer : IDisposable
+        class TimerImpl : IDisposable
         {
-            private readonly IDisposable _timer;
+            private readonly DispatcherPriority _priority;
+            private readonly TimeSpan _interval;
+            private readonly Action _tick;
+            private Timer _timer;
             private GCHandle _handle;
 
-            public WatTimer(IDisposable timer)
+            public TimerImpl(DispatcherPriority priority, TimeSpan interval, Action tick)
             {
-                _timer = timer;
+                _priority = priority;
+                _interval = interval;
+                _tick = tick;
+                _timer = new Timer(OnTimer, null, interval, TimeSpan.FromMilliseconds(-1));
                 _handle = GCHandle.Alloc(_timer);
             }
 
+            private void OnTimer(object state)
+            {
+                if (_timer == null)
+                    return;
+                Dispatcher.UIThread.Post(() =>
+                {
+                    
+                    if (_timer == null)
+                        return;
+                    _tick();
+                    _timer?.Change(_interval, TimeSpan.FromMilliseconds(-1));
+                });
+            }
+
+
             public void Dispose()
             {
                 _handle.Free();
                 _timer.Dispose();
+                _timer = null;
             }
         }
 
         public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
         {
-            return new WatTimer(new System.Threading.Timer(delegate
-            {
-                var tcs = new TaskCompletionSource<int>();
-                Send(() =>
-                {
-                    try
-                    {
-                        tick();
-                    }
-                    finally
-                    {
-                        tcs.SetResult(0);
-                    }
-                });
-
-
-                tcs.Task.Wait();
-            }, null, TimeSpan.Zero, interval));
-
-
+            return new TimerImpl(priority, interval, tick);
         }
 
         public void Signal(DispatcherPriority prio)

+ 0 - 5
src/Avalonia.Controls/Platform/PlatformManager.cs

@@ -41,10 +41,5 @@ namespace Avalonia.Controls.Platform
                 throw new Exception("Could not CreateEmbeddableWindow(): IWindowingPlatform is not registered.");
             return platform.CreateEmbeddableWindow();
         }
-
-        public static IPopupImpl CreatePopup()
-        {
-            return AvaloniaLocator.Current.GetService<IWindowingPlatform>().CreatePopup();
-        }
     }
 }

+ 1 - 1
src/Avalonia.Controls/Presenters/CarouselPresenter.cs

@@ -213,7 +213,7 @@ namespace Avalonia.Controls.Presenters
             if (container == null && IsVirtualized)
             {
                 var item = Items.Cast<object>().ElementAt(index);
-                var materialized = ItemContainerGenerator.Materialize(index, item, MemberSelector);
+                var materialized = ItemContainerGenerator.Materialize(index, item);
                 Panel.Children.Add(materialized.ContainerControl);
                 container = materialized.ContainerControl;
             }

+ 6 - 7
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@@ -237,7 +237,7 @@ namespace Avalonia.Controls.Presenters
                     // template.
                     LogicalChildren.Remove(oldChild);
                 }
-                else
+                else if (TemplatedParent != null)
                 {
                     // If we're in a ContentControl's template then invoke ChildChanging to let
                     // ContentControlMixin handle removing the logical child.
@@ -248,6 +248,10 @@ namespace Avalonia.Controls.Presenters
                         newChild,
                         BindingPriority.LocalValue));
                 }
+                else if (oldChild != null)
+                {
+                    ((ISetInheritanceParent)oldChild).SetParent(oldChild.Parent);
+                }
             }
 
             // Set the DataContext if the data isn't a control.
@@ -325,12 +329,6 @@ namespace Avalonia.Controls.Presenters
                 {
                     _dataTemplate = dataTemplate;
                     newChild = _dataTemplate.Build(content);
-
-                    // Give the new control its own name scope.
-                    if (newChild is Control controlResult)
-                    {
-                        NameScope.SetNameScope(controlResult, new NameScope());
-                    }
                 }
             }
             else
@@ -439,6 +437,7 @@ namespace Avalonia.Controls.Presenters
             {
                 VisualChildren.Remove(Child);
                 LogicalChildren.Remove(Child);
+                ((ISetInheritanceParent)Child).SetParent(Child.Parent);
                 Child = null;
                 _dataTemplate = null;
             }

+ 1 - 1
src/Avalonia.Controls/Presenters/ItemContainerSync.cs

@@ -88,7 +88,7 @@ namespace Avalonia.Controls.Presenters
 
             foreach (var item in items)
             {
-                var i = generator.Materialize(index++, item, owner.MemberSelector);
+                var i = generator.Materialize(index++, item);
 
                 if (i.ContainerControl != null)
                 {

+ 1 - 0
src/Avalonia.Controls/Presenters/ItemVirtualizer.cs

@@ -8,6 +8,7 @@ using System.Reactive.Linq;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Utils;
 using Avalonia.Input;
+using Avalonia.Layout;
 
 namespace Avalonia.Controls.Presenters
 {

+ 1 - 1
src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs

@@ -90,7 +90,7 @@ namespace Avalonia.Controls.Presenters
 
             foreach (var item in items)
             {
-                var i = generator.Materialize(index++, item, Owner.MemberSelector);
+                var i = generator.Materialize(index++, item);
 
                 if (i.ContainerControl != null)
                 {

+ 3 - 6
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@@ -314,7 +314,6 @@ namespace Avalonia.Controls.Presenters
 
             if (!panel.IsFull && Items != null && panel.IsAttachedToVisualTree)
             {
-                var memberSelector = Owner.MemberSelector;
                 var index = NextIndex;
                 var step = 1;
 
@@ -337,7 +336,7 @@ namespace Avalonia.Controls.Presenters
                         }
                     }
 
-                    var materialized = generator.Materialize(index, Items.ElementAt(index), memberSelector);
+                    var materialized = generator.Materialize(index, Items.ElementAt(index));
 
                     if (step == 1)
                     {
@@ -383,7 +382,6 @@ namespace Avalonia.Controls.Presenters
         {
             var panel = VirtualizingPanel;
             var generator = Owner.ItemContainerGenerator;
-            var selector = Owner.MemberSelector;
             var containers = generator.Containers.ToList();
             var itemIndex = FirstIndex;
 
@@ -393,7 +391,7 @@ namespace Avalonia.Controls.Presenters
 
                 if (!object.Equals(container.Item, item))
                 {
-                    if (!generator.TryRecycle(itemIndex, itemIndex, item, selector))
+                    if (!generator.TryRecycle(itemIndex, itemIndex, item))
                     {
                         throw new NotImplementedException();
                     }
@@ -420,7 +418,6 @@ namespace Avalonia.Controls.Presenters
         {
             var panel = VirtualizingPanel;
             var generator = Owner.ItemContainerGenerator;
-            var selector = Owner.MemberSelector;
 
             //validate delta it should never overflow last index or generate index < 0 
             delta = MathUtilities.Clamp(delta, -FirstIndex, ItemCount - FirstIndex - panel.Children.Count);
@@ -437,7 +434,7 @@ namespace Avalonia.Controls.Presenters
 
                 var item = Items.ElementAt(newItemIndex);
 
-                if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector))
+                if (!generator.TryRecycle(oldItemIndex, newItemIndex, item))
                 {
                     throw new NotImplementedException();
                 }

+ 0 - 15
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@@ -35,12 +35,6 @@ namespace Avalonia.Controls.Presenters
         public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty =
             ItemsControl.ItemTemplateProperty.AddOwner<ItemsPresenterBase>();
 
-        /// <summary>
-        /// Defines the <see cref="MemberSelector"/> property.
-        /// </summary>
-        public static readonly StyledProperty<IMemberSelector> MemberSelectorProperty =
-            ItemsControl.MemberSelectorProperty.AddOwner<ItemsPresenterBase>();
-
         private IEnumerable _items;
         private IDisposable _itemsSubscription;
         private bool _createdPanel;
@@ -127,15 +121,6 @@ namespace Avalonia.Controls.Presenters
             set { SetValue(ItemTemplateProperty, value); }
         }
 
-        /// <summary>
-        /// Selects a member from <see cref="Items"/> to use as the list item.
-        /// </summary>
-        public IMemberSelector MemberSelector
-        {
-            get { return GetValue(MemberSelectorProperty); }
-            set { SetValue(MemberSelectorProperty, value); }
-        }
-
         /// <summary>
         /// Gets the panel used to display the items.
         /// </summary>

+ 8 - 11
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -49,6 +49,14 @@ namespace Avalonia.Controls.Presenters
             AffectsRender<TextPresenter>(PasswordCharProperty,
                 SelectionBrushProperty, SelectionForegroundBrushProperty,
                 SelectionStartProperty, SelectionEndProperty);
+
+            Observable.Merge(
+                SelectionStartProperty.Changed,
+                SelectionEndProperty.Changed,
+                PasswordCharProperty.Changed
+            ).AddClassHandler<TextPresenter>((x,_) => x.InvalidateFormattedText());
+
+            CaretIndexProperty.Changed.AddClassHandler<TextPresenter>((x, e) => x.CaretIndexChanged((int)e.NewValue));
         }
 
         public TextPresenter()
@@ -56,17 +64,6 @@ namespace Avalonia.Controls.Presenters
             _caretTimer = new DispatcherTimer();
             _caretTimer.Interval = TimeSpan.FromMilliseconds(500);
             _caretTimer.Tick += CaretTimerTick;
-
-            Observable.Merge(
-                this.GetObservable(SelectionStartProperty),
-                this.GetObservable(SelectionEndProperty))
-                .Subscribe(_ => InvalidateFormattedText());
-
-            this.GetObservable(CaretIndexProperty)
-                .Subscribe(CaretIndexChanged);
-
-            this.GetObservable(PasswordCharProperty)
-                .Subscribe(_ => InvalidateFormattedText());
         }
 
         public int CaretIndex

+ 0 - 42
src/Avalonia.Controls/Primitives/AdornerDecorator.cs

@@ -1,42 +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 Avalonia.LogicalTree;
-
-namespace Avalonia.Controls.Primitives
-{
-    public class AdornerDecorator : Decorator
-    {
-        public AdornerDecorator()
-        {
-            AdornerLayer = new AdornerLayer();
-            ((ISetLogicalParent)AdornerLayer).SetParent(this);
-            AdornerLayer.ZIndex = int.MaxValue;
-            VisualChildren.Add(AdornerLayer);
-        }
-
-        protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
-        {
-            base.OnAttachedToLogicalTree(e);
-
-            ((ILogical)AdornerLayer).NotifyAttachedToLogicalTree(e);
-        }
-
-        public AdornerLayer AdornerLayer
-        {
-            get;
-        }
-
-        protected override Size MeasureOverride(Size availableSize)
-        {
-            AdornerLayer.Measure(availableSize);
-            return base.MeasureOverride(availableSize);
-        }
-
-        protected override Size ArrangeOverride(Size finalSize)
-        {
-            AdornerLayer.Arrange(new Rect(finalSize));
-            return base.ArrangeOverride(finalSize);
-        }
-    }
-}

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

@@ -42,7 +42,7 @@ namespace Avalonia.Controls.Primitives
         public static AdornerLayer GetAdornerLayer(IVisual visual)
         {
             return visual.GetVisualAncestors()
-                .OfType<AdornerDecorator>()
+                .OfType<VisualLayerManager>()
                 .FirstOrDefault()
                 ?.AdornerLayer;
         }

+ 26 - 0
src/Avalonia.Controls/Primitives/IPopupHost.cs

@@ -0,0 +1,26 @@
+using System;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives.PopupPositioning;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls.Primitives
+{
+    public interface IPopupHost : IDisposable
+    {
+        void SetChild(IControl control);
+        IContentPresenter Presenter { get; }
+        IVisual HostedVisualTreeRoot { get; }
+
+        event EventHandler<TemplateAppliedEventArgs> TemplateApplied;
+
+        void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
+            PopupPositioningEdge anchor = PopupPositioningEdge.None,
+            PopupPositioningEdge gravity = PopupPositioningEdge.None);
+        void Show();
+        void Hide();
+        IDisposable BindConstraints(AvaloniaObject popup, StyledProperty<double> widthProperty,
+            StyledProperty<double> minWidthProperty, StyledProperty<double> maxWidthProperty,
+            StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty,
+            StyledProperty<double> maxHeightProperty, StyledProperty<bool> topmostProperty);
+    }
+}

+ 38 - 0
src/Avalonia.Controls/Primitives/OverlayLayer.cs

@@ -0,0 +1,38 @@
+using System.Linq;
+using Avalonia.Rendering;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls.Primitives
+{
+    public class OverlayLayer : Canvas, ICustomSimpleHitTest
+    {
+        public Size AvailableSize { get; private set; }
+        public static OverlayLayer GetOverlayLayer(IVisual visual)
+        {
+            foreach(var v in visual.GetVisualAncestors())
+                if(v is VisualLayerManager vlm)
+                    if (vlm.OverlayLayer != null)
+                        return vlm.OverlayLayer;
+            if (visual is TopLevel tl)
+            {
+                var layers = tl.GetVisualDescendants().OfType<VisualLayerManager>().FirstOrDefault();
+                return layers?.OverlayLayer;
+            }
+
+            return null;
+        }
+        
+        public bool HitTest(Point point)
+        {
+            return Children.Any(ctrl => ctrl.TransformedBounds?.Contains(point) == true);
+        }
+        
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            // We are saving it here since child controls might need to know the entire size of the overlay
+            // and Bounds won't be updated in time
+            AvailableSize = finalSize;
+            return base.ArrangeOverride(finalSize);
+        }
+    }
+}

+ 149 - 0
src/Avalonia.Controls/Primitives/OverlayPopupHost.cs

@@ -0,0 +1,149 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive.Disposables;
+using Avalonia.Controls.Primitives.PopupPositioning;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls.Primitives
+{
+    public class OverlayPopupHost : ContentControl, IPopupHost, IInteractive, IManagedPopupPositionerPopup
+    {
+        private readonly OverlayLayer _overlayLayer;
+        private PopupPositionerParameters _positionerParameters = new PopupPositionerParameters();
+        private ManagedPopupPositioner _positioner;
+        private Point _lastRequestedPosition;
+        private bool _shown;
+
+        public OverlayPopupHost(OverlayLayer overlayLayer)
+        {
+            _overlayLayer = overlayLayer;
+            _positioner = new ManagedPopupPositioner(this);
+        }
+
+        public void SetChild(IControl control)
+        {
+            Content = control;
+        }
+
+        public IVisual HostedVisualTreeRoot => null;
+        
+        /// <inheritdoc/>
+        IInteractive IInteractive.InteractiveParent => Parent;
+
+        public void Dispose() => Hide();
+
+
+        public void Show()
+        {
+            _overlayLayer.Children.Add(this);
+            _shown = true;
+        }
+
+        public void Hide()
+        {
+            _overlayLayer.Children.Remove(this);
+            _shown = false;
+        }
+
+        public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty<double> widthProperty, StyledProperty<double> minWidthProperty,
+            StyledProperty<double> maxWidthProperty, StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty,
+            StyledProperty<double> maxHeightProperty, StyledProperty<bool> topmostProperty)
+        {
+            // Topmost property is not supported
+            var bindings = new List<IDisposable>();
+
+            void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to]));
+            Bind(WidthProperty, widthProperty);
+            Bind(MinWidthProperty, minWidthProperty);
+            Bind(MaxWidthProperty, maxWidthProperty);
+            Bind(HeightProperty, heightProperty);
+            Bind(MinHeightProperty, minHeightProperty);
+            Bind(MaxHeightProperty, maxHeightProperty);
+            
+            return Disposable.Create(() =>
+            {
+                foreach (var x in bindings)
+                    x.Dispose();
+            });
+        }
+
+        public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
+            PopupPositioningEdge anchor = PopupPositioningEdge.None, PopupPositioningEdge gravity = PopupPositioningEdge.None)
+        {
+            _positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot(), target, placement, offset, anchor,
+                gravity);
+            UpdatePosition();
+        }
+
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            if (_positionerParameters.Size != finalSize)
+            {
+                _positionerParameters.Size = finalSize;
+                UpdatePosition();
+            }
+            return base.ArrangeOverride(finalSize);
+        }
+
+
+        private void UpdatePosition()
+        {
+            // Don't bother the positioner with layout system artifacts
+            if (_positionerParameters.Size.Width == 0 || _positionerParameters.Size.Height == 0)
+                return;
+            if (_shown)
+            {
+                _positioner.Update(_positionerParameters);
+            }
+        }
+
+        IReadOnlyList<ManagedPopupPositionerScreenInfo> IManagedPopupPositionerPopup.Screens
+        {
+            get
+            {
+                var rc = new Rect(default, _overlayLayer.AvailableSize);
+                return new[] {new ManagedPopupPositionerScreenInfo(rc, rc)};
+            }
+        }
+
+        Rect IManagedPopupPositionerPopup.ParentClientAreaScreenGeometry =>
+            new Rect(default, _overlayLayer.Bounds.Size);
+
+        void IManagedPopupPositionerPopup.MoveAndResize(Point devicePoint, Size virtualSize)
+        {
+            _lastRequestedPosition = devicePoint;
+            Dispatcher.UIThread.Post(() =>
+            {
+                OverlayLayer.SetLeft(this, _lastRequestedPosition.X);
+                OverlayLayer.SetTop(this, _lastRequestedPosition.Y);
+            }, DispatcherPriority.Layout);
+        }
+
+        Point IManagedPopupPositionerPopup.TranslatePoint(Point pt) => pt;
+
+        Size IManagedPopupPositionerPopup.TranslateSize(Size size) => size;
+        
+        public static IPopupHost CreatePopupHost(IVisual target, IAvaloniaDependencyResolver dependencyResolver)
+        {
+            var platform = (target.GetVisualRoot() as TopLevel)?.PlatformImpl?.CreatePopup();
+            if (platform != null)
+                return new PopupRoot((TopLevel)target.GetVisualRoot(), platform, dependencyResolver);
+            
+            var overlayLayer = OverlayLayer.GetOverlayLayer(target);
+            if (overlayLayer == null)
+                throw new InvalidOperationException(
+                    "Unable to create IPopupImpl and no overlay layer is found for the target control");
+
+
+            return new OverlayPopupHost(overlayLayer);
+        }
+
+        public override void Render(DrawingContext context)
+        {
+            context.FillRectangle(Brushes.White, new Rect(default, Bounds.Size));
+        }
+    }
+}

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