Browse Source

Merge branch 'master' into change-default-system-chrome-hints

Dan Walmsley 4 years ago
parent
commit
1d3219e2ff
83 changed files with 1232 additions and 429 deletions
  1. 1 1
      build/ReactiveUI.props
  2. 1 0
      native/Avalonia.Native/src/OSX/AvnString.h
  3. 15 0
      native/Avalonia.Native/src/OSX/AvnString.mm
  4. 25 3
      native/Avalonia.Native/src/OSX/app.mm
  5. 1 1
      native/Avalonia.Native/src/OSX/clipboard.mm
  6. 2 2
      native/Avalonia.Native/src/OSX/common.h
  7. 4 4
      native/Avalonia.Native/src/OSX/main.mm
  8. 5 4
      native/Avalonia.Native/src/OSX/menu.h
  9. 30 4
      native/Avalonia.Native/src/OSX/menu.mm
  10. 11 3
      native/Avalonia.Native/src/OSX/window.mm
  11. 0 4
      nukebuild/Build.cs
  12. 1 1
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  13. 1 1
      samples/ControlCatalog.Android/Properties/AndroidManifest.xml
  14. 2 2
      samples/ControlCatalog/MainWindow.xaml
  15. 0 47
      src/Android/Avalonia.Android/ActivityTracker.cs
  16. 7 25
      src/Android/Avalonia.Android/AndroidPlatform.cs
  17. 8 7
      src/Android/Avalonia.Android/AndroidThreadingInterface.cs
  18. 1 1
      src/Android/Avalonia.Android/Avalonia.Android.csproj
  19. 3 8
      src/Android/Avalonia.Android/AvaloniaActivity.cs
  20. 34 1
      src/Android/Avalonia.Android/AvaloniaView.cs
  21. 101 0
      src/Android/Avalonia.Android/ChoreographerTimer.cs
  22. 13 4
      src/Android/Avalonia.Android/CursorFactory.cs
  23. 2 4
      src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
  24. 10 3
      src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
  25. 0 14
      src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs
  26. 4 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
  27. 1 1
      src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs
  28. 2 0
      src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
  29. 18 30
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  30. 22 69
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs
  31. 0 11
      src/Android/Avalonia.Android/app.config
  32. 2 2
      src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
  33. 1 1
      src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
  34. 24 4
      src/Avalonia.Animation/Animatable.cs
  35. 6 3
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  36. 8 1
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  37. 1 1
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  38. 35 14
      src/Avalonia.Base/ValueStore.cs
  39. 12 0
      src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs
  40. 2 2
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  41. 35 21
      src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs
  42. 3 1
      src/Avalonia.Controls/ApiCompatBaseline.txt
  43. 9 2
      src/Avalonia.Controls/Application.cs
  44. 14 0
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  45. 5 3
      src/Avalonia.Controls/AutoCompleteBox.cs
  46. 1 0
      src/Avalonia.Controls/Button.cs
  47. 55 22
      src/Avalonia.Controls/DefinitionBase.cs
  48. 2 0
      src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs
  49. 32 1
      src/Avalonia.Controls/NativeMenu.cs
  50. 16 0
      src/Avalonia.Controls/NativeMenuItemSeparator.cs
  51. 0 10
      src/Avalonia.Controls/NativeMenuItemSeperator.cs
  52. 16 2
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  53. 7 0
      src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs
  54. 18 4
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  55. 3 1
      src/Avalonia.Controls/Slider.cs
  56. 29 14
      src/Avalonia.Controls/TextBox.cs
  57. 14 0
      src/Avalonia.Controls/UrlOpenedEventArgs.cs
  58. 1 1
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  59. 1 1
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  60. 14 0
      src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs
  61. 4 3
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  62. 24 2
      src/Avalonia.Native/IAvnMenu.cs
  63. 10 5
      src/Avalonia.Native/avn.idl
  64. 11 7
      src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
  65. 1 1
      src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml
  66. 3 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs
  67. 1 1
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  68. 1 1
      src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
  69. 1 2
      src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs
  70. 1 1
      src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs
  71. 6 2
      src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs
  72. 6 2
      src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
  73. 14 0
      src/Windows/Avalonia.Win32/WindowImpl.cs
  74. 31 0
      tests/Avalonia.Animation.UnitTests/AnimatableTests.cs
  75. 122 0
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs
  76. 2 2
      tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs
  77. 32 0
      tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
  78. 46 18
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  79. 29 0
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  80. 26 0
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  81. 95 0
      tests/Avalonia.Controls.UnitTests/NumericUpDownTests.cs
  82. 50 12
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  83. 25 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs

+ 1 - 1
build/ReactiveUI.props

@@ -1,5 +1,5 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
-    <PackageReference Include="ReactiveUI" Version="12.1.1" />
+    <PackageReference Include="ReactiveUI" Version="13.2.10" />
   </ItemGroup>
 </Project>

+ 1 - 0
native/Avalonia.Native/src/OSX/AvnString.h

@@ -11,6 +11,7 @@
 
 extern IAvnString* CreateAvnString(NSString* string);
 extern IAvnStringArray* CreateAvnStringArray(NSArray<NSString*>* array);
+extern IAvnStringArray* CreateAvnStringArray(NSArray<NSURL*>* array);
 extern IAvnStringArray* CreateAvnStringArray(NSString* string);
 extern IAvnString* CreateByteArray(void* data, int len);
 #endif /* AvnString_h */

+ 15 - 0
native/Avalonia.Native/src/OSX/AvnString.mm

@@ -85,6 +85,16 @@ public:
         }
     }
     
+    AvnStringArrayImpl(NSArray<NSURL*>* array)
+    {
+        for(int c = 0; c < [array count]; c++)
+        {
+            ComPtr<IAvnString> s;
+            *s.getPPV() = new AvnStringImpl([array objectAtIndex:c].absoluteString);
+            _list.push_back(s);
+        }
+    }
+    
     AvnStringArrayImpl(NSString* string)
     {
         ComPtr<IAvnString> s;
@@ -117,6 +127,11 @@ IAvnStringArray* CreateAvnStringArray(NSArray<NSString*> * array)
     return new AvnStringArrayImpl(array);
 }
 
+IAvnStringArray* CreateAvnStringArray(NSArray<NSURL*> * array)
+{
+    return new AvnStringArrayImpl(array);
+}
+
 IAvnStringArray* CreateAvnStringArray(NSString* string)
 {
     return new AvnStringArrayImpl(string);

+ 25 - 3
native/Avalonia.Native/src/OSX/app.mm

@@ -1,10 +1,20 @@
 #include "common.h"
+#include "AvnString.h"
 @interface AvnAppDelegate : NSObject<NSApplicationDelegate>
+-(AvnAppDelegate* _Nonnull) initWithEvents: (IAvnApplicationEvents* _Nonnull) events;
 @end
 
 NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular;
 
 @implementation AvnAppDelegate
+ComPtr<IAvnApplicationEvents> _events;
+
+- (AvnAppDelegate *)initWithEvents:(IAvnApplicationEvents *)events
+{
+    _events = events;
+    return self;
+}
+
 - (void)applicationWillFinishLaunching:(NSNotification *)notification
 {
     if([[NSApplication sharedApplication] activationPolicy] != AvnDesiredActivationPolicy)
@@ -27,11 +37,23 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati
     [[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
 }
 
+- (void)application:(NSApplication *)sender openFiles:(NSArray<NSString *> *)filenames
+{
+    auto array = CreateAvnStringArray(filenames);
+    
+    _events->FilesOpened(array);
+}
+
+- (void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls
+{
+    auto array = CreateAvnStringArray(urls);
+    
+    _events->FilesOpened(array);
+}
 @end
 
 @interface AvnApplication : NSApplication
 
-
 @end
 
 @implementation AvnApplication
@@ -63,9 +85,9 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati
 
 @end
 
-extern void InitializeAvnApp()
+extern void InitializeAvnApp(IAvnApplicationEvents* events)
 {
     NSApplication* app = [AvnApplication sharedApplication];
-    id delegate = [AvnAppDelegate new];
+    id delegate = [[AvnAppDelegate alloc] initWithEvents:events];
     [app setDelegate:delegate];
 }

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

@@ -56,7 +56,7 @@ public:
                 return S_OK;
             }
             
-            NSArray* arr = (NSArray*)data;
+            NSArray<NSString*>* arr = (NSArray*)data;
             
             for(int c = 0; c < [arr count]; c++)
                 if(![[arr objectAtIndex:c] isKindOfClass:[NSString class]])

+ 2 - 2
native/Avalonia.Native/src/OSX/common.h

@@ -23,7 +23,7 @@ extern IAvnCursorFactory* CreateCursorFactory();
 extern IAvnGlDisplay* GetGlDisplay();
 extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events);
 extern IAvnMenuItem* CreateAppMenuItem();
-extern IAvnMenuItem* CreateAppMenuItemSeperator();
+extern IAvnMenuItem* CreateAppMenuItemSeparator();
 extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent);
 extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu);
 extern IAvnMenu* GetAppMenu ();
@@ -31,7 +31,7 @@ extern NSMenuItem* GetAppMenuItem ();
 extern void SetAutoGenerateDefaultAppMenuItems (bool enabled);
 extern bool GetAutoGenerateDefaultAppMenuItems ();
 
-extern void InitializeAvnApp();
+extern void InitializeAvnApp(IAvnApplicationEvents* events);
 extern NSApplicationActivationPolicy AvnDesiredActivationPolicy;
 extern NSPoint ToNSPoint (AvnPoint p);
 extern AvnPoint ToAvnPoint (NSPoint p);

+ 4 - 4
native/Avalonia.Native/src/OSX/main.mm

@@ -163,13 +163,13 @@ class AvaloniaNative : public ComSingleObject<IAvaloniaNativeFactory, &IID_IAval
     
 public:
     FORWARD_IUNKNOWN()
-    virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) override
+    virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator, IAvnApplicationEvents* events) override
     {
         _deallocator = deallocator;
         @autoreleasepool{
             [[ThreadingInitializer new] do];
         }
-        InitializeAvnApp();
+        InitializeAvnApp(events);
         return S_OK;
     };
     
@@ -253,9 +253,9 @@ public:
         return S_OK;
     }
     
-    virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) override
+    virtual HRESULT CreateMenuItemSeparator (IAvnMenuItem** ppv) override
     {
-        *ppv = ::CreateAppMenuItemSeperator();
+        *ppv = ::CreateAppMenuItemSeparator();
         return S_OK;
     }
     

+ 5 - 4
native/Avalonia.Native/src/OSX/menu.h

@@ -31,13 +31,13 @@ private:
     NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem
     IAvnActionCallback* _callback;
     IAvnPredicateCallback* _predicate;
-    bool _isSeperator;
+    bool _isSeparator;
     bool _isCheckable;
     
 public:
     FORWARD_IUNKNOWN()
     
-    AvnAppMenuItem(bool isSeperator);
+    AvnAppMenuItem(bool isSeparator);
     
     NSMenuItem* GetNative();
     
@@ -60,7 +60,6 @@ public:
     void RaiseOnClicked();
 };
 
-
 class AvnAppMenu : public ComSingleObject<IAvnMenu, &IID_IAvnMenu>
 {
 private:
@@ -71,10 +70,12 @@ public:
     FORWARD_IUNKNOWN()
     
     AvnAppMenu(IAvnMenuEvents* events);
-        
+
     AvnMenu* GetNative();
     
     void RaiseNeedsUpdate ();
+    void RaiseOpening();
+    void RaiseClosed();
     
     virtual HRESULT InsertItem (int index, IAvnMenuItem* item) override;
     

+ 30 - 4
native/Avalonia.Native/src/OSX/menu.mm

@@ -71,12 +71,12 @@
 }
 @end
 
-AvnAppMenuItem::AvnAppMenuItem(bool isSeperator)
+AvnAppMenuItem::AvnAppMenuItem(bool isSeparator)
 {
     _isCheckable = false;
-    _isSeperator = isSeperator;
+    _isSeparator = isSeparator;
     
-    if(isSeperator)
+    if(isSeparator)
     {
         _native = [NSMenuItem separatorItem];
     }
@@ -298,6 +298,23 @@ void AvnAppMenu::RaiseNeedsUpdate()
     }
 }
 
+void AvnAppMenu::RaiseOpening()
+{
+    if(_baseEvents != nullptr)
+    {
+        _baseEvents->Opening();
+    }
+}
+
+void AvnAppMenu::RaiseClosed()
+{
+    if(_baseEvents != nullptr)
+    {
+        _baseEvents->Closed();
+    }
+}
+
+
 HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item)
 {
     @autoreleasepool
@@ -382,6 +399,15 @@ HRESULT AvnAppMenu::Clear()
     _parent->RaiseNeedsUpdate();
 }
 
+- (void)menuWillOpen:(NSMenu *)menu
+{
+    _parent->RaiseOpening();
+}
+
+- (void)menuDidClose:(NSMenu *)menu
+{
+    _parent->RaiseClosed();
+}
 
 @end
 
@@ -401,7 +427,7 @@ extern IAvnMenuItem* CreateAppMenuItem()
     }
 }
 
-extern IAvnMenuItem* CreateAppMenuItemSeperator()
+extern IAvnMenuItem* CreateAppMenuItemSeparator()
 {
     @autoreleasepool
     {

+ 11 - 3
native/Avalonia.Native/src/OSX/window.mm

@@ -1877,7 +1877,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     
     for(int i = 0; i < numWindows; i++)
     {
-        [[windows objectAtIndex:i] performClose:nil];
+        auto window = (AvnWindow*)[windows objectAtIndex:i];
+        
+        if([window parentWindow] == nullptr) // Avalonia will handle the child windows.
+        {
+            [window performClose:nil];
+        }
     }
 }
 
@@ -2226,9 +2231,12 @@ protected:
     {
         @autoreleasepool
         {
-            [Window setContentSize:NSSize{x, y}];
+            if (Window != nullptr)
+            {
+                [Window setContentSize:NSSize{x, y}];
             
-            [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))];
+                [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))];
+            }
             
             return S_OK;
         }

+ 0 - 4
nukebuild/Build.cs

@@ -89,10 +89,6 @@ partial class Build : NukeBuild
             Process.Start(new ProcessStartInfo(command, args) {UseShellExecute = false}).WaitForExit();
         }
         ExecWait("dotnet version:", "dotnet", "--version");
-        if (Parameters.IsRunningOnUnix)
-            ExecWait("Mono version:", "mono", "--version");
-
-
     }
 
     IReadOnlyCollection<Output> MsBuildCommon(

+ 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>v10.0</TargetFrameworkVersion>
+    <TargetFrameworkVersion>v11.0</TargetFrameworkVersion>
     <AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

+ 1 - 1
samples/ControlCatalog.Android/Properties/AndroidManifest.xml

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="ControlCatalog.Android" android:versionCode="1" android:versionName="1.0" android:installLocation="auto">
-	<uses-sdk android:targetSdkVersion="29" />
+	<uses-sdk android:targetSdkVersion="30" />
 	<application android:label="ControlCatalog.Android"></application>
 </manifest>

+ 2 - 2
samples/ControlCatalog/MainWindow.xaml

@@ -18,11 +18,11 @@
       <NativeMenuItem Header="File">
         <NativeMenu>
           <NativeMenuItem Icon="/Assets/test_icon.ico" Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/>
-          <NativeMenuItemSeperator/>
+          <NativeMenuItemSeperator/><!-- Uses incorrect spelling to demonstrate backwards compatibility -->
           <NativeMenuItem Icon="/Assets/github_icon.png" Header="Recent">
             <NativeMenu/>
           </NativeMenuItem>
-          <NativeMenuItemSeperator/>
+          <NativeMenuItemSeparator/>
           <NativeMenuItem Header="{x:Static local:MainWindow.MenuQuitHeader}"
                           Gesture="{x:Static local:MainWindow.MenuQuitGesture}"
                           Clicked="OnCloseClicked" />

+ 0 - 47
src/Android/Avalonia.Android/ActivityTracker.cs

@@ -1,47 +0,0 @@
-using Android.App;
-using Android.OS;
-
-namespace Avalonia.Android
-{
-    internal class ActivityTracker : Java.Lang.Object, global::Android.App.Application.IActivityLifecycleCallbacks
-    {
-        public static Activity Current { get; private set; }
-        public void OnActivityCreated(Activity activity, Bundle savedInstanceState)
-        {
-            Current = activity;
-        }
-
-        public void OnActivityDestroyed(Activity activity)
-        {
-            if (Current == activity)
-                Current = null;
-        }
-
-        public void OnActivityPaused(Activity activity)
-        {
-            if (Current == activity)
-                Current = null;
-        }
-
-        public void OnActivityResumed(Activity activity)
-        {
-            Current = activity;
-        }
-
-        public void OnActivitySaveInstanceState(Activity activity, Bundle outState)
-        {
-            Current = activity;
-        }
-
-        public void OnActivityStarted(Activity activity)
-        {
-            Current = activity;
-        }
-
-        public void OnActivityStopped(Activity activity)
-        {
-            if (Current == activity)
-                Current = null;
-        }
-    }
-}

+ 7 - 25
src/Android/Avalonia.Android/AndroidPlatform.cs

@@ -29,60 +29,42 @@ namespace Avalonia
 
 namespace Avalonia.Android
 {
-    class AndroidPlatform : IPlatformSettings, IWindowingPlatform
+    class AndroidPlatform : IPlatformSettings
     {
         public static readonly AndroidPlatform Instance = new AndroidPlatform();
+        public static AndroidPlatformOptions Options { get; private set; }
         public Size DoubleClickSize => new Size(4, 4);
         public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(200);
-        public double RenderScalingFactor => _scalingFactor;
-        public double LayoutScalingFactor => _scalingFactor;
-
-        private readonly double _scalingFactor = 1;
-
-        public AndroidPlatform()
-        {
-            _scalingFactor = global::Android.App.Application.Context.Resources.DisplayMetrics.ScaledDensity;
-        }
 
         public static void Initialize(Type appType, AndroidPlatformOptions options)
         {
+            Options = options;
+
             AvaloniaLocator.CurrentMutable
                 .Bind<IClipboard>().ToTransient<ClipboardImpl>()
-                .Bind<IStandardCursorFactory>().ToTransient<CursorFactory>()
+                .Bind<ICursorFactory>().ToTransient<CursorFactory>()
                 .Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
                 .Bind<IPlatformSettings>().ToConstant(Instance)
                 .Bind<IPlatformThreadingInterface>().ToConstant(new AndroidThreadingInterface())
                 .Bind<ISystemDialogImpl>().ToTransient<SystemDialogImpl>()
-                .Bind<IWindowingPlatform>().ToConstant(Instance)
                 .Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoader>()
-                .Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
+                .Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop())
                 .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
                 .Bind<IAssetLoader>().ToConstant(new AssetLoader(appType.Assembly));
 
             SkiaPlatform.Initialize();
-            ((global::Android.App.Application) global::Android.App.Application.Context.ApplicationContext)
-                .RegisterActivityLifecycleCallbacks(new ActivityTracker());
 
             if (options.UseGpu)
             {
                 EglPlatformOpenGlInterface.TryInitialize();
             }
         }
-
-        public IWindowImpl CreateWindow()
-        {
-            throw new NotSupportedException();
-        }
-
-        public IWindowImpl CreateEmbeddableWindow()
-        {
-            throw new NotSupportedException();
-        }
     }
 
     public sealed class AndroidPlatformOptions
     {
+        public bool UseDeferredRendering { get; set; } = true;
         public bool UseGpu { get; set; } = true;
     }
 }

+ 8 - 7
src/Android/Avalonia.Android/AndroidThreadingInterface.cs

@@ -1,25 +1,26 @@
 using System;
 using System.Reactive.Disposables;
 using System.Threading;
+
 using Android.OS;
+
 using Avalonia.Platform;
 using Avalonia.Threading;
 
+using App = Android.App.Application;
+
 namespace Avalonia.Android
 {
-    class AndroidThreadingInterface : IPlatformThreadingInterface
+    internal sealed class AndroidThreadingInterface : IPlatformThreadingInterface
     {
         private Handler _handler;
 
         public AndroidThreadingInterface()
         {
-            _handler = new Handler(global::Android.App.Application.Context.MainLooper);
+            _handler = new Handler(App.Context.MainLooper);
         }
 
-        public void RunLoop(CancellationToken cancellationToken)
-        {
-            return;
-        }
+        public void RunLoop(CancellationToken cancellationToken) => throw new NotSupportedException();
 
         public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
         {
@@ -57,7 +58,7 @@ namespace Avalonia.Android
                     });
                 }
             }, null, TimeSpan.Zero, interval);
-            
+
             return Disposable.Create(() =>
             {
                 lock (l)

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

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

+ 3 - 8
src/Android/Avalonia.Android/AvaloniaActivity.cs

@@ -1,4 +1,3 @@
-
 using Android.App;
 using Android.OS;
 using Android.Views;
@@ -7,15 +6,13 @@ namespace Avalonia.Android
 {
     public abstract class AvaloniaActivity : Activity
     {
-        
         internal AvaloniaView View;
         object _content;
 
         protected override void OnCreate(Bundle savedInstanceState)
         {
-            RequestWindowFeature(WindowFeatures.NoTitle);
             View = new AvaloniaView(this);
-            if(_content != null)
+            if (_content != null)
                 View.Content = _content;
             SetContentView(View);
             TakeKeyEvents(true);
@@ -36,9 +33,7 @@ namespace Avalonia.Android
             }
         }
 
-        public override bool DispatchKeyEvent(KeyEvent e)
-        {
-            return View.DispatchKeyEvent(e);
-        }
+        public override bool DispatchKeyEvent(KeyEvent e) =>
+            View.DispatchKeyEvent(e) ? true : base.DispatchKeyEvent(e);
     }
 }

+ 34 - 1
src/Android/Avalonia.Android/AvaloniaView.cs

@@ -1,11 +1,12 @@
 using System;
 using Android.Content;
+using Android.Runtime;
 using Android.Views;
 using Android.Widget;
 using Avalonia.Android.Platform.SkiaPlatform;
 using Avalonia.Controls;
 using Avalonia.Controls.Embedding;
-using Avalonia.Platform;
+using Avalonia.Rendering;
 
 namespace Avalonia.Android
 {
@@ -14,6 +15,8 @@ namespace Avalonia.Android
         private readonly EmbeddableControlRoot _root;
         private readonly ViewImpl _view;
 
+        private IDisposable? _timerSubscription;
+
         public AvaloniaView(Context context) : base(context)
         {
             _view = new ViewImpl(context);
@@ -33,6 +36,36 @@ namespace Avalonia.Android
             return _view.View.DispatchKeyEvent(e);
         }
 
+        public override void OnVisibilityAggregated(bool isVisible)
+        {
+            base.OnVisibilityAggregated(isVisible);
+            OnVisibilityChanged(isVisible);
+        }
+
+        protected override void OnVisibilityChanged(View changedView, [GeneratedEnum] ViewStates visibility)
+        {
+            base.OnVisibilityChanged(changedView, visibility);
+            OnVisibilityChanged(visibility == ViewStates.Visible);
+        }
+
+        private void OnVisibilityChanged(bool isVisible)
+        {
+            if (isVisible)
+            {
+                if (AvaloniaLocator.Current.GetService<IRenderTimer>() is ChoreographerTimer timer)
+                {
+                    _timerSubscription = timer.SubscribeView(this);
+                }
+
+                _root.Renderer.Start();
+            }
+            else
+            {
+                _root.Renderer.Stop();
+                _timerSubscription?.Dispose();
+            }
+        }
+
         class ViewImpl : TopLevelImpl
         {
             public ViewImpl(Context context) : base(context)

+ 101 - 0
src/Android/Avalonia.Android/ChoreographerTimer.cs

@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive.Disposables;
+using System.Threading.Tasks;
+
+using Android.OS;
+using Android.Views;
+
+using Avalonia.Rendering;
+
+using Java.Lang;
+
+namespace Avalonia.Android
+{
+    internal sealed class ChoreographerTimer : Java.Lang.Object, IRenderTimer, Choreographer.IFrameCallback
+    {
+        private readonly object _lock = new object();
+
+        private readonly Thread _thread;
+        private readonly TaskCompletionSource<Choreographer> _choreographer = new TaskCompletionSource<Choreographer>();
+
+        private readonly ISet<AvaloniaView> _views = new HashSet<AvaloniaView>();
+
+        private Action<TimeSpan> _tick;
+        private int _count;
+
+        public ChoreographerTimer()
+        {
+            _thread = new Thread(Loop);
+            _thread.Start();
+        }
+
+        public event Action<TimeSpan> Tick
+        {
+            add
+            {
+                lock (_lock)
+                {
+                    _tick += value;
+                    _count++;
+
+                    if (_count == 1)
+                    {
+                        _choreographer.Task.Result.PostFrameCallback(this);
+                    }
+                }
+            }
+            remove
+            {
+                lock (_lock)
+                {
+                    _tick -= value;
+                    _count--;
+                }
+            }
+        }
+
+        internal IDisposable SubscribeView(AvaloniaView view)
+        {
+            lock (_lock)
+            {
+                _views.Add(view);
+
+                if (_views.Count == 1)
+                {
+                    _choreographer.Task.Result.PostFrameCallback(this);
+                }
+            }
+
+            return Disposable.Create(
+                () =>
+                {
+                    lock (_lock)
+                    {
+                        _views.Remove(view);
+                    }
+                }
+            );
+        }
+
+        private void Loop()
+        {
+            Looper.Prepare();
+            _choreographer.SetResult(Choreographer.Instance);
+            Looper.Loop();
+        }
+
+        public void DoFrame(long frameTimeNanos)
+        {
+            _tick?.Invoke(TimeSpan.FromTicks(frameTimeNanos / 100));
+
+            lock (_lock)
+            {
+                if (_count > 0 && _views.Count > 0)
+                {
+                    Choreographer.Instance.PostFrameCallback(this);
+                }
+            }
+        }
+    }
+}

+ 13 - 4
src/Android/Avalonia.Android/CursorFactory.cs

@@ -1,12 +1,21 @@
-using System;
 using Avalonia.Input;
 using Avalonia.Platform;
 
 namespace Avalonia.Android
 {
-    internal class CursorFactory : IStandardCursorFactory
+    internal class CursorFactory : ICursorFactory
     {
-        public IPlatformHandle GetCursor(StandardCursorType cursorType)
-            => new PlatformHandle(IntPtr.Zero, "ZeroCursor");
+        public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => CursorImpl.ZeroCursor;
+
+        public ICursorImpl GetCursor(StandardCursorType cursorType) => CursorImpl.ZeroCursor;
+
+        private sealed class CursorImpl : ICursorImpl
+        {
+            public static CursorImpl ZeroCursor { get; } = new CursorImpl();
+
+            private CursorImpl() { }
+
+            public void Dispose() { }
+        }
     }
 }

+ 2 - 4
src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs

@@ -1,6 +1,4 @@
-using System.Linq;
-
-using Avalonia.OpenGL.Egl;
+using Avalonia.OpenGL.Egl;
 using Avalonia.OpenGL.Surfaces;
 
 namespace Avalonia.Android.OpenGL
@@ -17,7 +15,7 @@ namespace Avalonia.Android.OpenGL
         }
 
         public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() =>
-            new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle));
+            new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle), _info.Handle);
 
         public static GlPlatformSurface TryCreate(IEglWindowGlPlatformSurfaceInfo info)
         {

+ 10 - 3
src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs

@@ -1,23 +1,30 @@
-using Avalonia.OpenGL.Egl;
+using System;
+
+using Avalonia.OpenGL.Egl;
 using Avalonia.OpenGL.Surfaces;
 
 namespace Avalonia.Android.OpenGL
 {
-    internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase
+    internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase, IGlPlatformSurfaceRenderTargetWithCorruptionInfo
     {
         private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info;
         private readonly EglSurface _surface;
+        private readonly IntPtr _handle;
 
         public GlRenderTarget(
             EglPlatformOpenGlInterface egl,
             EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info,
-            EglSurface surface)
+            EglSurface surface,
+            IntPtr handle)
             : base(egl)
         {
             _info = info;
             _surface = surface;
+            _handle = handle;
         }
 
+        public bool IsCorrupted => _handle != _info.Handle;
+
         public override IGlPlatformSurfaceRenderingSession BeginDraw() => BeginDraw(_surface, _info);
     }
 }

+ 0 - 14
src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs

@@ -1,14 +0,0 @@
-using Avalonia.Input;
-
-namespace Avalonia.Android.Platform.Input
-{
-    public class AndroidMouseDevice : MouseDevice
-    {
-        public static AndroidMouseDevice Instance { get; } = new AndroidMouseDevice();
-
-        public AndroidMouseDevice()
-        {
-
-        }
-    }
-}

+ 4 - 2
src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs

@@ -10,7 +10,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
     {
         private IntPtr _window;
 
-        public AndroidFramebuffer(Surface surface)
+        public AndroidFramebuffer(Surface surface, double scaling)
         {
             if(surface == null)
                 throw new ArgumentNullException(nameof(surface));
@@ -31,6 +31,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
             RowBytes = buffer.stride * (Format == PixelFormat.Rgb565 ? 2 : 4);
             Address = buffer.bits;
+
+            Dpi = scaling * new Vector(96, 96);
         }
 
         public void Dispose()
@@ -44,7 +46,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
         public IntPtr Address { get; set; }
         public PixelSize Size { get; }
         public int RowBytes { get; }
-        public Vector Dpi { get; } = new Vector(96, 96);
+        public Vector Dpi { get; }
         public PixelFormat Format { get; }
 
         [DllImport("android")]

+ 1 - 1
src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs

@@ -12,6 +12,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             _topLevel = topLevel;
         }
 
-        public ILockedFramebuffer Lock() => new AndroidFramebuffer(_topLevel.InternalView.Holder.Surface);
+        public ILockedFramebuffer Lock() => new AndroidFramebuffer(_topLevel.InternalView.Holder.Surface, _topLevel.RenderScaling);
     }
 }

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

@@ -43,11 +43,13 @@ namespace Avalonia.Android
             }
         }
 
+        [Obsolete("deprecated")]
         public override void Invalidate(global::Android.Graphics.Rect dirty)
         {
             Invalidate();
         }
 
+        [Obsolete("deprecated")]
         public override void Invalidate(int l, int t, int r, int b)
         {
             Invalidate();

+ 18 - 30
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -6,7 +6,6 @@ using Android.Runtime;
 using Android.Views;
 
 using Avalonia.Android.OpenGL;
-using Avalonia.Android.Platform.Input;
 using Avalonia.Android.Platform.Specific;
 using Avalonia.Android.Platform.Specific.Helpers;
 using Avalonia.Controls;
@@ -35,16 +34,16 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             _view = new ViewImpl(context, this, placeOnTop);
             _keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this);
             _touchHelper = new AndroidTouchEventsHelper<TopLevelImpl>(this, () => InputRoot,
-                p => GetAvaloniaPointFromEvent(p));
+                GetAvaloniaPointFromEvent);
 
             _gl = GlPlatformSurface.TryCreate(this);
             _framebuffer = new FramebufferManager(this);
 
-            MaxClientSize = new Size(_view.Resources.DisplayMetrics.WidthPixels,
-                _view.Resources.DisplayMetrics.HeightPixels);
-        }
-
+            RenderScaling = (int)_view.Resources.DisplayMetrics.Density;
 
+            MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels,
+                _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);
+        }
 
         private bool _handleEvents;
 
@@ -58,25 +57,14 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             }
         }
 
-        public virtual Point GetAvaloniaPointFromEvent(MotionEvent e) => new Point(e.GetX(), e.GetY());
+        public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) =>
+            new Point(e.GetX(pointerIndex), e.GetY(pointerIndex)) / RenderScaling;
 
         public IInputRoot InputRoot { get; private set; }
 
-        public virtual Size ClientSize
-        {
-            get
-            {
-                if (_view == null)
-                    return new Size(0, 0);
-                return new Size(_view.Width, _view.Height);
-            }
-            set
-            {
-
-            }
-        }
+        public virtual Size ClientSize => Size.ToSize(RenderScaling);
 
-        public IMouseDevice MouseDevice => AndroidMouseDevice.Instance;
+        public IMouseDevice MouseDevice { get; } = new MouseDevice();
 
         public Action Closed { get; set; }
 
@@ -98,10 +86,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
         public IEnumerable<object> Surfaces => new object[] { _gl, _framebuffer };
 
-        public IRenderer CreateRenderer(IRenderRoot root)
-        {
-            return new ImmediateRenderer(root);
-        }
+        public IRenderer CreateRenderer(IRenderRoot root) =>
+            AndroidPlatform.Options.UseDeferredRendering
+            ? new DeferredRenderer(root, AvaloniaLocator.Current.GetService<IRenderLoop>()) { RenderOnlyOnRenderThread = true }
+            : new ImmediateRenderer(root);
 
         public virtual void Hide()
         {
@@ -115,15 +103,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
         public Point PointToClient(PixelPoint point)
         {
-            return point.ToPoint(1);
+            return point.ToPoint(RenderScaling);
         }
 
         public PixelPoint PointToScreen(Point point)
         {
-            return PixelPoint.FromPoint(point, 1);
+            return PixelPoint.FromPoint(point, RenderScaling);
         }
 
-        public void SetCursor(IPlatformHandle cursor)
+        public void SetCursor(ICursorImpl cursor)
         {
             //still not implemented
         }
@@ -138,7 +126,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             _view.Visibility = ViewStates.Visible;
         }
 
-        public double RenderScaling => 1;
+        public double RenderScaling { get; }
 
         void Draw()
         {
@@ -193,7 +181,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
             void ISurfaceHolderCallback.SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height)
             {
-                var newSize = new Size(width, height);
+                var newSize = new PixelSize(width, height).ToSize(_tl.RenderScaling);
 
                 if (newSize != _oldSize)
                 {

+ 22 - 69
src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs

@@ -11,7 +11,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers
         private TView _view;
         public bool HandleEvents { get; set; }
 
-        public AndroidTouchEventsHelper(TView view, Func<IInputRoot> getInputRoot, Func<MotionEvent, Point> getPointfunc)
+        public AndroidTouchEventsHelper(TView view, Func<IInputRoot> getInputRoot, Func<MotionEvent, int, Point> getPointfunc)
         {
             this._view = view;
             HandleEvents = true;
@@ -19,11 +19,9 @@ namespace Avalonia.Android.Platform.Specific.Helpers
             _getInputRoot = getInputRoot;
         }
 
-        private DateTime _lastTouchMoveEventTime = DateTime.Now;
-        private Point? _lastTouchMovePoint;
-        private Func<MotionEvent, Point> _getPointFunc;
+        private TouchDevice _touchDevice = new TouchDevice();
+        private Func<MotionEvent, int, Point> _getPointFunc;
         private Func<IInputRoot> _getInputRoot;
-        private Point _point;
 
         public bool? DispatchTouchEvent(MotionEvent e, out bool callBase)
         {
@@ -33,89 +31,44 @@ namespace Avalonia.Android.Platform.Specific.Helpers
                 return null;
             }
 
-            RawPointerEventType? mouseEventType = null;
             var eventTime = DateTime.Now;
+
             //Basic touch support
-            switch (e.Action)
+            var pointerEventType = e.Action switch
             {
-                case MotionEventActions.Move:
-                    //may be bot flood the evnt system with too many event especially on not so powerfull mobile devices
-                    if ((eventTime - _lastTouchMoveEventTime).TotalMilliseconds > 10)
-                    {
-                        mouseEventType = RawPointerEventType.Move;
-                    }
-                    break;
-
-                case MotionEventActions.Down:
-                    mouseEventType = RawPointerEventType.LeftButtonDown;
+                MotionEventActions.Down => RawPointerEventType.TouchBegin,
+                MotionEventActions.Up => RawPointerEventType.TouchEnd,
+                MotionEventActions.Cancel => RawPointerEventType.TouchCancel,
+                _ => RawPointerEventType.TouchUpdate
+            };
 
-                    break;
+            if (e.Action.HasFlag(MotionEventActions.PointerDown))
+            {
+                pointerEventType = RawPointerEventType.TouchBegin;
+            }
 
-                case MotionEventActions.Up:
-                    mouseEventType = RawPointerEventType.LeftButtonUp;
-                    break;
+            if (e.Action.HasFlag(MotionEventActions.PointerUp))
+            {
+                pointerEventType = RawPointerEventType.TouchEnd;
             }
 
-            if (mouseEventType != null)
+            for (int i = 0; i < e.PointerCount; i++)
             {
                 //if point is in view otherwise it's possible avalonia not to find the proper window to dispatch the event
-                _point = _getPointFunc(e);
+                var point = _getPointFunc(e, i);
 
                 double x = _view.View.GetX();
                 double y = _view.View.GetY();
                 double r = x + _view.View.Width;
                 double b = y + _view.View.Height;
 
-                if (x <= _point.X && r >= _point.X && y <= _point.Y && b >= _point.Y)
+                if (x <= point.X && r >= point.X && y <= point.Y && b >= point.Y)
                 {
                     var inputRoot = _getInputRoot();
-                    var mouseDevice = Avalonia.Android.Platform.Input.AndroidMouseDevice.Instance;
-
-                    //in order the controls to work in a predictable way
-                    //we need to generate mouse move before first mouse down event
-                    //as this is the way buttons are working every time
-                    //otherwise there is a problem sometimes
-                    if (mouseEventType == RawPointerEventType.LeftButtonDown)
-                    {
-                        var me = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
-                                    RawPointerEventType.Move, _point, RawInputModifiers.None);
-                        _view.Input(me);
-                    }
 
-                    var mouseEvent = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
-                        mouseEventType.Value, _point, RawInputModifiers.LeftMouseButton);
+                    var mouseEvent = new RawTouchEventArgs(_touchDevice, (uint)eventTime.Ticks, inputRoot,
+                        i == e.ActionIndex ? pointerEventType : RawPointerEventType.TouchUpdate, point, RawInputModifiers.None, e.GetPointerId(i));
                     _view.Input(mouseEvent);
-
-                    if (e.Action == MotionEventActions.Move && mouseDevice.Captured == null)
-                    {
-                        if (_lastTouchMovePoint != null)
-                        {
-                            //raise mouse scroll event so the scrollers
-                            //are moving with the cursor
-                            double vectorX = _point.X - _lastTouchMovePoint.Value.X;
-                            double vectorY = _point.Y - _lastTouchMovePoint.Value.Y;
-                            //based on test correction of 0.02 is working perfect
-                            double correction = 0.02;
-                            var ps = AndroidPlatform.Instance.LayoutScalingFactor;
-                            var mouseWheelEvent = new RawMouseWheelEventArgs(
-                                        mouseDevice,
-                                        (uint)eventTime.Ticks,
-                                        inputRoot,
-                                        _point,
-                                        new Vector(vectorX * correction / ps, vectorY * correction / ps), RawInputModifiers.LeftMouseButton);
-                            _view.Input(mouseWheelEvent);
-                        }
-                        _lastTouchMovePoint = _point;
-                        _lastTouchMoveEventTime = eventTime;
-                    }
-                    else if (e.Action == MotionEventActions.Down)
-                    {
-                        _lastTouchMovePoint = _point;
-                    }
-                    else
-                    {
-                        _lastTouchMovePoint = null;
-                    }
                 }
             }
 

+ 0 - 11
src/Android/Avalonia.Android/app.config

@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<configuration>
-  <runtime>
-    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
-      <dependentAssembly>
-        <assemblyIdentity name="System.Runtime.InteropServices.WindowsRuntime" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
-        <bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
-      </dependentAssembly>
-    </assemblyBinding>
-  </runtime>
-</configuration>

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

@@ -16,7 +16,7 @@
     <AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
     <GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
     <AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk>
-    <TargetFrameworkVersion>v10.0</TargetFrameworkVersion>
+    <TargetFrameworkVersion>v11.0</TargetFrameworkVersion>
     <AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@@ -150,4 +150,4 @@
   <Import Project="..\..\..\build\System.Memory.props" />
   <Import Project="..\..\..\build\AndroidWorkarounds.props" />
   <Import Project="..\..\..\build\LegacyProject.targets" />
-</Project>
+</Project>

+ 1 - 1
src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="Avalonia.AndroidTestApplication" android:versionCode="1" android:versionName="1.0" android:installLocation="auto">
-	<uses-sdk android:targetSdkVersion="29" />
+	<uses-sdk android:targetSdkVersion="30" />
 	<application android:label="Avalonia.AndroidTestApplication" android:icon="@drawable/Icon" android:hardwareAccelerated="true"></application>
 	<uses-permission android:name="android.permission.INTERNET" />
 </manifest>

+ 24 - 4
src/Avalonia.Animation/Animatable.cs

@@ -2,6 +2,7 @@ using System;
 using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Specialized;
+using System.Linq;
 using Avalonia.Data;
 
 #nullable enable
@@ -93,16 +94,35 @@ namespace Avalonia.Animation
                 var oldTransitions = change.OldValue.GetValueOrDefault<Transitions>();
                 var newTransitions = change.NewValue.GetValueOrDefault<Transitions>();
 
+                // When transitions are replaced, we add the new transitions before removing the old
+                // transitions, so that when the old transition being disposed causes the value to
+                // change, there is a corresponding entry in `_transitionStates`. This means that we
+                // need to account for any transitions present in both the old and new transitions
+                // collections.
                 if (newTransitions is object)
                 {
+                    var toAdd = (IList)newTransitions;
+
+                    if (newTransitions.Count > 0 && oldTransitions?.Count > 0)
+                    {
+                        toAdd = newTransitions.Except(oldTransitions).ToList();
+                    }
+
                     newTransitions.CollectionChanged += TransitionsCollectionChanged;
-                    AddTransitions(newTransitions);
+                    AddTransitions(toAdd);
                 }
 
                 if (oldTransitions is object)
                 {
+                    var toRemove = (IList)oldTransitions;
+
+                    if (oldTransitions.Count > 0 && newTransitions?.Count > 0)
+                    {
+                        toRemove = oldTransitions.Except(newTransitions).ToList();
+                    }
+
                     oldTransitions.CollectionChanged -= TransitionsCollectionChanged;
-                    RemoveTransitions(oldTransitions);
+                    RemoveTransitions(toRemove);
                 }
             }
             else if (_transitionsEnabled &&
@@ -115,9 +135,9 @@ namespace Avalonia.Animation
                 {
                     var transition = Transitions[i];
 
-                    if (transition.Property == change.Property)
+                    if (transition.Property == change.Property &&
+                        _transitionState.TryGetValue(transition, out var state))
                     {
-                        var state = _transitionState[transition];
                         var oldValue = state.BaseValue;
                         var newValue = GetAnimationBaseValue(transition.Property);
 

+ 6 - 3
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@@ -65,7 +65,6 @@ namespace Avalonia.PropertyStore
         {
             _subscription?.Dispose();
             _subscription = null;
-            _isSubscribed = false;
             OnCompleted();
         }
 
@@ -74,6 +73,7 @@ namespace Avalonia.PropertyStore
             var oldValue = _value;
             _value = default;
             Priority = BindingPriority.Unset;
+            _isSubscribed = false;
             _sink.Completed(Property, this, oldValue);
         }
 
@@ -104,8 +104,11 @@ namespace Avalonia.PropertyStore
         public void Start(bool ignoreBatchUpdate)
         {
             // We can't use _subscription to check whether we're subscribed because it won't be set
-            // until Subscribe has finished, which will be too late to prevent reentrancy.
-            if (!_isSubscribed && (!_batchUpdate || ignoreBatchUpdate))
+            // until Subscribe has finished, which will be too late to prevent reentrancy. In addition
+            // don't re-subscribe to completed/disposed bindings (indicated by Unset priority).
+            if (!_isSubscribed &&
+                Priority != BindingPriority.Unset &&
+                (!_batchUpdate || ignoreBatchUpdate))
             {
                 _isSubscribed = true;
                 _subscription = Source.Subscribe(this);

+ 8 - 1
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@@ -6,12 +6,19 @@ using Avalonia.Data;
 
 namespace Avalonia.PropertyStore
 {
+    /// <summary>
+    /// Represents an untyped interface to <see cref="ConstantValueEntry{T}"/>.
+    /// </summary>
+    internal interface IConstantValueEntry : IPriorityValueEntry, IDisposable
+    {
+    }
+
     /// <summary>
     /// Stores a value with a priority in a <see cref="ValueStore"/> or
     /// <see cref="PriorityValue{T}"/>.
     /// </summary>
     /// <typeparam name="T">The property type.</typeparam>
-    internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IDisposable
+    internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IConstantValueEntry
     {
         private IValueSink _sink;
         private Optional<T> _value;

+ 1 - 1
src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs

@@ -94,7 +94,7 @@ namespace Avalonia.Utilities
             return (0, false);
         }
 
-        public bool TryGetValue(AvaloniaProperty property, [MaybeNull] out TValue value)
+        public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value)
         {
             (int index, bool found) = TryFindEntry(property.Id);
             if (!found)

+ 35 - 14
src/Avalonia.Base/ValueStore.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using Avalonia.Data;
 using Avalonia.PropertyStore;
 using Avalonia.Utilities;
@@ -56,7 +57,7 @@ namespace Avalonia
 
         public bool IsAnimating(AvaloniaProperty property)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 return slot.Priority < BindingPriority.LocalValue;
             }
@@ -66,7 +67,7 @@ namespace Avalonia
 
         public bool IsSet(AvaloniaProperty property)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 return slot.GetValue().HasValue;
             }
@@ -79,7 +80,7 @@ namespace Avalonia
             BindingPriority maxPriority,
             out T value)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 var v = ((IValue<T>)slot).GetValue(maxPriority);
 
@@ -103,7 +104,7 @@ namespace Avalonia
 
             IDisposable? result = null;
 
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 result = SetExisting(slot, property, value, priority);
             }
@@ -138,7 +139,7 @@ namespace Avalonia
             IObservable<BindingValue<T>> source,
             BindingPriority priority)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 return BindExisting(slot, property, source, priority);
             }
@@ -160,7 +161,7 @@ namespace Avalonia
 
         public void ClearLocalValue<T>(StyledPropertyBase<T> property)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 if (slot is PriorityValue<T> p)
                 {
@@ -173,7 +174,7 @@ namespace Avalonia
                     // During batch update values can't be removed immediately because they're needed to raise
                     // a correctly-typed _sink.ValueChanged notification. They instead mark themselves for removal
                     // by setting their priority to Unset.
-                    if (_batchUpdate is null)
+                    if (!IsBatchUpdating())
                     {
                         _values.Remove(property);
                     }
@@ -198,7 +199,7 @@ namespace Avalonia
 
         public void CoerceValue<T>(StyledPropertyBase<T> property)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 if (slot is PriorityValue<T> p)
                 {
@@ -209,7 +210,7 @@ namespace Avalonia
 
         public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 var slotValue = slot.GetValue();
                 return new Diagnostics.AvaloniaPropertyValue(
@@ -242,6 +243,7 @@ namespace Avalonia
             IPriorityValueEntry entry,
             Optional<T> oldValue)
         {
+            // We need to include remove sentinels here so call `_values.TryGetValue` directly.
             if (_values.TryGetValue(property, out var slot) && slot == entry)
             {
                 if (_batchUpdate is null)
@@ -285,7 +287,7 @@ namespace Avalonia
                 else
                 {
                     var priorityValue = new PriorityValue<T>(_owner, property, this, l);
-                    if (_batchUpdate is object)
+                    if (IsBatchUpdating())
                         priorityValue.BeginBatchUpdate();
                     result = priorityValue.SetValue(value, priority);
                     _values.SetValue(property, priorityValue);
@@ -311,7 +313,7 @@ namespace Avalonia
             {
                 priorityValue = new PriorityValue<T>(_owner, property, this, e);
 
-                if (_batchUpdate is object)
+                if (IsBatchUpdating())
                 {
                     priorityValue.BeginBatchUpdate();
                 }
@@ -338,7 +340,7 @@ namespace Avalonia
         private void AddValue(AvaloniaProperty property, IValue value)
         {
             _values.AddValue(property, value);
-            if (_batchUpdate is object && value is IBatchUpdate batch)
+            if (IsBatchUpdating() && value is IBatchUpdate batch)
                 batch.BeginBatchUpdate();
             value.Start();
         }
@@ -364,6 +366,21 @@ namespace Avalonia
             }
         }
 
+        private bool IsBatchUpdating() => _batchUpdate?.IsBatchUpdating == true;
+
+        private bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out IValue value)
+        {
+            return _values.TryGetValue(property, out value) && !IsRemoveSentinel(value);
+        }
+
+        private static bool IsRemoveSentinel(IValue value)
+        {
+            // Local value entries are optimized and contain only a single value field to save space,
+            // so there's no way to mark them for removal at the end of a batch update. Instead a
+            // ConstantValueEntry with a priority of Unset is used as a sentinel value.
+            return value is IConstantValueEntry t && t.Priority == BindingPriority.Unset;
+        }
+
         private class BatchUpdate
         {
             private ValueStore _owner;
@@ -373,6 +390,8 @@ namespace Avalonia
 
             public BatchUpdate(ValueStore owner) => _owner = owner;
 
+            public bool IsBatchUpdating => _batchUpdateCount > 0;
+
             public void Begin()
             {
                 if (_batchUpdateCount++ == 0)
@@ -437,8 +456,10 @@ namespace Avalonia
 
                             // During batch update values can't be removed immediately because they're needed to raise
                             // the _sink.ValueChanged notification. They instead mark themselves for removal by setting
-                            // their priority to Unset.
-                            if (slot.Priority == BindingPriority.Unset)
+                            // their priority to Unset. We need to re-read the slot here because raising ValueChanged
+                            // could have caused it to be updated.
+                            if (values.TryGetValue(entry.property, out var updatedSlot) &&
+                                updatedSlot.Priority == BindingPriority.Unset)
                             {
                                 values.Remove(entry.property);
                             }

+ 12 - 0
src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs

@@ -0,0 +1,12 @@
+using System;
+using XamlX.Transform;
+
+namespace Avalonia.Build.Tasks
+{
+    public class DeterministicIdGenerator : IXamlIdentifierGenerator
+    {
+        private int _nextId = 1;
+        
+        public string GenerateIdentifierPart() => (_nextId++).ToString();
+    }
+}

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

@@ -22,7 +22,6 @@ using XamlX.IL;
 
 namespace Avalonia.Build.Tasks
 {
-    
     public static partial class XamlCompilerTaskExecutor
     {
         static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml")
@@ -99,7 +98,8 @@ namespace Avalonia.Build.Tasks
                 XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage),
                 AvaloniaXamlIlLanguage.CustomValueConverter,
                 new XamlIlClrPropertyInfoEmitter(typeSystem.CreateTypeBuilder(clrPropertiesDef)),
-                new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure)));
+                new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure)),
+                new DeterministicIdGenerator());
 
 
             var contextDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlContext", 

+ 35 - 21
src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs

@@ -17,7 +17,6 @@ namespace Avalonia.Controls
     /// </summary>
     public class DataGridCheckBoxColumn : DataGridBoundColumn
     {
-        private bool _beganEditWithKeyboard;
         private CheckBox _currentCheckBox;
         private DataGrid _owningGrid;
 
@@ -153,23 +152,7 @@ namespace Avalonia.Controls
         {
             if (editingElement is CheckBox editingCheckBox)
             {
-                bool? uneditedValue = editingCheckBox.IsChecked;
-                bool editValue = false;
-                if(editingEventArgs is PointerPressedEventArgs args)
-                {
-                    // Editing was triggered by a mouse click
-                    Point position = args.GetPosition(editingCheckBox);
-                    Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height);
-                    editValue = rect.Contains(position);
-                }
-                else if (_beganEditWithKeyboard)
-                {
-                    // Editing began by a user pressing spacebar
-                    editValue = true;
-                    _beganEditWithKeyboard = false;
-                }
-
-                if (editValue)
+                void EditValue()
                 {
                     // User clicked the checkbox itself or pressed space, let's toggle the IsChecked value
                     if (editingCheckBox.IsThreeState)
@@ -192,6 +175,40 @@ namespace Avalonia.Controls
                         editingCheckBox.IsChecked = !editingCheckBox.IsChecked;
                     }
                 }
+
+                bool? uneditedValue = editingCheckBox.IsChecked;
+                if(editingEventArgs is PointerPressedEventArgs args)
+                {
+                    void ProcessPointerArgs()
+                    {
+                        // Editing was triggered by a mouse click
+                        Point position = args.GetPosition(editingCheckBox);
+                        Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height);
+                        if(rect.Contains(position))
+                        {
+                            EditValue();
+                        }
+                    }
+                    
+                    void OnLayoutUpdated(object sender, EventArgs e)
+                    {
+                        if(!editingCheckBox.Bounds.IsEmpty)
+                        {
+                            editingCheckBox.LayoutUpdated -= OnLayoutUpdated;
+                            ProcessPointerArgs();
+                        }
+                    }
+
+                    if(editingCheckBox.Bounds.IsEmpty)
+                    {
+                        editingCheckBox.LayoutUpdated += OnLayoutUpdated;
+                    }
+                    else
+                    {
+                        ProcessPointerArgs();
+                    }
+                }
+
                 return uneditedValue;
             }
             return false;
@@ -284,13 +301,10 @@ namespace Avalonia.Controls
                     CheckBox checkBox = GetCellContent(row) as CheckBox;
                     if (checkBox == _currentCheckBox)
                     {
-                        _beganEditWithKeyboard = true;
                         OwningGrid.BeginEdit();
-                        return;
                     }
                 }
             }
-            _beganEditWithKeyboard = false;
         }
 
         private void OwningGrid_LoadingRow(object sender, DataGridRowEventArgs e)

+ 3 - 1
src/Avalonia.Controls/ApiCompatBaseline.txt

@@ -1,8 +1,10 @@
 Compat issues with assembly Avalonia.Controls:
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseClosed()' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract.
 MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
 MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract.
 EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation.
 MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
-Total Issues: 5
+Total Issues: 7

+ 9 - 2
src/Avalonia.Controls/Application.cs

@@ -30,7 +30,7 @@ namespace Avalonia
     /// method.
     /// - Tracks the lifetime of the application.
     /// </remarks>
-    public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost
+    public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost, IApplicationPlatformEvents
     {
         /// <summary>
         /// The application-global data templates.
@@ -55,6 +55,8 @@ namespace Avalonia
         /// <inheritdoc/>
         public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
 
+        public event EventHandler<UrlOpenedEventArgs> UrlsOpened; 
+
         /// <summary>
         /// Creates an instance of the <see cref="Application"/> class.
         /// </summary>
@@ -247,7 +249,11 @@ namespace Avalonia
 
         public virtual void OnFrameworkInitializationCompleted()
         {
-            
+        }
+        
+        void  IApplicationPlatformEvents.RaiseUrlsOpened(string[] urls)
+        {
+            UrlsOpened?.Invoke(this, new UrlOpenedEventArgs (urls));
         }
 
         private void NotifyResourcesChanged(ResourcesChangedEventArgs e)
@@ -288,5 +294,6 @@ namespace Avalonia
             get => _name;
             set => SetAndRaise(NameProperty, ref _name, value);
         }
+        
     }
 }

+ 14 - 0
src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

@@ -5,6 +5,7 @@ using System.Threading;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Interactivity;
+using Avalonia.Platform;
 using Avalonia.Threading;
 
 namespace Avalonia.Controls.ApplicationLifetimes
@@ -102,6 +103,14 @@ namespace Avalonia.Controls.ApplicationLifetimes
         public int Start(string[] args)
         {
             Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args));
+
+            var options = AvaloniaLocator.Current.GetService<ClassicDesktopStyleApplicationLifetimeOptions>();
+            
+            if(options != null && options.ProcessUrlActivationCommandLine && args.Length > 0)
+            {
+                ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args);
+            }
+
             _cts = new CancellationTokenSource();
             MainWindow?.Show();
             Dispatcher.UIThread.MainLoop(_cts.Token);
@@ -115,6 +124,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
                 _activeLifetime = null;
         }
     }
+    
+    public class ClassicDesktopStyleApplicationLifetimeOptions
+    {
+        public bool ProcessUrlActivationCommandLine { get; set; }
+    }
 }
 
 namespace Avalonia

+ 5 - 3
src/Avalonia.Controls/AutoCompleteBox.cs

@@ -483,7 +483,9 @@ namespace Avalonia.Controls
             AvaloniaProperty.RegisterDirect<AutoCompleteBox, object>(
                 nameof(SelectedItem),
                 o => o.SelectedItem,
-                (o, v) => o.SelectedItem = v);
+                (o, v) => o.SelectedItem = v,
+                defaultBindingMode: BindingMode.TwoWay,
+                enableDataValidation: true);
 
         /// <summary>
         /// Identifies the
@@ -1333,7 +1335,7 @@ namespace Avalonia.Controls
 
             base.OnApplyTemplate(e);
         }
-        
+
         /// <summary>
         /// Called to update the validation state for properties for which data validation is
         /// enabled.
@@ -1342,7 +1344,7 @@ namespace Avalonia.Controls
         /// <param name="value">The new binding value for the property.</param>
         protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
         {
-            if (property == TextProperty)
+            if (property == TextProperty || property == SelectedItemProperty)
             {
                 DataValidationErrors.SetError(this, value.Error);
             }

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

@@ -218,6 +218,7 @@ namespace Avalonia.Controls
             if (Command != null)
             {
                 Command.CanExecuteChanged += CanExecuteChanged;
+                CanExecuteChanged(this, EventArgs.Empty);
             }
         }
 

+ 55 - 22
src/Avalonia.Controls/DefinitionBase.cs

@@ -662,31 +662,64 @@ namespace Avalonia.Controls
                 {
                     DefinitionBase definitionBase = _registry[i];
 
-                    if (sharedMinSizeChanged || definitionBase.LayoutWasUpdated)
+                    // we'll set d.UseSharedMinimum to maintain the invariant:
+                    //      d.UseSharedMinimum iff d._minSize < this.MinSize
+                    // i.e. iff d is not a "long-pole" definition.
+                    //
+                    // Measure/Arrange of d's Grid uses d._minSize for long-pole
+                    // definitions, and max(d._minSize, shared size) for
+                    // short-pole definitions.  This distinction allows us to react
+                    // to changes in "long-pole-ness" more efficiently and correctly,
+                    // by avoiding remeasures when a long-pole definition changes.
+                    bool useSharedMinimum = !MathUtilities.AreClose(definitionBase._minSize, sharedMinSize);
+
+                    // before doing that, determine whether d's Grid needs to be remeasured.
+                    // It's important _not_ to remeasure if the last measure is still
+                    // valid, otherwise infinite loops are possible
+                    bool measureIsValid;
+
+                    if(!definitionBase.UseSharedMinimum)
                     {
-                        //  if definition's min size is different, then need to re-measure
-                        if (!MathUtilities.AreClose(sharedMinSize, definitionBase.MinSize))
-                        {
-                            Grid parentGrid = (Grid)definitionBase.Parent;
-                            parentGrid.InvalidateMeasure();
-                            definitionBase.UseSharedMinimum = true;
-                        }
-                        else
-                        {
-                            definitionBase.UseSharedMinimum = false;
-
-                            //  if measure is valid then also need to check arrange.
-                            //  Note: definitionBase.SizeCache is volatile but at this point 
-                            //  it contains up-to-date final size
-                            if (!MathUtilities.AreClose(sharedMinSize, definitionBase.SizeCache))
-                            {
-                                Grid parentGrid = (Grid)definitionBase.Parent;
-                                parentGrid.InvalidateArrange();
-                            }
-                        }
+                        // d was a long-pole.  measure is valid iff it's still a long-pole,
+                        // since previous measure didn't use shared size.
+                        measureIsValid = !useSharedMinimum;
+                    }
+                    else if(useSharedMinimum)
+                    {
+                        // d was a short-pole, and still is.  measure is valid
+                        // iff the shared size didn't change
+                        measureIsValid = !sharedMinSizeChanged;
+                    }
+                    else
+                    {
+                        // d was a short-pole, but is now a long-pole.  This can
+                        // happen in several ways:
+                        //  a. d's minSize increased to or past the old shared size
+                        //  b. other long-pole definitions decreased, leaving
+                        //      d as the new winner
+                        // In the former case, the measure is valid - it used
+                        // d's new larger minSize.  In the latter case, the
+                        // measure is invalid - it used the old shared size,
+                        // which is larger than d's (possibly changed) minSize
+                        measureIsValid = (definitionBase.LayoutWasUpdated &&
+                                        MathUtilities.GreaterThanOrClose(definitionBase._minSize, this.MinSize));
+                    }
 
-                        definitionBase.LayoutWasUpdated = false;
+                    if(!measureIsValid)
+                    {
+                        definitionBase.Parent.InvalidateMeasure();
                     }
+                    else if (!MathUtilities.AreClose(sharedMinSize, definitionBase.SizeCache))
+                    {
+                        //  if measure is valid then also need to check arrange.
+                        //  Note: definitionBase.SizeCache is volatile but at this point 
+                        //  it contains up-to-date final size
+                        definitionBase.Parent.InvalidateArrange();
+                    }
+
+                    // now we can restore the invariant, and clear the layout flag
+                    definitionBase.UseSharedMinimum = useSharedMinimum;
+                    definitionBase.LayoutWasUpdated = false;
                 }
 
                 _minSize = sharedMinSize;

+ 2 - 0
src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs

@@ -3,5 +3,7 @@ namespace Avalonia.Controls
     public interface INativeMenuExporterEventsImplBridge
     {
         void RaiseNeedsUpdate ();
+        void RaiseOpening();
+        void RaiseClosed();
     }
 }

+ 32 - 1
src/Avalonia.Controls/NativeMenu.cs

@@ -12,13 +12,34 @@ namespace Avalonia.Controls
         private readonly AvaloniaList<NativeMenuItemBase> _items =
             new AvaloniaList<NativeMenuItemBase> { ResetBehavior = ResetBehavior.Remove };
         private NativeMenuItem _parent;
+
         [Content]
         public IList<NativeMenuItemBase> Items => _items;
 
         /// <summary>
-        /// Raised when the user clicks the menu and before its opened. Use this event to update the menu dynamically.
+        /// Raised when the menu requests an update.
+        /// </summary>
+        /// <remarks>
+        /// Use this event to add, remove or modify menu items before a menu is
+        /// shown or a hotkey is pressed.
+        /// </remarks>
+        public event EventHandler<EventArgs> NeedsUpdate;
+
+        /// <summary>
+        /// Raised before the menu is opened.
         /// </summary>
+        /// <remarks>
+        /// Do not update the menu in this event; use <see cref="NeedsUpdate"/>.
+        /// </remarks>
         public event EventHandler<EventArgs> Opening;
+        
+        /// <summary>
+        /// Raised after the menu is closed.
+        /// </summary>
+        /// <remarks>
+        /// Do not update the menu in this event; use <see cref="NeedsUpdate"/>.
+        /// </remarks>
+        public event EventHandler<EventArgs> Closed;
 
         public NativeMenu()
         {
@@ -27,10 +48,20 @@ namespace Avalonia.Controls
         }
 
         void INativeMenuExporterEventsImplBridge.RaiseNeedsUpdate()
+        {
+            NeedsUpdate?.Invoke(this, EventArgs.Empty);
+        }
+
+        void INativeMenuExporterEventsImplBridge.RaiseOpening()
         {
             Opening?.Invoke(this, EventArgs.Empty);
         }
 
+        void INativeMenuExporterEventsImplBridge.RaiseClosed()
+        {
+            Closed?.Invoke(this, EventArgs.Empty);
+        }
+
         private void Validator(NativeMenuItemBase obj)
         {
             if (obj.Parent != null)

+ 16 - 0
src/Avalonia.Controls/NativeMenuItemSeparator.cs

@@ -0,0 +1,16 @@
+using System;
+
+namespace Avalonia.Controls
+{
+
+    [Obsolete("This class exists to maintain backwards compatiblity with existing code. Use NativeMenuItemSeparator instead")]
+    public class NativeMenuItemSeperator : NativeMenuItemSeparator 
+    {
+    }
+
+    public class NativeMenuItemSeparator : NativeMenuItemBase
+    {
+        [Obsolete("This is a temporary hack to make our MenuItem recognize this as a separator, don't use", true)]
+        public string Header => "-";
+    }
+}

+ 0 - 10
src/Avalonia.Controls/NativeMenuItemSeperator.cs

@@ -1,10 +0,0 @@
-using System;
-
-namespace Avalonia.Controls
-{
-    public class NativeMenuItemSeperator : NativeMenuItemBase
-    {
-        [Obsolete("This is a temporary hack to make our MenuItem recognize this as a separator, don't use", true)]
-        public string Header => "-";
-    }
-}

+ 16 - 2
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@@ -91,14 +91,14 @@ namespace Avalonia.Controls
         /// </summary>
         public static readonly DirectProperty<NumericUpDown, string> TextProperty =
             AvaloniaProperty.RegisterDirect<NumericUpDown, string>(nameof(Text), o => o.Text, (o, v) => o.Text = v,
-                defaultBindingMode: BindingMode.TwoWay);
+                defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
 
         /// <summary>
         /// Defines the <see cref="Value"/> property.
         /// </summary>
         public static readonly DirectProperty<NumericUpDown, double> ValueProperty =
             AvaloniaProperty.RegisterDirect<NumericUpDown, double>(nameof(Value), updown => updown.Value,
-                (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay);
+                (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
 
         /// <summary>
         /// Defines the <see cref="Watermark"/> property.
@@ -370,6 +370,20 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Called to update the validation state for properties for which data validation is
+        /// enabled.
+        /// </summary>
+        /// <param name="property">The property.</param>
+        /// <param name="value">The new binding value for the property.</param>
+        protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
+        {
+            if (property == TextProperty || property == ValueProperty)
+            {
+                DataValidationErrors.SetError(this, value.Error);
+            }
+        }
+
         /// <summary>
         /// Called when the <see cref="CultureInfo"/> property value changed.
         /// </summary>

+ 7 - 0
src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs

@@ -0,0 +1,7 @@
+namespace Avalonia.Platform
+{
+    public interface IApplicationPlatformEvents
+    {
+        void RaiseUrlsOpened(string[] urls);
+    }
+}

+ 18 - 4
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -64,7 +64,7 @@ namespace Avalonia.Controls.Primitives
                 nameof(SelectedItem),
                 o => o.SelectedItem,
                 (o, v) => o.SelectedItem = v,
-                defaultBindingMode: BindingMode.TwoWay);
+                defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
 
         /// <summary>
         /// Defines the <see cref="SelectedItems"/> property.
@@ -466,6 +466,20 @@ namespace Avalonia.Controls.Primitives
             EndUpdating();
         }
 
+        /// <summary>
+        /// Called to update the validation state for properties for which data validation is
+        /// enabled.
+        /// </summary>
+        /// <param name="property">The property.</param>
+        /// <param name="value">The new binding value for the property.</param>
+        protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
+        {
+            if (property == SelectedItemProperty)
+            {
+                DataValidationErrors.SetError(this, value.Error);
+            }
+        }
+        
         protected override void OnInitialized()
         {
             base.OnInitialized();
@@ -707,7 +721,7 @@ namespace Avalonia.Controls.Primitives
                 _oldSelectedItem = SelectedItem;
             }
             else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) &&
-                _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems)
+                     _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems)
             {
                 RaisePropertyChanged(
                     SelectedItemsProperty,
@@ -977,7 +991,7 @@ namespace Avalonia.Controls.Primitives
             public Optional<ISelectionModel> Selection { get; set; }
             public Optional<IList?> SelectedItems { get; set; }
 
-            public Optional<int> SelectedIndex 
+            public Optional<int> SelectedIndex
             {
                 get => _selectedIndex;
                 set
@@ -996,6 +1010,6 @@ namespace Avalonia.Controls.Primitives
                     _selectedIndex = default;
                 }
             }
-       }
+        }
     }
 }

+ 3 - 1
src/Avalonia.Controls/Slider.cs

@@ -341,7 +341,9 @@ namespace Avalonia.Controls
 
             var pointNum = orient ? x.Position.X : x.Position.Y;
             var logicalPos = MathUtilities.Clamp(pointNum / pointDen, 0.0d, 1.0d);
-            var invert = orient ? 0 : 1;
+            var invert = orient ? 
+                IsDirectionReversed ? 1 : 0 :
+                IsDirectionReversed ? 0 : 1;
             var calcVal = Math.Abs(invert - logicalPos);
             var range = Maximum - Minimum;
             var finalValue = calcVal * range + Minimum;

+ 29 - 14
src/Avalonia.Controls/TextBox.cs

@@ -514,21 +514,36 @@ namespace Avalonia.Controls
 
         private void HandleTextInput(string input)
         {
-            if (!IsReadOnly)
+            if (IsReadOnly)
             {
-                input = RemoveInvalidCharacters(input);
-                string text = Text ?? string.Empty;
-                int caretIndex = CaretIndex;
-                if (!string.IsNullOrEmpty(input) && (MaxLength == 0 || input.Length + text.Length - (Math.Abs(SelectionStart - SelectionEnd)) <= MaxLength))
-                {
-                    DeleteSelection();
-                    caretIndex = CaretIndex;
-                    text = Text ?? string.Empty;
-                    SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
-                    CaretIndex += input.Length;
-                    ClearSelection();
-                    _undoRedoHelper.DiscardRedo();
-                }
+                return;
+            }
+            
+            input = RemoveInvalidCharacters(input);
+            
+            if (string.IsNullOrEmpty(input))
+            {
+                return;
+            }
+            
+            string text = Text ?? string.Empty;
+            int caretIndex = CaretIndex;
+            int newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd);
+            
+            if (MaxLength > 0 && newLength > MaxLength)
+            {
+                input = input.Remove(Math.Max(0, input.Length - (newLength - MaxLength)));
+            }
+            
+            if (!string.IsNullOrEmpty(input))
+            {
+                DeleteSelection();
+                caretIndex = CaretIndex;
+                text = Text ?? string.Empty;
+                SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
+                CaretIndex += input.Length;
+                ClearSelection();
+                _undoRedoHelper.DiscardRedo();
             }
         }
 

+ 14 - 0
src/Avalonia.Controls/UrlOpenedEventArgs.cs

@@ -0,0 +1,14 @@
+using System;
+
+namespace Avalonia
+{
+    public class UrlOpenedEventArgs : EventArgs
+    {
+        public UrlOpenedEventArgs(string[] urls)
+        {
+            Urls = urls;
+        }
+        
+        public string[] Urls { get; }
+    }
+}

+ 1 - 1
src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs

@@ -169,7 +169,7 @@ namespace Avalonia.DesignerSupport.Remote
             if (entryPoint == null)
                 throw Die($"Assembly {args.AppPath} doesn't have an entry point");
             var builderMethod = entryPoint.DeclaringType.GetMethod(BuilderMethodName,
-                BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
+                BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, Array.Empty<Type>(), null);
             if (builderMethod == null)
                 throw Die($"{entryPoint.DeclaringType.FullName} doesn't have a method named {BuilderMethodName}");
             Design.IsDesignMode = true;

+ 1 - 1
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@@ -192,7 +192,7 @@ namespace Avalonia.FreeDesktop
             {
                 var (it, menu) = i;
 
-                if (it is NativeMenuItemSeperator)
+                if (it is NativeMenuItemSeparator)
                 {
                     if (name == "type")
                         return "separator";

+ 14 - 0
src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs

@@ -0,0 +1,14 @@
+using System;
+using Avalonia.Native.Interop;
+using Avalonia.Platform;
+
+namespace Avalonia.Native
+{
+    internal class AvaloniaNativeApplicationPlatform : CallbackBase, IAvnApplicationEvents
+    {
+        void IAvnApplicationEvents.FilesOpened(IAvnStringArray urls)
+        {
+            ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(urls.ToStringArray());
+        }
+    }
+}

+ 4 - 3
src/Avalonia.Native/AvaloniaNativePlatform.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Runtime.InteropServices;
-using System.Security.Cryptography;
 using Avalonia.Controls.Platform;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
@@ -9,7 +8,6 @@ using Avalonia.Native.Interop;
 using Avalonia.OpenGL;
 using Avalonia.Platform;
 using Avalonia.Rendering;
-using Avalonia.Platform.Interop;
 
 namespace Avalonia.Native
 {
@@ -86,7 +84,10 @@ namespace Avalonia.Native
         void DoInitialize(AvaloniaNativePlatformOptions options)
         {
             _options = options;
-            _factory.Initialize(new GCHandleDeallocator());
+            
+            var applicationPlatform = new AvaloniaNativeApplicationPlatform();
+            
+            _factory.Initialize(new GCHandleDeallocator(), applicationPlatform);
             if (_factory.MacOptions != null)
             {
                 var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>();

+ 24 - 2
src/Avalonia.Native/IAvnMenu.cs

@@ -20,11 +20,23 @@ namespace Avalonia.Native.Interop
         {
             _parent?.RaiseNeedsUpdate();
         }
+
+        public void Opening()
+        {
+            _parent?.RaiseOpening();
+        }
+
+        public void Closed()
+        {
+            _parent?.RaiseClosed();
+        }
     }
 
     partial interface IAvnMenu
     {
         void RaiseNeedsUpdate();
+        void RaiseOpening();
+        void RaiseClosed();
         void Deinitialise();
     }
 }
@@ -45,6 +57,16 @@ namespace Avalonia.Native.Interop.Impl
             _exporter.UpdateIfNeeded();
         }
 
+        public void RaiseOpening()
+        {
+            (ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseOpening();
+        }
+
+        public void RaiseClosed()
+        {
+            (ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseClosed();
+        }
+
         internal NativeMenu ManagedMenu { get; private set; }
 
         public static __MicroComIAvnMenuProxy Create(IAvaloniaNativeFactory factory)
@@ -103,8 +125,8 @@ namespace Avalonia.Native.Interop.Impl
 
         private __MicroComIAvnMenuItemProxy CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item)
         {
-            var nativeItem = (__MicroComIAvnMenuItemProxy)(item is NativeMenuItemSeperator ?
-                factory.CreateMenuItemSeperator() :
+            var nativeItem = (__MicroComIAvnMenuItemProxy)(item is NativeMenuItemSeparator ?
+                factory.CreateMenuItemSeparator() :
                 factory.CreateMenuItem());
             nativeItem.ManagedMenuItem = item;
 

+ 10 - 5
src/Avalonia.Native/avn.idl

@@ -403,7 +403,7 @@ enum AvnExtendClientAreaChromeHints
 [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)]
 interface IAvaloniaNativeFactory : IUnknown
 {
-     HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator);
+     HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator, IAvnApplicationEvents* appCb);
      IAvnMacOptions* GetMacOptions();
      HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv);
      HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv);
@@ -417,7 +417,7 @@ interface IAvaloniaNativeFactory : IUnknown
      HRESULT SetAppMenu(IAvnMenu* menu);
      HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv);
      HRESULT CreateMenuItem(IAvnMenuItem** ppv);
-     HRESULT CreateMenuItemSeperator(IAvnMenuItem** ppv);
+     HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv);
 }
 
 [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)]
@@ -685,10 +685,9 @@ interface IAvnMenuItem : IUnknown
 [uuid(0af7df53-7632-42f4-a650-0992c361b477)]
 interface IAvnMenuEvents : IUnknown
 {
-    /**
-     * NeedsUpdate
-     */
      void NeedsUpdate();
+     void Opening();
+     void Closed();
 }
 
 [uuid(5142bb41-66ab-49e7-bb37-cd079c000f27)]
@@ -728,3 +727,9 @@ interface IAvnNativeControlHostTopLevelAttachment : IUnknown
      void HideWithSize(float width, float height);
      void ReleaseChild();
 }
+
+[uuid(6575b5af-f27a-4609-866c-f1f014c20f79)]
+interface IAvnApplicationEvents : IUnknown
+{
+     void FilesOpened (IAvnStringArray* urls);
+}

+ 11 - 7
src/Avalonia.ReactiveUI/AppBuilderExtensions.cs

@@ -9,18 +9,22 @@ namespace Avalonia.ReactiveUI
     {
         /// <summary>
         /// Initializes ReactiveUI framework to use with Avalonia. Registers Avalonia 
-        /// scheduler and Avalonia activation for view fetcher. Always remember to
-        /// call this method if you are using ReactiveUI in your application.
+        /// scheduler, an activation for view fetcher, a template binding hook. Remember
+        /// to call this method if you are using ReactiveUI in your application.
         /// </summary>
         public static TAppBuilder UseReactiveUI<TAppBuilder>(this TAppBuilder builder)
-            where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
-        {
-            return builder.AfterPlatformServicesSetup(_ =>
+            where TAppBuilder : AppBuilderBase<TAppBuilder>, new() =>
+            builder.AfterPlatformServicesSetup(_ => Locator.RegisterResolverCallbackChanged(() =>
             {
+                if (Locator.CurrentMutable is null)
+                {
+                    return;
+                }
+
+                PlatformRegistrationManager.SetRegistrationNamespaces(RegistrationNamespace.Avalonia);
                 RxApp.MainThreadScheduler = AvaloniaScheduler.Instance;
                 Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
                 Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook));
-            });
-        }
+            }));
     }
 }

+ 1 - 1
src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml

@@ -35,7 +35,7 @@
       <ItemsControl Items="{Binding}">
         <ItemsControl.ItemTemplate>
           <DataTemplate>
-            <TextBlock Text="{Binding Message}" Foreground="{DynamicResource SystemControlErrorTextForegroundBrush}" TextWrapping="Wrap" />
+            <TextBlock Text="{Binding }" Foreground="{DynamicResource SystemControlErrorTextForegroundBrush}" TextWrapping="Wrap" />
           </DataTemplate>
         </ItemsControl.ItemTemplate>
       </ItemsControl>

+ 3 - 2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs

@@ -14,8 +14,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
             XamlXmlnsMappings xmlnsMappings,
             XamlValueConverter customValueConverter,
             XamlIlClrPropertyInfoEmitter clrPropertyEmitter,
-            XamlIlPropertyInfoAccessorFactoryEmitter accessorFactoryEmitter)
-            : base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter)
+            XamlIlPropertyInfoAccessorFactoryEmitter accessorFactoryEmitter,
+            IXamlIdentifierGenerator identifierGenerator = null)
+            : base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter, identifierGenerator)
         {
             ClrPropertyEmitter = clrPropertyEmitter;
             AccessorFactoryEmitter = accessorFactoryEmitter;

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github

@@ -1 +1 @@
-Subproject commit f3ca2028f4f64be3556a6afd22f192902de095e5
+Subproject commit 9e90d34e97c766ba8dcb70128147fcded65d195a

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs

@@ -30,7 +30,7 @@ namespace Avalonia.Markup.Xaml.Templates
 
         public IControl Build(object data, IControl existing)
         {
-            return existing ?? TemplateContent.Load(Content).Control;
+            return existing ?? TemplateContent.Load(Content)?.Control;
         }
     }
 }

+ 1 - 2
src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs

@@ -10,8 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates
         [TemplateContent]
         public object Content { get; set; }
 
-        public IPanel Build()
-                => (IPanel)TemplateContent.Load(Content).Control;
+        public IPanel Build() => (IPanel)TemplateContent.Load(Content)?.Control;
 
         object ITemplate.Build() => Build();
     }

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs

@@ -10,7 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates
         [TemplateContent]
         public object Content { get; set; }
 
-        public IControl Build() => TemplateContent.Load(Content).Control;
+        public IControl Build() => TemplateContent.Load(Content)?.Control;
 
         object ITemplate.Build() => Build();
     }

+ 6 - 2
src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs

@@ -1,6 +1,4 @@
 using System;
-using Avalonia.Controls;
-using System.Collections.Generic;
 using Avalonia.Controls.Templates;
 
 namespace Avalonia.Markup.Xaml.Templates
@@ -14,6 +12,12 @@ namespace Avalonia.Markup.Xaml.Templates
             {
                 return (ControlTemplateResult)direct(null);
             }
+
+            if (templateContent is null)
+            {
+                return null;
+            }
+
             throw new ArgumentException(nameof(templateContent));
         }
     }

+ 6 - 2
src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs

@@ -51,8 +51,12 @@ namespace Avalonia.Markup.Xaml.Templates
 
         public IControl Build(object data)
         {
-            var visualTreeForItem = TemplateContent.Load(Content).Control;
-            visualTreeForItem.DataContext = data;
+            var visualTreeForItem = TemplateContent.Load(Content)?.Control;
+            if (visualTreeForItem != null)
+            {
+                visualTreeForItem.DataContext = data;
+            }
+
             return visualTreeForItem;
         }
     }

+ 14 - 0
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -84,6 +84,7 @@ namespace Avalonia.Win32
         private WindowImpl _parent;        
         private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default;
         private bool _isCloseRequested;
+        private bool _shown;
 
         public WindowImpl()
         {
@@ -565,6 +566,7 @@ namespace Avalonia.Win32
         public void Hide()
         {
             UnmanagedMethods.ShowWindow(_hwnd, ShowWindowCommand.Hide);
+            _shown = false;
         }
 
         public virtual void Show(bool activate)
@@ -871,6 +873,11 @@ namespace Avalonia.Win32
 
         private void ExtendClientArea()
         {
+            if (!_shown)
+            {
+                return;
+            }
+            
             if (DwmIsCompositionEnabled(out bool compositionEnabled) < 0 || !compositionEnabled)
             {
                 _isClientAreaExtended = false;
@@ -916,6 +923,13 @@ namespace Avalonia.Win32
 
         private void ShowWindow(WindowState state, bool activate)
         {
+            _shown = true;
+            
+            if (_isClientAreaExtended)
+            {
+                ExtendClientArea();
+            }
+            
             ShowWindowCommand? command;
 
             var newWindowProperties = _windowProperties;

+ 31 - 0
tests/Avalonia.Animation.UnitTests/AnimatableTests.cs

@@ -330,6 +330,37 @@ namespace Avalonia.Animation.UnitTests
             }
         }
 
+        [Fact]
+        public void Transitions_Can_Be_Changed_To_Collection_That_Contains_The_Same_Transitions()
+        {
+            var target = CreateTarget();
+            var control = CreateControl(target.Object);
+
+            control.Transitions = new Transitions { target.Object };
+        }
+
+        [Fact]
+        public void Transitions_Can_Re_Set_During_Batch_Update()
+        {
+            var target = CreateTarget();
+            var control = CreateControl(target.Object);
+
+            // Assigning and then clearing Transitions ensures we have a transition state
+            // collection created.
+            control.Transitions = null;
+
+            control.BeginBatchUpdate();
+
+            // Setting opacity then Transitions means that we receive the Transitions change
+            // after the Opacity change when EndBatchUpdate is called.
+            control.Opacity = 0.5;
+            control.Transitions = new Transitions { target.Object };
+
+            // Which means that the transition state hasn't been initialized with the new
+            // Transitions when the Opacity change notification gets raised here.
+            control.EndBatchUpdate();
+        }
+
         private static Mock<ITransition> CreateTarget()
         {
             return CreateTransition(Visual.OpacityProperty);

+ 122 - 0
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs

@@ -53,6 +53,21 @@ namespace Avalonia.Base.UnitTests
             Assert.Empty(raised);
         }
 
+        [Fact]
+        public void Binding_Disposal_Should_Not_Raise_Property_Changes_During_Batch_Update()
+        {
+            var target = new TestClass();
+            var observable = new TestObservable<string>("foo");
+            var raised = new List<string>();
+
+            var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
+            target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
+            target.BeginBatchUpdate();
+            sub.Dispose();
+
+            Assert.Empty(raised);
+        }
+
         [Fact]
         public void SetValue_Change_Should_Be_Raised_After_Batch_Update_1()
         {
@@ -240,6 +255,27 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal(BindingPriority.Unset, raised[0].Priority);
         }
 
+        [Fact]
+        public void Binding_Disposal_Should_Be_Raised_After_Batch_Update()
+        {
+            var target = new TestClass();
+            var observable = new TestObservable<string>("foo");
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            sub.Dispose();
+            target.EndBatchUpdate();
+
+            Assert.Equal(1, raised.Count);
+            Assert.Null(target.Foo);
+            Assert.Equal("foo", raised[0].OldValue);
+            Assert.Null(raised[0].NewValue);
+            Assert.Equal(BindingPriority.Unset, raised[0].Priority);
+        }
+
         [Fact]
         public void ClearValue_Change_Should_Be_Raised_After_Batch_Update_1()
         {
@@ -449,6 +485,92 @@ namespace Avalonia.Base.UnitTests
             Assert.Null(raised[1].NewValue);
         }
 
+        [Fact]
+        public void Can_Set_Cleared_Value_When_Ending_Batch_Update()
+        {
+            var target = new TestClass();
+            var raised = 0;
+
+            target.Foo = "foo";
+
+            target.BeginBatchUpdate();
+            target.ClearValue(TestClass.FooProperty);
+            target.PropertyChanged += (sender, e) =>
+            {
+                if (e.Property == TestClass.FooProperty && e.NewValue is null)
+                {
+                    target.Foo = "bar";
+                    ++raised;
+                }
+            };
+            target.EndBatchUpdate();
+
+            Assert.Equal("bar", target.Foo);
+            Assert.Equal(1, raised);
+        }
+
+        [Fact]
+        public void Can_Bind_Cleared_Value_When_Ending_Batch_Update()
+        {
+            var target = new TestClass();
+            var raised = 0;
+            var notifications = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.Foo = "foo";
+
+            target.BeginBatchUpdate();
+            target.ClearValue(TestClass.FooProperty);
+            target.PropertyChanged += (sender, e) =>
+            {
+                if (e.Property == TestClass.FooProperty && e.NewValue is null)
+                {
+                    target.Bind(TestClass.FooProperty, new TestObservable<string>("bar"));
+                    ++raised;
+                }
+
+                notifications.Add(e);
+            };
+            target.EndBatchUpdate();
+
+            Assert.Equal("bar", target.Foo);
+            Assert.Equal(1, raised);
+            Assert.Equal(2, notifications.Count);
+            Assert.Equal(null, notifications[0].NewValue);
+            Assert.Equal("bar", notifications[1].NewValue);
+        }
+
+        [Fact]
+        public void Can_Bind_Completed_Binding_Back_To_Original_Value_When_Ending_Batch_Update()
+        {
+            var target = new TestClass();
+            var raised = 0;
+            var notifications = new List<AvaloniaPropertyChangedEventArgs>();
+            var observable1 = new TestObservable<string>("foo");
+            var observable2 = new TestObservable<string>("foo");
+
+            target.Bind(TestClass.FooProperty, observable1);
+
+            target.BeginBatchUpdate();
+            observable1.OnCompleted();
+            target.PropertyChanged += (sender, e) =>
+            {
+                if (e.Property == TestClass.FooProperty && e.NewValue is null)
+                {
+                    target.Bind(TestClass.FooProperty, observable2);
+                    ++raised;
+                }
+
+                notifications.Add(e);
+            };
+            target.EndBatchUpdate();
+
+            Assert.Equal("foo", target.Foo);
+            Assert.Equal(1, raised);
+            Assert.Equal(2, notifications.Count);
+            Assert.Equal(null, notifications[0].NewValue);
+            Assert.Equal("foo", notifications[1].NewValue);
+        }
+
         public class TestClass : AvaloniaObject
         {
             public static readonly StyledProperty<string> FooProperty =

+ 2 - 2
tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs

@@ -64,9 +64,9 @@ namespace Avalonia.Benchmarks.Themes
             AssetLoader.RegisterResUriParsers();
             return new Styles
             {
-                new StyleInclude(new Uri("avares://Avalonia.Benchmarks"))
+                new Avalonia.Themes.Fluent.FluentTheme(new Uri("avares://Avalonia.Benchmarks"))
                 {
-                    Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentDark.xaml")
+
                 }
             };
         }

+ 32 - 0
tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs

@@ -14,6 +14,8 @@ using Avalonia.UnitTests;
 using Moq;
 using Xunit;
 using System.Collections.ObjectModel;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
 
 namespace Avalonia.Controls.UnitTests
 {
@@ -396,6 +398,36 @@ namespace Avalonia.Controls.UnitTests
                 Assert.Equal(control.Text, control.ItemSelector(input, selectedItem));
             });
         }
+        
+        [Fact]
+        public void Text_Validation()
+        {
+            RunTest((control, textbox) =>
+            {
+                var exception = new InvalidCastException("failed validation");
+                var textObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
+                control.Bind(AutoCompleteBox.TextProperty, textObservable);
+                Dispatcher.UIThread.RunJobs();
+
+                Assert.Equal(DataValidationErrors.GetHasErrors(control), true);
+                Assert.Equal(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception }), true);
+            });
+        }
+        
+        [Fact]
+        public void SelectedItem_Validation()
+        {
+            RunTest((control, textbox) =>
+            {
+                var exception = new InvalidCastException("failed validation");
+                var itemObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
+                control.Bind(AutoCompleteBox.SelectedItemProperty, itemObservable);
+                Dispatcher.UIThread.RunJobs();
+
+                Assert.Equal(DataValidationErrors.GetHasErrors(control), true);
+                Assert.Equal(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception }), true);
+            });
+        }
 
         /// <summary>
         /// Retrieves a defined predicate filter through a new AutoCompleteBox 

+ 46 - 18
tests/Avalonia.Controls.UnitTests/CarouselTests.cs

@@ -1,10 +1,14 @@
 using System.Collections.ObjectModel;
 using System.Linq;
+using System.Reactive.Subjects;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
+using Avalonia.Data;
 using Avalonia.LogicalTree;
+using Avalonia.Threading;
 using Avalonia.VisualTree;
+using Avalonia.UnitTests;
 using Xunit;
 
 namespace Avalonia.Controls.UnitTests
@@ -77,9 +81,9 @@ namespace Avalonia.Controls.UnitTests
         {
             var items = new ObservableCollection<string>
             {
-               "Foo",
-               "Bar",
-               "FooBar"
+                "Foo",
+                "Bar",
+                "FooBar"
             };
 
             var target = new Carousel
@@ -113,9 +117,9 @@ namespace Avalonia.Controls.UnitTests
         {
             var items = new ObservableCollection<string>
             {
-               "Foo",
-               "Bar",
-               "FooBar"
+                "Foo",
+                "Bar",
+                "FooBar"
             };
 
             var target = new Carousel
@@ -172,9 +176,9 @@ namespace Avalonia.Controls.UnitTests
         {
             var items = new ObservableCollection<string>
             {
-               "Foo",
-               "Bar",
-               "FooBar"
+                "Foo",
+                "Bar",
+                "FooBar"
             };
 
             var target = new Carousel
@@ -206,9 +210,9 @@ namespace Avalonia.Controls.UnitTests
         {
             var items = new ObservableCollection<string>
             {
-               "Foo",
-               "Bar",
-               "FooBar"
+                "Foo",
+                "Bar",
+                "FooBar"
             };
 
             var target = new Carousel
@@ -235,9 +239,9 @@ namespace Avalonia.Controls.UnitTests
         {
             var items = new ObservableCollection<string>
             {
-               "Foo",
-               "Bar",
-               "FooBar"
+                "Foo",
+                "Bar",
+                "FooBar"
             };
 
             var target = new Carousel
@@ -269,9 +273,9 @@ namespace Avalonia.Controls.UnitTests
         {
             var items = new ObservableCollection<string>
             {
-               "Foo",
-               "Bar",
-               "FooBar"
+                "Foo",
+                "Bar",
+                "FooBar"
             };
 
             var target = new Carousel
@@ -311,5 +315,29 @@ namespace Avalonia.Controls.UnitTests
             contentPresenter.UpdateChild();
             return Assert.IsType<TextBlock>(contentPresenter.Child);
         }
+
+        [Fact]
+        public void SelectedItem_Validation()
+        {
+            using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
+            {
+                var target = new Carousel
+                {
+                    Template = new FuncControlTemplate<Carousel>(CreateTemplate), IsVirtualized = false
+                };
+
+                target.ApplyTemplate();
+                target.Presenter.ApplyTemplate();
+
+                var exception = new System.InvalidCastException("failed validation");
+                var textObservable =
+                    new BehaviorSubject<BindingNotification>(new BindingNotification(exception,
+                        BindingErrorType.DataValidationError));
+                target.Bind(ComboBox.SelectedItemProperty, textObservable);
+
+                Assert.True(DataValidationErrors.GetHasErrors(target));
+                Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
+            }
+        }
     }
 }

+ 29 - 0
tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

@@ -1,11 +1,14 @@
 using System.Linq;
+using System.Reactive.Subjects;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Shapes;
 using Avalonia.Controls.Templates;
+using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
+using Avalonia.Threading;
 using Avalonia.UnitTests;
 using Xunit;
 
@@ -173,5 +176,31 @@ namespace Avalonia.Controls.UnitTests
                 Assert.Equal(expectedSelectedIndex, target.SelectedIndex);
             }
         }
+        
+        [Fact]
+        public void SelectedItem_Validation()
+        {
+
+            using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
+            {
+                var target = new ComboBox
+                {
+                    Template = GetTemplate(),
+                    VirtualizationMode =  ItemVirtualizationMode.None
+                };
+
+                target.ApplyTemplate();
+                target.Presenter.ApplyTemplate();
+                
+                var exception = new System.InvalidCastException("failed validation");
+                var textObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
+                target.Bind(ComboBox.SelectedItemProperty, textObservable);
+
+                Assert.True(DataValidationErrors.GetHasErrors(target));
+                Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
+                
+            }
+            
+        } 
     }
 }

+ 26 - 0
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@@ -1,11 +1,14 @@
 using System.Linq;
+using System.Reactive.Subjects;
 using Avalonia.Collections;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
+using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.LogicalTree;
 using Avalonia.Styling;
+using Avalonia.Threading;
 using Avalonia.UnitTests;
 using Avalonia.VisualTree;
 using Xunit;
@@ -559,5 +562,28 @@ namespace Avalonia.Controls.UnitTests
 
             public string Value { get; }
         }
+
+
+        [Fact]
+        public void SelectedItem_Validation()
+        {
+            var target = new ListBox
+            {
+                Template = ListBoxTemplate(),
+                Items = new[] { "Foo" },
+                ItemTemplate = new FuncDataTemplate<string>((_, __) => new Canvas()),
+                SelectionMode = SelectionMode.AlwaysSelected,
+                VirtualizationMode = ItemVirtualizationMode.None
+            };
+
+            Prepare(target);
+            
+            var exception = new System.InvalidCastException("failed validation");
+            var textObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
+            target.Bind(ComboBox.SelectedItemProperty, textObservable);
+                
+            Assert.True(DataValidationErrors.GetHasErrors(target));
+            Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
+        }
     }
 }

+ 95 - 0
tests/Avalonia.Controls.UnitTests/NumericUpDownTests.cs

@@ -0,0 +1,95 @@
+using System;
+using System.Linq;
+using System.Reactive.Subjects;
+using Avalonia.Controls.Templates;
+using Avalonia.Data;
+using Avalonia.Threading;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests
+{
+    public class NumericUpDownTests
+    {
+        private static TestServices Services => TestServices.StyledWindow;
+
+        [Fact]
+        public void Text_Validation()
+        {
+            RunTest((control, textbox) =>
+            {
+                var exception = new InvalidCastException("failed validation");
+                var textObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
+                control.Bind(NumericUpDown.TextProperty, textObservable);
+                Dispatcher.UIThread.RunJobs();
+
+                Assert.True(DataValidationErrors.GetHasErrors(control));
+                Assert.True(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception }));
+            });
+        }
+
+        [Fact]
+        public void Value_Validation()
+        {
+            RunTest((control, textbox) =>
+            {
+                var exception = new InvalidCastException("failed validation");
+                var valueObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
+                control.Bind(NumericUpDown.ValueProperty, valueObservable);
+                Dispatcher.UIThread.RunJobs();
+
+                Assert.True(DataValidationErrors.GetHasErrors(control));
+                Assert.True(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception }));
+            });
+        }
+
+        private void RunTest(Action<NumericUpDown, TextBox> test)
+        {
+            using (UnitTestApplication.Start(Services))
+            {
+                var control = CreateControl();
+                TextBox textBox = GetTextBox(control);
+                var window = new Window { Content = control };
+                window.ApplyTemplate();
+                window.Presenter.ApplyTemplate();
+                Dispatcher.UIThread.RunJobs();
+                test.Invoke(control, textBox);
+            }
+        }
+
+        private NumericUpDown CreateControl()
+        {
+            var control = new NumericUpDown
+            {
+                Template = CreateTemplate()
+            };
+
+            control.ApplyTemplate();
+            return control;
+        }
+        private TextBox GetTextBox(NumericUpDown control)
+        {
+            return control.GetTemplateChildren()
+                          .OfType<ButtonSpinner>()
+                          .Select(b => b.Content)
+                          .OfType<TextBox>()
+                          .First();
+        }
+        private IControlTemplate CreateTemplate()
+        {
+            return new FuncControlTemplate<NumericUpDown>((control, scope) =>
+            {
+                var textBox =
+                    new TextBox
+                    {
+                        Name = "PART_TextBox"
+                    }.RegisterInNameScope(scope);
+                return new ButtonSpinner
+                    {
+                        Name = "PART_Spinner",
+                        Content = textBox,
+                    }.RegisterInNameScope(scope);
+            });
+        }
+    }
+}

+ 50 - 12
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@@ -646,22 +646,49 @@ namespace Avalonia.Controls.UnitTests
                 Assert.Null(target.Text);
             }
         }
-
-        [Fact]
-        public void Text_Box_MaxLength_Work_Properly()
+        
+        [Theory]
+        [InlineData("abc", "d", 3, 0, 0, false, "abc")]
+        [InlineData("abc", "dd", 4, 3, 3, false, "abcd")]
+        [InlineData("abc", "ddd", 3, 0, 2, true, "ddc")]
+        [InlineData("abc", "dddd", 4, 1, 3, true, "addd")]
+        [InlineData("abc", "ddddd", 5, 3, 3, true, "abcdd")]
+        public void MaxLength_Works_Properly(
+            string initalText,
+            string textInput,
+            int maxLength,
+            int selectionStart,
+            int selectionEnd,
+            bool fromClipboard,
+            string expected)
         {
             using (UnitTestApplication.Start(Services))
             {
                 var target = new TextBox
                 {
                     Template = CreateTemplate(),
-                    Text = "abc",
-                    MaxLength = 3,
+                    Text = initalText,
+                    MaxLength = maxLength,
+                    SelectionStart = selectionStart,
+                    SelectionEnd = selectionEnd
                 };
-
-                RaiseKeyEvent(target, Key.D, KeyModifiers.None);
-
-                Assert.Equal("abc", target.Text);
+                
+                if (fromClipboard)
+                {
+                    AvaloniaLocator.CurrentMutable.Bind<IClipboard>().ToSingleton<ClipboardStub>();
+                    
+                    var clipboard = AvaloniaLocator.CurrentMutable.GetService<IClipboard>();
+                    clipboard.SetTextAsync(textInput).GetAwaiter().GetResult();
+                    
+                    RaiseKeyEvent(target, Key.V, KeyModifiers.Control);
+                    clipboard.ClearAsync().GetAwaiter().GetResult();
+                }
+                else
+                {
+                    RaiseTextEvent(target, textInput);
+                }
+                
+                Assert.Equal(expected, target.Text);
             }
         }
 
@@ -758,11 +785,22 @@ namespace Avalonia.Controls.UnitTests
 
         private class ClipboardStub : IClipboard // in order to get tests working that use the clipboard
         {
-            public Task<string> GetTextAsync() => Task.FromResult("");
+            private string _text;
+
+            public Task<string> GetTextAsync() => Task.FromResult(_text);
 
-            public Task SetTextAsync(string text) => Task.CompletedTask;
+            public Task SetTextAsync(string text)
+            {
+                _text = text;
+                return Task.CompletedTask;
+            }
 
-            public Task ClearAsync() => Task.CompletedTask;
+            public Task ClearAsync()
+            {
+                _text = null;
+                return Task.CompletedTask;
+            }
+            
             public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask;
 
             public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());

+ 25 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs

@@ -7,6 +7,31 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
 {
     public class DataTemplateTests : XamlTestBase
     {
+        [Fact]
+        public void DataTemplate_Can_Be_Empty()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:sys='clr-namespace:System;assembly=netstandard'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.DataTemplates>
+        <DataTemplate DataType='{x:Type sys:String}' />
+    </Window.DataTemplates>
+    <ContentControl Name='target' Content='Foo'/>
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var target = window.FindControl<ContentControl>("target");
+
+                window.ApplyTemplate();
+                target.ApplyTemplate();
+                ((ContentPresenter)target.Presenter).UpdateChild();
+
+                Assert.Null(target.Presenter.Child);
+            }
+        }
+
         [Fact]
         public void DataTemplate_Can_Contain_Name()
         {