Explorar o código

Merge remote-tracking branch 'origin/master' into glcontrol

# Conflicts:
#	src/Avalonia.Native/GlPlatformFeature.cs
#	src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
Nikita Tsukanov %!s(int64=5) %!d(string=hai) anos
pai
achega
8d3b418bee
Modificáronse 100 ficheiros con 7303 adicións e 2610 borrados
  1. 27 0
      Avalonia.sln
  2. 0 1
      build/CoreLibraries.props
  3. 1 1
      build/SharedVersion.props
  4. 11 17
      native/Avalonia.Native/inc/avalonia-native.h
  5. 13 0
      native/Avalonia.Native/inc/comimpl.h
  6. 12 0
      native/Avalonia.Native/inc/rendertarget.h
  7. 16 4
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  8. 166 0
      native/Avalonia.Native/src/OSX/cgl.mm
  9. 3 4
      native/Avalonia.Native/src/OSX/common.h
  10. 0 261
      native/Avalonia.Native/src/OSX/gl.mm
  11. 6 6
      native/Avalonia.Native/src/OSX/main.mm
  12. 5 2
      native/Avalonia.Native/src/OSX/platformthreading.mm
  13. 284 0
      native/Avalonia.Native/src/OSX/rendertarget.mm
  14. 59 158
      native/Avalonia.Native/src/OSX/window.mm
  15. 1 0
      nukebuild/Build.cs
  16. 6 2
      readme.md
  17. 1 0
      samples/BindingDemo/BindingDemo.csproj
  18. 1 0
      samples/ControlCatalog/ControlCatalog.csproj
  19. 6 1
      samples/ControlCatalog/MainView.xaml
  20. 46 39
      samples/ControlCatalog/Pages/ImagePage.xaml
  21. 20 19
      samples/ControlCatalog/Pages/ImagePage.xaml.cs
  22. 6 2
      samples/ControlCatalog/Pages/ProgressBarPage.xaml
  23. 134 0
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  24. 18 0
      samples/ControlCatalog/Pages/TextBlockPage.xaml.cs
  25. 1 1
      samples/RenderDemo/Pages/RenderTargetBitmapPage.cs
  26. 1 0
      samples/RenderDemo/RenderDemo.csproj
  27. 1 0
      samples/VirtualizationDemo/VirtualizationDemo.csproj
  28. 0 64
      scripts/avalonia-rename.ps1
  29. 15 11
      src/Avalonia.Animation/Animatable.cs
  30. 1 1
      src/Avalonia.Animation/IterationCount.cs
  31. 5 3
      src/Avalonia.Base/AttachedProperty.cs
  32. 364 360
      src/Avalonia.Base/AvaloniaObject.cs
  33. 330 5
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  34. 74 74
      src/Avalonia.Base/AvaloniaProperty.cs
  35. 17 23
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
  36. 67 0
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs
  37. 97 66
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  38. 27 1
      src/Avalonia.Base/AvaloniaProperty`1.cs
  39. 0 28
      src/Avalonia.Base/BoxedValue.cs
  40. 20 0
      src/Avalonia.Base/Collections/AvaloniaList.cs
  41. 40 0
      src/Avalonia.Base/Collections/Pooled/ClearMode.cs
  42. 31 0
      src/Avalonia.Base/Collections/Pooled/ICollectionDebugView.cs
  43. 21 0
      src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs
  44. 1531 0
      src/Avalonia.Base/Collections/Pooled/PooledList.cs
  45. 699 0
      src/Avalonia.Base/Collections/Pooled/PooledStack.cs
  46. 28 0
      src/Avalonia.Base/Collections/Pooled/StackDebugView.cs
  47. 691 0
      src/Avalonia.Base/Collections/Pooled/ThrowHelper.cs
  48. 26 0
      src/Avalonia.Base/Data/BindingNotification.cs
  49. 17 2
      src/Avalonia.Base/Data/BindingOperations.cs
  50. 432 0
      src/Avalonia.Base/Data/BindingValue.cs
  51. 152 0
      src/Avalonia.Base/Data/Optional.cs
  52. 2 29
      src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs
  53. 78 10
      src/Avalonia.Base/DirectProperty.cs
  54. 153 0
      src/Avalonia.Base/DirectPropertyBase.cs
  55. 63 18
      src/Avalonia.Base/IAvaloniaObject.cs
  56. 0 51
      src/Avalonia.Base/IPriorityValueOwner.cs
  57. 0 9
      src/Avalonia.Base/IStyledPropertyAccessor.cs
  58. 1 6
      src/Avalonia.Base/IStyledPropertyMetadata.cs
  59. 0 160
      src/Avalonia.Base/PriorityBindingEntry.cs
  60. 0 227
      src/Avalonia.Base/PriorityLevel.cs
  61. 0 315
      src/Avalonia.Base/PriorityValue.cs
  62. 107 0
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  63. 33 0
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  64. 25 0
      src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs
  65. 24 0
      src/Avalonia.Base/PropertyStore/IValue.cs
  66. 23 0
      src/Avalonia.Base/PropertyStore/IValueSink.cs
  67. 22 0
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  68. 195 0
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  69. 76 0
      src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs
  70. 16 2
      src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs
  71. 61 0
      src/Avalonia.Base/Reactive/BindingValueAdapter.cs
  72. 35 0
      src/Avalonia.Base/Reactive/BindingValueExtensions.cs
  73. 63 0
      src/Avalonia.Base/Reactive/TypedBindingAdapter.cs
  74. 57 0
      src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs
  75. 3 1
      src/Avalonia.Base/StyledProperty.cs
  76. 96 29
      src/Avalonia.Base/StyledPropertyBase.cs
  77. 16 29
      src/Avalonia.Base/StyledPropertyMetadata`1.cs
  78. 21 0
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  79. 0 128
      src/Avalonia.Base/Utilities/DeferredSetter.cs
  80. 36 16
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  81. 94 0
      src/Avalonia.Base/Utilities/ValueSingleOrList.cs
  82. 7 2
      src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs
  83. 203 132
      src/Avalonia.Base/ValueStore.cs
  84. 1 1
      src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs
  85. 48 106
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  86. 55 51
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  87. 3 17
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  88. 1 1
      src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs
  89. 1 1
      src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs
  90. 18 15
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  91. 8 7
      src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs
  92. 36 3
      src/Avalonia.Controls/Application.cs
  93. 8 29
      src/Avalonia.Controls/AutoCompleteBox.cs
  94. 28 5
      src/Avalonia.Controls/Button.cs
  95. 26 2
      src/Avalonia.Controls/ButtonSpinner.cs
  96. 4 14
      src/Avalonia.Controls/Calendar/Calendar.cs
  97. 3 3
      src/Avalonia.Controls/Calendar/CalendarExtensions.cs
  98. 7 7
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  99. 14 26
      src/Avalonia.Controls/Calendar/DatePicker.cs
  100. 2 2
      src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs

+ 27 - 0
Avalonia.sln

@@ -204,6 +204,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Ava
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}"
+EndProject
 Global
 	GlobalSection(SharedMSBuildProjectFiles) = preSolution
 		src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
@@ -1895,6 +1897,30 @@ Global
 		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhone.Build.0 = Release|Any CPU
 		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
 		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|iPhone.Build.0 = Debug|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|iPhone.Build.0 = Debug|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|Any CPU.Build.0 = Release|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhone.ActiveCfg = Release|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhone.Build.0 = Release|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -1951,6 +1977,7 @@ Global
 		{41B02319-965D-4945-8005-C1A3D1224165} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
 		{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
+		{351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

+ 0 - 1
build/CoreLibraries.props

@@ -4,7 +4,6 @@
       <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Animation/Avalonia.Animation.csproj" />
       <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Controls/Avalonia.Controls.csproj" />
       <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj" />
-      <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj" />
       <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Input/Avalonia.Input.csproj" />
       <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Interactivity/Avalonia.Interactivity.csproj" />
       <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Layout/Avalonia.Layout.csproj" />

+ 1 - 1
build/SharedVersion.props

@@ -3,7 +3,7 @@
   <PropertyGroup>
     <Product>Avalonia</Product>
     <Version>0.9.999</Version>
-    <Copyright>Copyright 2019 &#169; The AvaloniaUI Project</Copyright>
+    <Copyright>Copyright 2020 &#169; The AvaloniaUI Project</Copyright>
     <PackageProjectUrl>https://avaloniaui.net</PackageProjectUrl>
     <RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>

+ 11 - 17
native/Avalonia.Native/inc/avalonia-native.h

@@ -177,14 +177,14 @@ AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown
 public:
     virtual HRESULT Initialize() = 0;
     virtual IAvnMacOptions* GetMacOptions() = 0;
-    virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnWindow** ppv) = 0;
-    virtual HRESULT CreatePopup (IAvnWindowEvents* cb, IAvnPopup** ppv) = 0;
+    virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv) = 0;
+    virtual HRESULT CreatePopup (IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv) = 0;
     virtual HRESULT CreatePlatformThreadingInterface(IAvnPlatformThreadingInterface** ppv) = 0;
     virtual HRESULT CreateSystemDialogs (IAvnSystemDialogs** ppv) = 0;
     virtual HRESULT CreateScreens (IAvnScreens** ppv) = 0;
     virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0;
     virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0;
-    virtual HRESULT ObtainGlFeature(IAvnGlFeature** ppv) = 0;
+    virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) = 0;
     virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) = 0;
     virtual HRESULT SetAppMenu(IAvnAppMenu* menu) = 0;
     virtual HRESULT CreateMenu (IAvnAppMenu** ppv) = 0;
@@ -219,15 +219,12 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown
     virtual HRESULT SetTopMost (bool value) = 0;
     virtual HRESULT SetCursor(IAvnCursor* cursor) = 0;
     virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ret) = 0;
-    virtual HRESULT GetSoftwareFramebuffer(AvnFramebuffer*ret) = 0;
     virtual HRESULT SetMainMenu(IAvnAppMenu* menu) = 0;
     virtual HRESULT ObtainMainMenu(IAvnAppMenu** retOut) = 0;
     virtual HRESULT ObtainNSWindowHandle(void** retOut) = 0;
     virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0;
     virtual HRESULT ObtainNSViewHandle(void** retOut) = 0;
     virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0;
-    virtual bool TryLock() = 0;
-    virtual void Unlock() = 0;
 };
 
 AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase
@@ -360,24 +357,21 @@ AVNCOM(IAvnCursorFactory, 11) : IUnknown
     virtual HRESULT GetCursor (AvnStandardCursorType cursorType, IAvnCursor** retOut) = 0;
 };
 
-
-AVNCOM(IAvnGlFeature, 12) : IUnknown
-{
-    virtual HRESULT ObtainDisplay(IAvnGlDisplay**retOut) = 0;
-    virtual HRESULT ObtainImmediateContext(IAvnGlContext**retOut) = 0;
-};
-
 AVNCOM(IAvnGlDisplay, 13) : IUnknown
 {
-    virtual HRESULT GetSampleCount(int* ret) = 0;
-    virtual HRESULT GetStencilSize(int* ret) = 0;
-    virtual HRESULT ClearContext() = 0;
+    virtual HRESULT CreateContext(IAvnGlContext* share, IAvnGlContext**ppv) = 0;
+    virtual void LegacyClearCurrentContext() = 0;
+    virtual HRESULT WrapContext(void* native, IAvnGlContext**ppv) = 0;
     virtual void* GetProcAddress(char* proc) = 0;
 };
 
 AVNCOM(IAvnGlContext, 14) : IUnknown
 {
-    virtual HRESULT MakeCurrent() = 0;
+    virtual HRESULT MakeCurrent(IUnknown** ppv) = 0;
+    virtual HRESULT LegacyMakeCurrent() = 0;
+    virtual int GetSampleCount() = 0;
+    virtual int GetStencilSize() = 0;
+    virtual void* GetNativeHandle() = 0;
 };
 
 AVNCOM(IAvnGlSurfaceRenderTarget, 15) : IUnknown

+ 13 - 0
native/Avalonia.Native/inc/comimpl.h

@@ -162,6 +162,19 @@ public:
         return _obj;
     }
     
+    TInterface* getRetainedReference()
+    {
+        if(_obj == NULL)
+            return NULL;
+        _obj->AddRef();
+        return _obj;
+    }
+    
+    TInterface** getPPV()
+    {
+        return &_obj;
+    }
+    
     operator TInterface*() const
     {
         return _obj;

+ 12 - 0
native/Avalonia.Native/inc/rendertarget.h

@@ -0,0 +1,12 @@
+
+@protocol IRenderTarget
+-(void) setNewLayer: (CALayer*) layer;
+-(HRESULT) setSwFrame: (AvnFramebuffer*) fb;
+-(void) resize: (AvnPixelSize) size withScale: (float) scale;
+-(AvnPixelSize) pixelSize;
+-(IAvnGlSurfaceRenderTarget*) createSurfaceRenderTarget;
+@end
+
+@interface IOSurfaceRenderTarget : NSObject<IRenderTarget>
+-(IOSurfaceRenderTarget*) initWithOpenGlContext: (IAvnGlContext*) context;
+@end

+ 16 - 4
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj

@@ -8,6 +8,10 @@
 
 /* Begin PBXBuildFile section */
 		1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; };
+		1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */; };
+		1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */; };
+		1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */; };
+		1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */; };
 		37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; };
 		37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; };
 		37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; };
@@ -18,7 +22,6 @@
 		5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; };
 		AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; };
 		AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB1E522B217613570091CD71 /* OpenGL.framework */; };
-		AB573DC4217605E400D389A2 /* gl.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB573DC3217605E400D389A2 /* gl.mm */; };
 		AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB661C1D2148230F00291242 /* AppKit.framework */; };
 		AB661C202148286E00291242 /* window.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB661C1F2148286E00291242 /* window.mm */; };
 		AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */; };
@@ -26,6 +29,10 @@
 
 /* Begin PBXFileReference section */
 		1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = "<group>"; };
+		1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = rendertarget.mm; sourceTree = "<group>"; };
+		1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOSurface.framework; path = System/Library/Frameworks/IOSurface.framework; sourceTree = SDKROOT; };
+		1A3E5EAD23E9FB1300EDE661 /* cgl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cgl.mm; sourceTree = "<group>"; };
+		1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
 		37155CE3233C00EB0034DCE9 /* menu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = menu.h; sourceTree = "<group>"; };
 		379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = "<group>"; };
 		37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = "<group>"; };
@@ -41,7 +48,6 @@
 		5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = "<group>"; };
 		AB00E4F62147CA920032A60A /* main.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = "<group>"; };
 		AB1E522B217613570091CD71 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; };
-		AB573DC3217605E400D389A2 /* gl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = gl.mm; sourceTree = "<group>"; };
 		AB661C1D2148230F00291242 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; };
 		AB661C1F2148286E00291242 /* window.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = window.mm; sourceTree = "<group>"; };
 		AB661C212148288600291242 /* common.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = common.h; sourceTree = "<group>"; };
@@ -54,6 +60,8 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */,
+				1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */,
 				AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */,
 				AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */,
 			);
@@ -65,6 +73,8 @@
 		AB661C1C2148230E00291242 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */,
+				1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */,
 				AB1E522B217613570091CD71 /* OpenGL.framework */,
 				AB661C1D2148230F00291242 /* AppKit.framework */,
 			);
@@ -78,7 +88,7 @@
 				37DDA9B121933371002E132B /* AvnString.h */,
 				37DDA9AF219330F8002E132B /* AvnString.mm */,
 				37A4E71A2178846A00EACBCD /* headers */,
-				AB573DC3217605E400D389A2 /* gl.mm */,
+				1A3E5EAD23E9FB1300EDE661 /* cgl.mm */,
 				5BF943652167AD1D009CAE35 /* cursor.h */,
 				5B21A981216530F500CEE36E /* cursor.mm */,
 				5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */,
@@ -91,6 +101,7 @@
 				AB00E4F62147CA920032A60A /* main.mm */,
 				37155CE3233C00EB0034DCE9 /* menu.h */,
 				520624B222973F4100C4DCEF /* menu.mm */,
+				1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */,
 				37A517B22159597E00FBA241 /* Screens.mm */,
 				37C09D8721580FE4006A6758 /* SystemDialogs.mm */,
 				AB7A61F02147C815003C5833 /* Products */,
@@ -180,12 +191,13 @@
 				5B21A982216530F500CEE36E /* cursor.mm in Sources */,
 				37DDA9B0219330F8002E132B /* AvnString.mm in Sources */,
 				AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */,
+				1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */,
+				1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */,
 				37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */,
 				520624B322973F4100C4DCEF /* menu.mm in Sources */,
 				37A517B32159597E00FBA241 /* Screens.mm in Sources */,
 				AB00E4F72147CA920032A60A /* main.mm in Sources */,
 				37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */,
-				AB573DC4217605E400D389A2 /* gl.mm in Sources */,
 				AB661C202148286E00291242 /* window.mm in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;

+ 166 - 0
native/Avalonia.Native/src/OSX/cgl.mm

@@ -0,0 +1,166 @@
+#include "common.h"
+#include <dlfcn.h>
+
+static CGLContextObj CreateCglContext(CGLContextObj share)
+{
+    int attributes[] = {
+        kCGLPFAAccelerated,
+        kCGLPFAOpenGLProfile, (CGLPixelFormatAttribute)kCGLOGLPVersion_3_2_Core,
+        kCGLPFADepthSize, 8,
+        kCGLPFAStencilSize, 8,
+        kCGLPFAColorSize, 32,
+        0
+    };
+    
+    CGLPixelFormatObj pix;
+    CGLError errorCode;
+    GLint num; // stores the number of possible pixel formats
+    errorCode = CGLChoosePixelFormat( (CGLPixelFormatAttribute*)attributes, &pix, &num );
+    if(errorCode != 0)
+        return nil;
+    CGLContextObj ctx = nil;
+    errorCode = CGLCreateContext(pix, share, &ctx );
+    CGLDestroyPixelFormat( pix );
+    if(errorCode != 0)
+        return nil;
+    return ctx;
+};
+
+
+
+class AvnGlContext : public virtual ComSingleObject<IAvnGlContext, &IID_IAvnGlContext>
+{
+    // Debug
+    int _usageCount = 0;
+public:
+    CGLContextObj Context;
+    int SampleCount = 0, StencilBits = 0;
+    FORWARD_IUNKNOWN()
+    
+    class SavedGlContext : public virtual ComUnknownObject
+    {
+        CGLContextObj _savedContext;
+        ComPtr<AvnGlContext> _parent;
+    public:
+        SavedGlContext(CGLContextObj saved, AvnGlContext* parent)
+        {
+            _savedContext = saved;
+            _parent = parent;
+            _parent->_usageCount++;
+        }
+        
+        ~SavedGlContext()
+        {
+            if(_parent->Context == CGLGetCurrentContext())
+                CGLSetCurrentContext(_savedContext);
+            _parent->_usageCount--;
+            CGLUnlockContext(_parent->Context);
+        }
+    };
+    
+    AvnGlContext(CGLContextObj context)
+    {
+        Context = context;
+        CGLPixelFormatObj fmt = CGLGetPixelFormat(context);
+        CGLDescribePixelFormat(fmt, 0, kCGLPFASamples, &SampleCount);
+        CGLDescribePixelFormat(fmt, 0, kCGLPFAStencilSize, &StencilBits);
+        
+    }
+    
+    virtual HRESULT LegacyMakeCurrent() override
+    {
+        if(CGLSetCurrentContext(Context) != 0)
+            return E_FAIL;
+        return S_OK;
+    }
+    
+    virtual HRESULT MakeCurrent(IUnknown** ppv) override
+    {
+        CGLContextObj saved = CGLGetCurrentContext();
+        CGLLockContext(Context);
+        if(CGLSetCurrentContext(Context) != 0)
+        {
+            CGLUnlockContext(Context);
+            return E_FAIL;
+        }
+        *ppv = new SavedGlContext(saved, this);
+        
+        return S_OK;
+    }
+    
+    virtual int GetSampleCount() override
+    {
+        return SampleCount;
+    }
+    
+    virtual int GetStencilSize() override
+    {
+        return StencilBits;
+    }
+    
+    virtual void* GetNativeHandle() override
+    {
+        return Context;
+    }
+    
+    ~AvnGlContext()
+    {
+        CGLReleaseContext(Context);
+    }
+};
+
+class AvnGlDisplay : public virtual ComSingleObject<IAvnGlDisplay, &IID_IAvnGlDisplay>
+{
+    void* _libgl;
+    
+public:
+    FORWARD_IUNKNOWN()
+    
+    AvnGlDisplay()
+    {
+        _libgl = dlopen("/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib", RTLD_LAZY);
+    }
+    
+    virtual void* GetProcAddress(char* proc)  override
+    {
+        return dlsym(_libgl, proc);
+    }
+    
+    virtual HRESULT CreateContext(IAvnGlContext* share, IAvnGlContext**ppv) override
+    {
+        CGLContextObj shareContext = nil;
+        if(share != nil)
+        {
+            AvnGlContext* shareCtx = dynamic_cast<AvnGlContext*>(share);
+            if(shareCtx != nil)
+                shareContext = shareCtx->Context;
+        }
+        CGLContextObj ctx = ::CreateCglContext(shareContext);
+        if(ctx == nil)
+            return E_FAIL;
+        *ppv = new AvnGlContext(ctx);
+        return S_OK;
+    }
+    
+    virtual HRESULT WrapContext(void* native, IAvnGlContext**ppv) override
+    {
+        if(native == nil)
+            return E_INVALIDARG;
+        *ppv = new AvnGlContext((CGLContextObj) native);
+        return S_OK;
+    }
+    
+    virtual void LegacyClearCurrentContext() override
+    {
+        CGLSetCurrentContext(nil);
+    }
+};
+
+static IAvnGlDisplay* GlDisplay = new AvnGlDisplay();
+
+
+extern IAvnGlDisplay* GetGlDisplay()
+{
+    return GlDisplay;
+};
+

+ 3 - 4
native/Avalonia.Native/src/OSX/common.h

@@ -11,14 +11,13 @@
 #include <pthread.h>
 
 extern IAvnPlatformThreadingInterface* CreatePlatformThreading();
-extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events);
-extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events);
+extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl);
+extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl);
 extern IAvnSystemDialogs* CreateSystemDialogs();
 extern IAvnScreens* CreateScreens();
 extern IAvnClipboard* CreateClipboard();
 extern IAvnCursorFactory* CreateCursorFactory();
-extern IAvnGlFeature* GetGlFeature();
-extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* view);
+extern IAvnGlDisplay* GetGlDisplay();
 extern IAvnAppMenu* CreateAppMenu();
 extern IAvnAppMenuItem* CreateAppMenuItem();
 extern IAvnAppMenuItem* CreateAppMenuItemSeperator();

+ 0 - 261
native/Avalonia.Native/src/OSX/gl.mm

@@ -1,261 +0,0 @@
-#include "common.h"
-#include <OpenGL/gl.h>
-#include <dlfcn.h>
-#include "window.h"
-
-template <typename T, size_t N> char (&ArrayCounter(T (&a)[N]))[N];
-#define ARRAY_COUNT(a) (sizeof(ArrayCounter(a)))
-
-NSOpenGLPixelFormat* CreateFormat()
-{
-    NSOpenGLPixelFormatAttribute attribs[] =
-    {
-        NSOpenGLPFADoubleBuffer,
-        NSOpenGLPFAColorSize, 32,
-        NSOpenGLPFAStencilSize, 8,
-        NSOpenGLPFADepthSize, 8,
-        0
-    };
-    return [[NSOpenGLPixelFormat alloc] initWithAttributes:attribs];
-}
-
-class AvnGlContext : public virtual ComSingleObject<IAvnGlContext, &IID_IAvnGlContext>
-{
-public:
-    FORWARD_IUNKNOWN()
-    NSOpenGLContext* GlContext;
-    GLuint Framebuffer, RenderBuffer, StencilBuffer;
-    AvnGlContext(NSOpenGLContext* gl, bool offscreen)
-    {
-        Framebuffer = 0;
-        RenderBuffer = 0;
-        StencilBuffer = 0;
-        GlContext = gl;
-        if(offscreen)
-        {
-            [GlContext makeCurrentContext];
-
-            glGenFramebuffersEXT(1, &Framebuffer);
-            glBindFramebufferEXT(GL_FRAMEBUFFER, Framebuffer);
-            glGenRenderbuffersEXT(1, &RenderBuffer);
-            glGenRenderbuffersEXT(1, &StencilBuffer);
-
-            glBindRenderbufferEXT(GL_RENDERBUFFER, StencilBuffer);
-            glFramebufferRenderbufferEXT(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, StencilBuffer);
-            glBindRenderbufferEXT(GL_RENDERBUFFER, RenderBuffer);
-            glFramebufferRenderbufferEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, RenderBuffer);
-        }
-        
-    }
-    
-    
-    virtual HRESULT MakeCurrent()  override
-    {
-        [GlContext makeCurrentContext];/*
-        glBindFramebufferEXT(GL_FRAMEBUFFER, Framebuffer);
-        glBindRenderbufferEXT(GL_RENDERBUFFER, RenderBuffer);
-        glFramebufferRenderbufferEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, RenderBuffer);
-        glBindRenderbufferEXT(GL_RENDERBUFFER, StencilBuffer);
-        glFramebufferRenderbufferEXT(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, StencilBuffer);*/
-        return S_OK;
-    }
-};
-
-class AvnGlDisplay : public virtual ComSingleObject<IAvnGlDisplay, &IID_IAvnGlDisplay>
-{
-    int _sampleCount, _stencilSize;
-    void* _libgl;
-    
-public:
-    FORWARD_IUNKNOWN()
-    
-    AvnGlDisplay(int sampleCount, int stencilSize)
-    {
-        _sampleCount = sampleCount;
-        _stencilSize = stencilSize;
-        _libgl = dlopen("/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib", RTLD_LAZY);
-    }
-    
-    virtual HRESULT GetSampleCount(int* ret)  override
-    {
-        *ret = _sampleCount;
-        return S_OK;
-    }
-    virtual HRESULT GetStencilSize(int* ret) override
-    {
-        *ret = _stencilSize;
-        return S_OK;
-    }
-    
-    virtual HRESULT ClearContext()  override
-    {
-        [NSOpenGLContext clearCurrentContext];
-        return S_OK;
-    }
-    
-    virtual void* GetProcAddress(char* proc)  override
-    {
-        return dlsym(_libgl, proc);
-    }
-};
-
-
-class GlFeature : public virtual ComSingleObject<IAvnGlFeature, &IID_IAvnGlFeature>
-{
-    IAvnGlDisplay* _display;
-    AvnGlContext *_immediate;
-    NSOpenGLContext* _shared;
-public:
-    FORWARD_IUNKNOWN()
-    NSOpenGLPixelFormat* _format;
-    GlFeature(IAvnGlDisplay* display, AvnGlContext* immediate, NSOpenGLPixelFormat* format)
-    {
-        _display = display;
-        _immediate = immediate;
-        _format = format;
-        _shared = [[NSOpenGLContext alloc] initWithFormat:_format shareContext:_immediate->GlContext];
-    }
-    
-    NSOpenGLContext* CreateContext()
-    {
-        return _shared;
-        //return [[NSOpenGLContext alloc] initWithFormat:_format shareContext:nil];
-    }
-    
-    virtual HRESULT ObtainDisplay(IAvnGlDisplay**retOut)  override
-    {
-        *retOut = _display;
-        _display->AddRef();
-        return S_OK;
-    }
-    
-    virtual HRESULT ObtainImmediateContext(IAvnGlContext**retOut)  override
-    {
-        *retOut = _immediate;
-        _immediate->AddRef();
-        return S_OK;
-    }
-};
-
-static GlFeature* Feature;
-
-GlFeature* CreateGlFeature()
-{
-    auto format = CreateFormat();
-    if(format == nil)
-    {
-        NSLog(@"Unable to choose pixel format");
-        return NULL;
-    }
-    
-    auto immediateContext = [[NSOpenGLContext alloc] initWithFormat:format shareContext:nil];
-    if(immediateContext == nil)
-    {
-        NSLog(@"Unable to create NSOpenGLContext");
-        return NULL;
-    }
-
-    int stencilBits = 0, sampleCount = 0;
-    
-    auto fmt = CGLGetPixelFormat([immediateContext CGLContextObj]);
-    CGLDescribePixelFormat(fmt, 0, kCGLPFASamples, &sampleCount);
-    CGLDescribePixelFormat(fmt, 0, kCGLPFAStencilSize, &stencilBits);
-    
-    auto offscreen = new AvnGlContext(immediateContext, true);
-    auto display = new AvnGlDisplay(sampleCount, stencilBits);
-    
-    return new GlFeature(display, offscreen, format);
-}
-
-
-static GlFeature* GetFeature()
-{
-    if(Feature == nil)
-        Feature = CreateGlFeature();
-    return Feature;
-}
-
-extern IAvnGlFeature* GetGlFeature()
-{
-    return GetFeature();
-}
-
-class AvnGlRenderingSession : public ComSingleObject<IAvnGlSurfaceRenderingSession, &IID_IAvnGlSurfaceRenderingSession>
-{
-    AvnView* _view;
-    AvnWindow* _window;
-    NSOpenGLContext* _context;
-public:
-    FORWARD_IUNKNOWN()
-    AvnGlRenderingSession(AvnWindow*window, AvnView* view, NSOpenGLContext* context)
-    {
-        _context = context;
-        _window = window;
-        _view = view;
-    }
-    
-    virtual HRESULT GetPixelSize(AvnPixelSize* ret)  override
-    {
-        *ret = [_view getPixelSize];
-        return S_OK;
-    }
-    virtual HRESULT GetScaling(double* ret)  override
-    {
-        *ret = [_window getScaling];
-        return S_OK;
-    }
-    
-    virtual ~AvnGlRenderingSession()
-    {
-        [_context flushBuffer];
-        [NSOpenGLContext clearCurrentContext];
-        CGLUnlockContext([_context CGLContextObj]);
-        [_view unlockFocus];
-    }
-};
-
-class AvnGlRenderTarget : public ComSingleObject<IAvnGlSurfaceRenderTarget, &IID_IAvnGlSurfaceRenderTarget>
-{
-    NSView* _view;
-    NSWindow* _window;
-    NSOpenGLContext* _context;
-public:
-    FORWARD_IUNKNOWN()
-    AvnGlRenderTarget(NSWindow* window, NSView*view)
-    {
-        _window = window;
-        _view = view;
-        _context = GetFeature()->CreateContext();
-    }
-    
-    virtual HRESULT BeginDrawing(IAvnGlSurfaceRenderingSession** ret)  override
-    {
-        auto f = GetFeature();
-        if(f == NULL)
-            return E_FAIL;
-        
-        @try
-        {
-            if(![_view lockFocusIfCanDraw])
-                return E_ABORT;
-        }
-        @catch(NSException* exception)
-        {
-            return E_ABORT;
-        }
-        
-        
-        auto gl = _context;
-        CGLLockContext([_context CGLContextObj]);
-        [gl setView: _view];
-        [gl update];
-        [gl makeCurrentContext];
-        *ret = new AvnGlRenderingSession(_window, _view, gl);
-        return S_OK;
-    }
-};
-
-extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* view)
-{
-    return new AvnGlRenderTarget(window, view);
-}

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

@@ -174,20 +174,20 @@ public:
         return (IAvnMacOptions*)new MacOptions();
     }
     
-    virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnWindow** ppv)  override
+    virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv)  override
     {
         if(cb == nullptr || ppv == nullptr)
             return E_POINTER;
-        *ppv = CreateAvnWindow(cb);
+        *ppv = CreateAvnWindow(cb, gl);
         return S_OK;
     };
     
-    virtual HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnPopup** ppv) override
+    virtual HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv) override
     {
         if(cb == nullptr || ppv == nullptr)
             return E_POINTER;
         
-        *ppv = CreateAvnPopup(cb);
+        *ppv = CreateAvnPopup(cb, gl);
         return S_OK;
     }
     
@@ -221,9 +221,9 @@ public:
         return S_OK;
     }
     
-    virtual HRESULT ObtainGlFeature(IAvnGlFeature** ppv) override
+    virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) override
     {
-        auto rv = ::GetGlFeature();
+        auto rv = ::GetGlDisplay();
         if(rv == NULL)
             return E_FAIL;
         rv->AddRef();

+ 5 - 2
native/Avalonia.Native/src/OSX/platformthreading.mm

@@ -157,11 +157,14 @@ NSArray<NSString*>* _modes;
 
 -(void) perform
 {
+    ComPtr<IAvnSignaledCallback> cb;
     @synchronized (self) {
         _signaled  = false;
-        if(_parent != NULL && _parent->SignaledCallback != NULL)
-            _parent->SignaledCallback->Signaled(0, false);
+        if(_parent != NULL)
+            cb = _parent->SignaledCallback;
     }
+    if(cb != nullptr)
+        cb->Signaled(0, false);
 }
 
 -(void) setParent:(PlatformThreadingInterface *)parent

+ 284 - 0
native/Avalonia.Native/src/OSX/rendertarget.mm

@@ -0,0 +1,284 @@
+#include "common.h"
+#include "rendertarget.h"
+#import <IOSurface/IOSurface.h>
+#import <IOSurface/IOSurfaceObjC.h>
+
+#include <OpenGL/CGLIOSurface.h>
+#include <OpenGL/OpenGL.h>
+#include <OpenGL/glext.h>
+#include <OpenGL/gl3.h>
+#include <OpenGL/gl3ext.h>
+
+@interface IOSurfaceHolder : NSObject
+@end
+
+@implementation IOSurfaceHolder
+{
+    @public IOSurfaceRef surface;
+    @public AvnPixelSize size;
+    @public float scale;
+    ComPtr<IAvnGlContext> _context;
+    GLuint _framebuffer, _texture, _renderbuffer;
+}
+
+- (IOSurfaceHolder*) initWithSize: (AvnPixelSize) size
+                        withScale: (float)scale
+                withOpenGlContext: (IAvnGlContext*) context
+{
+    long bytesPerRow = IOSurfaceAlignProperty(kIOSurfaceBytesPerRow, size.Width * 4);
+    long allocSize = IOSurfaceAlignProperty(kIOSurfaceAllocSize, size.Height * bytesPerRow);
+    NSDictionary* options = @{
+                              (id)kIOSurfaceWidth: @(size.Width),
+                              (id)kIOSurfaceHeight:  @(size.Height),
+                              (id)kIOSurfacePixelFormat: @((uint)'BGRA'),
+                              (id)kIOSurfaceBytesPerElement: @(4),
+                              (id)kIOSurfaceBytesPerRow: @(bytesPerRow),
+                              (id)kIOSurfaceAllocSize: @(allocSize),
+                              
+                              //(id)kIOSurfaceCacheMode: @(kIOMapWriteCombineCache),
+                              (id)kIOSurfaceElementWidth: @(1),
+                              (id)kIOSurfaceElementHeight: @(1)
+                              };
+    
+    surface = IOSurfaceCreate((CFDictionaryRef)options);
+    self->scale = scale;
+    self->size = size;
+    self->_context = context;
+    return self;
+}
+
+-(HRESULT) prepareForGlRender
+{
+    if(_context == nil)
+        return E_FAIL;
+    if(CGLGetCurrentContext() != _context->GetNativeHandle())
+        return E_FAIL;
+    if(_framebuffer == 0)
+        glGenFramebuffersEXT(1, &_framebuffer);
+    
+    
+    glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, _framebuffer);
+    if(_texture == 0)
+    {
+        glGenTextures(1, &_texture);
+    
+        glBindTexture(GL_TEXTURE_RECTANGLE_EXT, _texture);
+        CGLError res = CGLTexImageIOSurface2D((CGLContextObj)_context->GetNativeHandle(),
+                               GL_TEXTURE_RECTANGLE_EXT, GL_RGBA8,
+                               size.Width, size.Height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, surface, 0);
+        glBindTexture(GL_TEXTURE_RECTANGLE_EXT, 0);
+        
+        if(res != 0)
+        {
+            glDeleteTextures(1, &_texture);
+            _texture = 0;
+            glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
+            return E_FAIL;
+        }
+        glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_RECTANGLE_EXT, _texture, 0);
+    }
+    
+    if(_renderbuffer == 0)
+    {
+        glGenRenderbuffers(1, &_renderbuffer);
+        glBindRenderbuffer(GL_RENDERBUFFER, _renderbuffer);
+        glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, size.Width, size.Height);
+        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _renderbuffer);
+    }
+    
+    return S_OK;
+}
+
+-(void) finishDraw
+{
+    ComPtr<IUnknown> release;
+    _context->MakeCurrent(release.getPPV());
+    glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
+    glFlush();
+}
+
+-(void) dealloc
+{
+    
+    if(_framebuffer != 0)
+    {
+        ComPtr<IUnknown> release;
+        _context->MakeCurrent(release.getPPV());
+        glDeleteFramebuffers(1, &_framebuffer);
+        if(_texture != 0)
+            glDeleteTextures(1, &_texture);
+        if(_renderbuffer != 0)
+            glDeleteRenderbuffers(1, &_renderbuffer);
+    }
+    IOSurfaceDecrementUseCount(surface);
+}
+@end
+
+static IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(IOSurfaceRenderTarget* target);
+
+@implementation IOSurfaceRenderTarget
+{
+    CALayer* _layer;
+    @public IOSurfaceHolder* surface;
+    @public NSObject* lock;
+    ComPtr<IAvnGlContext> _glContext;
+}
+
+- (IOSurfaceRenderTarget*) initWithOpenGlContext: (IAvnGlContext*) context;
+{
+    self = [super init];
+    _glContext = context;
+    lock = [NSObject new];
+    surface  = nil;
+    [self resize:{1,1} withScale: 1];
+    
+    return self;
+}
+
+- (AvnPixelSize) pixelSize {
+    return {1, 1};
+}
+
+- (CALayer *)layer {
+    return _layer;
+}
+
+- (void)resize:(AvnPixelSize)size withScale: (float) scale;{
+    @synchronized (lock) {
+        if(surface == nil
+           || surface->size.Width != size.Width
+           || surface->size.Height != size.Height
+           || surface->scale != scale)
+            surface = [[IOSurfaceHolder alloc] initWithSize:size withScale:scale withOpenGlContext:_glContext.getRaw()];
+    }
+}
+
+- (void)updateLayer {
+    if ([NSThread isMainThread])
+    {
+        @synchronized (lock) {
+            if(_layer == nil)
+                return;
+            [_layer setContents: nil];
+            if(surface != nil)
+            {
+                [_layer setContentsScale: surface->scale];
+                [_layer setContents: (__bridge IOSurface*) surface->surface];
+            }
+        }
+    }
+    else
+        dispatch_async(dispatch_get_main_queue(), ^{
+            [self updateLayer];
+        });
+}
+
+- (void) setNewLayer:(CALayer *)layer {
+    _layer = layer;
+    [self updateLayer];
+}
+
+- (HRESULT)setSwFrame:(AvnFramebuffer *)fb {
+    @synchronized (lock) {
+        if(fb->PixelFormat == AvnPixelFormat::kAvnRgb565)
+            return E_INVALIDARG;
+        if(surface == nil)
+            return E_FAIL;
+        IOSurfaceRef surf = surface->surface;
+        if(IOSurfaceLock(surf, 0, nil))
+            return E_FAIL;
+        size_t w = MIN(fb->Width, IOSurfaceGetWidth(surf));
+        size_t h = MIN(fb->Height, IOSurfaceGetHeight(surf));
+        size_t wbytes = w*4;
+        size_t sstride = IOSurfaceGetBytesPerRow(surf);
+        size_t fstride = fb->Stride;
+        char*pSurface = (char*)IOSurfaceGetBaseAddress(surf);
+        char*pFb = (char*)fb->Data;
+        for(size_t y = 0; y < h; y++)
+        {
+            memcpy(pSurface + y*sstride, pFb + y*fstride, wbytes);
+        }
+        IOSurfaceUnlock(surf, 0, nil);
+        [self updateLayer];
+        return S_OK;
+    }
+}
+
+-(IAvnGlSurfaceRenderTarget*) createSurfaceRenderTarget
+{
+    return CreateGlRenderTarget(self);
+}
+
+@end
+
+class AvnGlRenderingSession : public ComSingleObject<IAvnGlSurfaceRenderingSession, &IID_IAvnGlSurfaceRenderingSession>
+{
+    ComPtr<IUnknown> _releaseContext;
+    IOSurfaceRenderTarget* _target;
+    IOSurfaceHolder* _surface;
+public:
+    FORWARD_IUNKNOWN()
+    AvnGlRenderingSession(IOSurfaceRenderTarget* target, ComPtr<IUnknown> releaseContext)
+    {
+        _target = target;
+        // This happens in a synchronized block set up by AvnRenderTarget, so we take the current surface for this
+        // particular render session
+        _surface = _target->surface;
+        _releaseContext = releaseContext;
+    }
+    
+    virtual HRESULT GetPixelSize(AvnPixelSize* ret)  override
+    {
+        if(!_surface)
+            return E_FAIL;
+        *ret = _surface->size;
+        return S_OK;
+    }
+    
+    virtual HRESULT GetScaling(double* ret)  override
+    {
+        if(!_surface)
+            return E_FAIL;
+        *ret = _surface->scale;
+        return S_OK;
+    }
+    
+    virtual ~AvnGlRenderingSession()
+    {
+        [_surface finishDraw];
+        [_target updateLayer];
+        _releaseContext = nil;
+    }
+};
+
+class AvnGlRenderTarget : public ComSingleObject<IAvnGlSurfaceRenderTarget, &IID_IAvnGlSurfaceRenderTarget>
+{
+    IOSurfaceRenderTarget* _target;
+public:
+    FORWARD_IUNKNOWN()
+    AvnGlRenderTarget(IOSurfaceRenderTarget* target)
+    {
+        _target = target;
+    }
+    
+    virtual HRESULT BeginDrawing(IAvnGlSurfaceRenderingSession** ret)  override
+    {
+        ComPtr<IUnknown> releaseContext;
+        @synchronized (_target->lock) {
+            if(_target->surface == nil)
+                return E_FAIL;
+            _target->_glContext->MakeCurrent(releaseContext.getPPV());
+            HRESULT res = [_target->surface prepareForGlRender];
+            if(res)
+                return res;
+            *ret = new AvnGlRenderingSession(_target, releaseContext);
+            return S_OK;
+        }
+    }
+};
+
+
+static IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(IOSurfaceRenderTarget* target)
+{
+    return new AvnGlRenderTarget(target);
+}

+ 59 - 158
native/Avalonia.Native/src/OSX/window.mm

@@ -7,44 +7,9 @@
 #include "cursor.h"
 #include "menu.h"
 #include <OpenGL/gl.h>
+#include "rendertarget.h"
+
 
-class SoftwareDrawingOperation
-{
-public:
-    void* Data = 0;
-    AvnFramebuffer Desc;
-    void Alloc(NSView* view)
-    {
-        auto logicalSize = [view frame].size;
-        auto pixelSize = [view convertSizeToBacking:logicalSize];
-        int w = pixelSize.width;
-        int h = pixelSize.height;
-        int stride = w * 4;
-        Data = malloc(h * stride);
-        Desc = {
-            .Data = Data,
-            .Stride = stride,
-            .Width = w,
-            .Height = h,
-            .PixelFormat = kAvnRgba8888,
-            .Dpi = AvnVector { .X = w / logicalSize.width * 96, .Y = h / logicalSize.height * 96}
-        };
-    }
-    
-    void Dealloc()
-    {
-        if(Data != NULL)
-        {
-            free(Data);
-            Data = NULL;
-        }
-    }
-    
-    ~SoftwareDrawingOperation()
-    {
-        Dealloc();
-    }
-};
 
 class WindowBaseImpl : public virtual ComSingleObject<IAvnWindowBase, &IID_IAvnWindowBase>, public INSWindowHolder
 {
@@ -61,15 +26,18 @@ public:
     AvnView* View;
     AvnWindow* Window;
     ComPtr<IAvnWindowBaseEvents> BaseEvents;
-    SoftwareDrawingOperation CurrentSwDrawingOperation;
+    ComPtr<IAvnGlContext> _glContext;
+    NSObject<IRenderTarget>* renderTarget;
     AvnPoint lastPositionSet;
     NSString* _lastTitle;
     IAvnAppMenu* _mainMenu;
     
-    WindowBaseImpl(IAvnWindowBaseEvents* events)
+    WindowBaseImpl(IAvnWindowBaseEvents* events, IAvnGlContext* gl)
     {
         _mainMenu = nullptr;
         BaseEvents = events;
+        _glContext = gl;
+        renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext: gl];
         View = [[AvnView alloc] initWithParent:this];
 
         Window = [[AvnWindow alloc] initWithParent:this];
@@ -291,29 +259,6 @@ public:
         return S_OK;
     }
     
-    virtual bool TryLock() override
-    {
-        @autoreleasepool
-        {
-            @try
-            {
-                return [View lockFocusIfCanDraw] == YES;
-            }
-            @catch (NSException*)
-            {
-                return NO;
-            }
-        }
-    }
-    
-    virtual void Unlock() override
-    {
-        @autoreleasepool
-        {
-            [View unlockFocus];
-        }
-    }
-    
     virtual HRESULT BeginMoveDrag () override
     {
         @autoreleasepool
@@ -408,16 +353,6 @@ public:
         return S_OK;
     }
     
-    virtual HRESULT GetSoftwareFramebuffer(AvnFramebuffer*ret) override
-    {
-        if(![[NSThread currentThread] isMainThread])
-            return E_FAIL;
-        if(CurrentSwDrawingOperation.Data == NULL)
-            CurrentSwDrawingOperation.Alloc(View);
-        *ret = CurrentSwDrawingOperation.Desc;
-        return S_OK;
-    }
-    
     virtual HRESULT SetCursor(IAvnCursor* cursor) override
     {
         @autoreleasepool
@@ -451,8 +386,8 @@ public:
     {
         if(View == NULL)
             return E_FAIL;
-        *ppv = ::CreateGlRenderTarget(Window, View);
-        return S_OK;
+        *ppv = [renderTarget createSurfaceRenderTarget];
+        return *ppv == nil ? E_FAIL : S_OK;
     }
 
 protected:
@@ -490,7 +425,7 @@ private:
     }
     
     ComPtr<IAvnWindowEvents> WindowEvents;
-    WindowImpl(IAvnWindowEvents* events) : WindowBaseImpl(events)
+    WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl)
     {
         WindowEvents = events;
         [Window setCanBecomeKeyAndMain];
@@ -731,6 +666,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     NSEvent* _lastMouseDownEvent;
     bool _lastKeyHandled;
     AvnPixelSize _lastPixelSize;
+    NSObject<IRenderTarget>* _renderTarget;
 }
 
 - (void)onClosed
@@ -741,19 +677,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     }
 }
 
-- (BOOL)lockFocusIfCanDraw
-{
-    @synchronized (self)
-    {
-        if(_parent == nullptr)
-        {
-            return NO;
-        }
-    }
-    
-    return [super lockFocusIfCanDraw];
-}
-
 -(AvnPixelSize) getPixelSize
 {
     return _lastPixelSize;
@@ -764,18 +687,43 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     return _lastMouseDownEvent;
 }
 
+- (void) updateRenderTarget
+{
+    [_renderTarget resize:_lastPixelSize withScale: [[self window] backingScaleFactor]];
+    [self setNeedsDisplayInRect:[self frame]];
+}
+
 -(AvnView*)  initWithParent: (WindowBaseImpl*) parent
 {
     self = [super init];
-    [self setWantsBestResolutionOpenGLSurface:true];
+    _renderTarget = parent->renderTarget;
     [self setWantsLayer:YES];
+    [self setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize];
+    
     _parent = parent;
     _area = nullptr;
     _lastPixelSize.Height = 100;
     _lastPixelSize.Width = 100;
+
     return self;
 }
 
+- (BOOL)isFlipped
+{
+    return YES;
+}
+
+- (BOOL)wantsUpdateLayer
+{
+    return YES;
+}
+
+- (void)setLayer:(CALayer *)layer
+{
+    [_renderTarget setNewLayer: layer];
+    [super setLayer: layer];
+}
+
 - (BOOL)isOpaque
 {
     return YES;
@@ -805,7 +753,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
         [self removeTrackingArea:_area];
         _area = nullptr;
     }
-    
+
+    if (_parent == nullptr)
+    {
+        return;
+    }
+
     NSRect rect = NSZeroRect;
     rect.size = newSize;
     
@@ -818,87 +771,32 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     auto fsize = [self convertSizeToBacking: [self frame].size];
     _lastPixelSize.Width = (int)fsize.width;
     _lastPixelSize.Height = (int)fsize.height;
-
+    [self updateRenderTarget];
     _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height});
 }
 
-- (void) drawFb: (AvnFramebuffer*) fb
-{
-    auto colorSpace = CGColorSpaceCreateDeviceRGB();
-    auto dataProvider = CGDataProviderCreateWithData(NULL, fb->Data, fb->Height*fb->Stride, NULL);
-
-    
-    auto image = CGImageCreate(fb->Width, fb->Height, 8, 32, fb->Stride, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast,
-                               dataProvider, nullptr, false, kCGRenderingIntentDefault);
-    
-    auto ctx = [NSGraphicsContext currentContext];
-    
-    [ctx saveGraphicsState];
-    auto cgc = [ctx CGContext];
-    
-    CGContextDrawImage(cgc, CGRect{0,0, fb->Width/(fb->Dpi.X/96), fb->Height/(fb->Dpi.Y/96)}, image);
-    CGImageRelease(image);
-    CGColorSpaceRelease(colorSpace);
-    CGDataProviderRelease(dataProvider);
-    
-    [ctx restoreGraphicsState];
-
-}
 
-- (void)drawRect:(NSRect)dirtyRect
+- (void)updateLayer
 {
     if (_parent == nullptr)
     {
         return;
     }
-        
-    _parent->BaseEvents->RunRenderPriorityJobs();
-    
-    @synchronized (self) {
-        if(_swRenderedFrame != NULL)
-        {
-            [self drawFb: &_swRenderedFrameBuffer];
-            return;
-        }
-    }
     
-    auto swOp = &_parent->CurrentSwDrawingOperation;
+    _parent->BaseEvents->RunRenderPriorityJobs();
     _parent->BaseEvents->Paint();
-    if(swOp->Data != NULL)
-        [self drawFb: &swOp->Desc];
-    
-    swOp->Dealloc();
-    return;
 }
 
--(void) redrawSelf
+- (void)drawRect:(NSRect)dirtyRect
 {
-    @autoreleasepool
-    {
-        @synchronized(self)
-        {
-            if(!_queuedDisplayFromThread)
-                return;
-            _queuedDisplayFromThread = false;
-        }
-        [self setNeedsDisplayInRect:[self frame]];
-        [self display];
-        
-    }
+    return;
 }
 
 -(void) setSwRenderedFrame: (AvnFramebuffer*) fb dispose: (IUnknown*) dispose
 {
     @autoreleasepool {
-        @synchronized (self) {
-            _swRenderedFrame = dispose;
-            _swRenderedFrameBuffer = *fb;
-            if(!_queuedDisplayFromThread)
-            {
-                _queuedDisplayFromThread = true;
-                [self performSelector:@selector(redrawSelf) onThread:[NSThread mainThread] withObject:NULL waitUntilDone:false modes: AllLoopModes];
-            }
-        }
+        [_renderTarget setSwFrame:fb];
+        dispose->Release();
     }
 }
 
@@ -923,7 +821,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     auto fsize = [self convertSizeToBacking: [self frame].size];
     _lastPixelSize.Width = (int)fsize.width;
     _lastPixelSize.Height = (int)fsize.height;
-    
+    [self updateRenderTarget];
     _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]);
     
     [super viewDidChangeBackingProperties];
@@ -1283,6 +1181,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     _closed = false;
     
     _lastScaling = [self backingScaleFactor];
+    [self setOpaque:NO];
+    [self setBackgroundColor: [NSColor clearColor]];
+    [self invalidateShadow];
     return self;
 }
 
@@ -1470,7 +1371,7 @@ private:
     END_INTERFACE_MAP()
     virtual ~PopupImpl(){}
     ComPtr<IAvnWindowEvents> WindowEvents;
-    PopupImpl(IAvnWindowEvents* events) : WindowBaseImpl(events)
+    PopupImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl)
     {
         WindowEvents = events;
         [Window setLevel:NSPopUpMenuWindowLevel];
@@ -1494,20 +1395,20 @@ protected:
     }
 };
 
-extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events)
+extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl)
 {
     @autoreleasepool
     {
-        IAvnPopup* ptr = dynamic_cast<IAvnPopup*>(new PopupImpl(events));
+        IAvnPopup* ptr = dynamic_cast<IAvnPopup*>(new PopupImpl(events, gl));
         return ptr;
     }
 }
 
-extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events)
+extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl)
 {
     @autoreleasepool
     {
-        IAvnWindow* ptr = (IAvnWindow*)new WindowImpl(events);
+        IAvnWindow* ptr = (IAvnWindow*)new WindowImpl(events, gl);
         return ptr;
     }
 }

+ 1 - 0
nukebuild/Build.cs

@@ -191,6 +191,7 @@ partial class Build : NukeBuild
             RunCoreTest("./tests/Avalonia.Animation.UnitTests");
             RunCoreTest("./tests/Avalonia.Base.UnitTests");
             RunCoreTest("./tests/Avalonia.Controls.UnitTests");
+            RunCoreTest("./tests/Avalonia.Controls.DataGrid.UnitTests");
             RunCoreTest("./tests/Avalonia.Input.UnitTests");
             RunCoreTest("./tests/Avalonia.Interactivity.UnitTests");
             RunCoreTest("./tests/Avalonia.Layout.UnitTests");

+ 6 - 2
readme.md

@@ -10,19 +10,23 @@
 
 **Avalonia** is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS and with experimental support for Android and iOS.
 
-**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development. To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239).
+**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and [breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) as we continue along into this project's development. To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239).
 
 | Control catalog | Desktop platforms | Mobile platforms |
 |---|---|---|
 | <a href='https://youtu.be/wHcB3sGLVYg'><img width='300' src='http://avaloniaui.net/images/screen.png'></a> | <a href='https://www.youtube.com/watch?t=28&v=c_AB_XSILp0' target='_blank'><img width='300' src='http://avaloniaui.net/images/avalonia-video.png'></a> | <a href='https://www.youtube.com/watch?v=NJ9-hnmUbBM' target='_blank'><img width='300' src='https://i.ytimg.com/vi/NJ9-hnmUbBM/hqdefault.jpg'></a> |
 
+[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is curated list of awesome Avalonia UI tools, libraries, projects and resources.
+
 ## Getting Started
 
 Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started. After installing it, open "New Project" dialog in Visual Studio, choose "Avalonia" in "Visual C#" section, select "Avalonia .NET Core Application" and press OK (<a href="http://avaloniaui.net/docs/quickstart/images/new-project-dialog.png">screenshot</a>). Now you can write code and markup that will work on multiple platforms!
 
 For those without Visual Studio, a starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core).
 
-Avalonia is delivered via <b>NuGet</b> package manager. You can find the packages here: ([stable(ish)](https://www.nuget.org/packages/Avalonia/), [nightly](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed))
+If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature,  so only **YOU** can make it happen.
+
+Avalonia is delivered via <b>NuGet</b> package manager. You can find the packages here: [stable(ish)](https://www.nuget.org/packages/Avalonia/)
 
 Use these commands in the Package Manager console to install Avalonia manually:
 ```

+ 1 - 0
samples/BindingDemo/BindingDemo.csproj

@@ -4,6 +4,7 @@
     <TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
   </PropertyGroup>
   <ItemGroup>
+    <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
     <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
   </ItemGroup>

+ 1 - 0
samples/ControlCatalog/ControlCatalog.csproj

@@ -23,6 +23,7 @@
 
   <ItemGroup>
     <ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
   </ItemGroup>

+ 6 - 1
samples/ControlCatalog/MainView.xaml

@@ -33,7 +33,11 @@
       <TabItem Header="DatePicker"><pages:DatePickerPage/></TabItem>
       <TabItem Header="Drag+Drop"><pages:DragAndDropPage/></TabItem>
       <TabItem Header="Expander"><pages:ExpanderPage/></TabItem>
-      <TabItem Header="Image"><pages:ImagePage/></TabItem>
+      <TabItem Header="Image"
+               ScrollViewer.VerticalScrollBarVisibility="Disabled"
+               ScrollViewer.HorizontalScrollBarVisibility="Disabled">
+        <pages:ImagePage/>
+      </TabItem>
       <TabItem Header="ItemsRepeater"
                ScrollViewer.VerticalScrollBarVisibility="Disabled"
                ScrollViewer.HorizontalScrollBarVisibility="Disabled">
@@ -51,6 +55,7 @@
       <TabItem Header="TabControl"><pages:TabControlPage/></TabItem>
       <TabItem Header="TabStrip"><pages:TabStripPage/></TabItem>
       <TabItem Header="TextBox"><pages:TextBoxPage/></TabItem>
+      <TabItem Header="TextBlock"><pages:TextBlockPage/></TabItem>
       <TabItem Header="ToolTip"><pages:ToolTipPage/></TabItem>
       <TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>
       <TabItem Header="Viewbox"><pages:ViewboxPage/></TabItem>

+ 46 - 39
samples/ControlCatalog/Pages/ImagePage.xaml

@@ -1,45 +1,52 @@
 <UserControl xmlns="https://github.com/avaloniaui"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              x:Class="ControlCatalog.Pages.ImagePage">
-  <StackPanel Orientation="Vertical" Spacing="4">
-    <TextBlock Classes="h1">Image</TextBlock>
-    <TextBlock Classes="h2">Displays an image</TextBlock>
-
-    <StackPanel Orientation="Horizontal"
-                Margin="0,16,0,0"
-                HorizontalAlignment="Center"
-                Spacing="16">
-      <StackPanel Orientation="Vertical">
-        <TextBlock>No Stretch</TextBlock>
-        <Image Source="/Assets/delicate-arch-896885_640.jpg"
-               Width="100" Height="200"
-               Stretch="None"/>
-      </StackPanel>
-
-      <StackPanel Orientation="Vertical">
-        <TextBlock>Fill</TextBlock>
-        <Image Source="/Assets/delicate-arch-896885_640.jpg"
-               Width="100" Height="200"
-               Stretch="Fill"/>
-      </StackPanel>
+  <DockPanel>
+    <StackPanel DockPanel.Dock="Top" Orientation="Vertical" Spacing="4">
+      <TextBlock Classes="h1">Image</TextBlock>
+      <TextBlock Classes="h2">Displays an image</TextBlock>
+    </StackPanel>
 
-      <StackPanel Orientation="Vertical">
-        <TextBlock>Uniform</TextBlock>
-        <Image Source="/Assets/delicate-arch-896885_640.jpg"
-                Width="100" Height="200"
-                Stretch="Uniform"/>
-      </StackPanel>
+    <Grid ColumnDefinitions="*,*" RowDefinitions="Auto,*" Margin="64">
+      
+      <DockPanel Grid.Column="0" Grid.Row="1" Margin="16">
+        <TextBlock DockPanel.Dock="Top" Classes="h3" Margin="0 8">Bitmap</TextBlock>
+        <ComboBox Name="bitmapStretch" DockPanel.Dock="Top" SelectedIndex="2" SelectionChanged="BitmapStretchChanged">
+          <ComboBoxItem>None</ComboBoxItem>
+          <ComboBoxItem>Fill</ComboBoxItem>
+          <ComboBoxItem>Uniform</ComboBoxItem>
+          <ComboBoxItem>UniformToFill</ComboBoxItem>
+        </ComboBox>
+        <Image Name="bitmapImage"
+               Source="/Assets/delicate-arch-896885_640.jpg"/>
+      </DockPanel>
 
-      <StackPanel Orientation="Vertical">
-        <TextBlock>UniformToFill</TextBlock>
-        <Image Source="/Assets/delicate-arch-896885_640.jpg"
-               Width="100" Height="200"
-               Stretch="UniformToFill"/>
-      </StackPanel>
-    </StackPanel>
-    <StackPanel Orientation="Vertical">
-      <TextBlock>Window Icon as an Image</TextBlock>
-      <Image Name="Icon" Width="100" Height="200" Stretch="None" />
-    </StackPanel>
-  </StackPanel>
+      <DockPanel Grid.Column="1" Grid.Row="1" Margin="16">
+        <TextBlock DockPanel.Dock="Top" Classes="h3" Margin="0 8">Drawing</TextBlock>
+        <ComboBox Name="drawingStretch" DockPanel.Dock="Top" SelectedIndex="2" SelectionChanged="DrawingStretchChanged">
+          <ComboBoxItem>None</ComboBoxItem>
+          <ComboBoxItem>Fill</ComboBoxItem>
+          <ComboBoxItem>Uniform</ComboBoxItem>
+          <ComboBoxItem>UniformToFill</ComboBoxItem>
+        </ComboBox>
+        <Image Name="drawingImage">
+          <Image.Source>
+            <DrawingImage>
+              <GeometryDrawing Brush="Red">
+                <PathGeometry>
+                  <PathFigure StartPoint="0,0" IsClosed="True">
+                    <QuadraticBezierSegment Point1="50,0" Point2="50,-50" />
+                    <QuadraticBezierSegment Point1="100,-50" Point2="100,0" />
+                    <LineSegment Point="50,0" />
+                    <LineSegment Point="50,50" />
+                  </PathFigure>
+                </PathGeometry>
+              </GeometryDrawing>
+            </DrawingImage>
+          </Image.Source>
+        </Image>
+      </DockPanel>
+    </Grid>
+    
+  </DockPanel>
 </UserControl>

+ 20 - 19
samples/ControlCatalog/Pages/ImagePage.xaml.cs

@@ -1,40 +1,41 @@
-using System.IO;
-using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
-using Avalonia.Media.Imaging;
+using Avalonia.Media;
 
 namespace ControlCatalog.Pages
 {
     public class ImagePage : UserControl
     {
-        private Image iconImage;
+        private readonly Image _bitmapImage;
+        private readonly Image _drawingImage;
+
         public ImagePage()
         {
-            this.InitializeComponent();
+            InitializeComponent();
+            _bitmapImage = this.FindControl<Image>("bitmapImage");
+            _drawingImage = this.FindControl<Image>("drawingImage");
         }
 
         private void InitializeComponent()
         {
             AvaloniaXamlLoader.Load(this);
-            iconImage = this.Get<Image>("Icon");
         }
 
-        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        public void BitmapStretchChanged(object sender, SelectionChangedEventArgs e)
+        {
+            if (_bitmapImage != null)
+            {
+                var comboxBox = (ComboBox)sender;
+                _bitmapImage.Stretch = (Stretch)comboxBox.SelectedIndex;
+            }
+        }
+
+        public void DrawingStretchChanged(object sender, SelectionChangedEventArgs e)
         {
-            base.OnAttachedToVisualTree(e);
-            if (iconImage.Source == null)
+            if (_drawingImage != null)
             {
-                var windowRoot = e.Root as Window;
-                if (windowRoot != null)
-                {
-                    using (var stream = new MemoryStream())
-                    {
-                        windowRoot.Icon.Save(stream);
-                        stream.Seek(0, SeekOrigin.Begin);
-                        iconImage.Source = new Bitmap(stream);
-                    }
-                }
+                var comboxBox = (ComboBox)sender;
+                _drawingImage.Stretch = (Stretch)comboxBox.SelectedIndex;
             }
         }
     }

+ 6 - 2
samples/ControlCatalog/Pages/ProgressBarPage.xaml

@@ -6,15 +6,19 @@
     <TextBlock Classes="h2">A progress bar control</TextBlock>
 
     <StackPanel>
+      <CheckBox
+          x:Name="showProgress"
+          Margin="10,16,0,0"
+          Content="Show Progress Text" />
       <StackPanel Orientation="Horizontal"
                   Margin="0,16,0,0"
                   HorizontalAlignment="Center"
                   Spacing="16">
         <StackPanel Spacing="16">
-          <ProgressBar Value="{Binding #hprogress.Value}" />
+          <ProgressBar ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #hprogress.Value}" />
           <ProgressBar IsIndeterminate="True"/>
         </StackPanel>
-        <ProgressBar Value="{Binding #vprogress.Value}" Orientation="Vertical" />
+        <ProgressBar ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #vprogress.Value}" Orientation="Vertical" />
         <ProgressBar Orientation="Vertical" IsIndeterminate="True" />
       </StackPanel>
       <StackPanel Margin="16">

+ 134 - 0
samples/ControlCatalog/Pages/TextBlockPage.xaml

@@ -0,0 +1,134 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             x:Class="ControlCatalog.Pages.TextBlockPage">
+  <StackPanel>
+    <TextBlock Classes="h1">TextBlock</TextBlock>
+    <TextBlock Classes="h2">A control that can display text</TextBlock>
+    <StackPanel
+      Orientation="Horizontal"
+      Spacing="16"
+      HorizontalAlignment="Center"
+      Margin="0,16,0,0">
+      <StackPanel.Styles>
+        <Style Selector="Border">
+          <Setter Property="BorderThickness" Value="1"/>
+          <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
+          <Setter Property="Padding" Value="2"/>
+        </Style>
+      </StackPanel.Styles>
+      <Border>
+        <StackPanel Width="200" Spacing="8">
+          <TextBlock TextTrimming="CharacterEllipsis" Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit."/>
+          <TextBlock TextTrimming="WordEllipsis" Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit."/>
+          <TextBlock Text="Left aligned text" TextAlignment="Left" />
+          <TextBlock Text="Center aligned text" TextAlignment="Center" />
+          <TextBlock Text="Right aligned text" TextAlignment="Right" />
+        </StackPanel>
+      </Border>
+      <Border>
+        <StackPanel Width="200" Spacing="8">
+          <TextBlock
+            TextWrapping="Wrap"
+            Text="Multiline TextBlock with TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
+        </StackPanel>
+      </Border>
+      <Border>
+        <StackPanel Width="200" Spacing="8">
+          <TextBlock Text="Custom font regular" FontWeight="Normal" FontStyle="Normal" FontFamily="avares://ControlCatalog/Assets/Fonts#Source Sans Pro"/>
+          <TextBlock Text="Custom font bold" FontWeight="Bold" FontStyle="Normal" FontFamily="avares://ControlCatalog/Assets/Fonts#Source Sans Pro"/>
+          <TextBlock Text="Custom font italic" FontWeight="Normal" FontStyle="Italic" FontFamily="/Assets/Fonts/SourceSansPro-Italic.ttf#Source Sans Pro"/>
+          <TextBlock Text="Custom font italic bold" FontWeight="Bold" FontStyle="Italic" FontFamily="/Assets/Fonts/SourceSansPro-*.ttf#Source Sans Pro"/>
+        </StackPanel>
+      </Border>
+    </StackPanel>
+    <StackPanel
+      Orientation="Horizontal"
+      Spacing="16"
+      HorizontalAlignment="Center"
+      Margin="0,16,0,0">
+      <StackPanel.Styles>
+        <Style Selector="Border">
+          <Setter Property="BorderThickness" Value="1"/>
+          <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
+          <Setter Property="Padding" Value="2"/>
+        </Style>
+      </StackPanel.Styles>
+      <Border>
+        <StackPanel Width="200" Spacing="8">
+          <TextBlock TextDecorations="Underline" Text="Underline"/>
+          <TextBlock TextDecorations="Strikethrough" Text="Strikethrough"/>
+          <TextBlock TextDecorations="Overline" Text="Overline" />
+          <TextBlock TextDecorations="Baseline" Text="Baseline"/>
+          <TextBlock Text="Custom TextDecorations">
+            <TextBlock.TextDecorations>
+              <TextDecorationCollection>
+                <TextDecoration
+                  Location="Overline"
+                  PenThicknessUnit="Pixel">
+                  <TextDecoration.Pen>
+                    <Pen Thickness="2">
+                      <Pen.Brush>
+                        <LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
+                          <LinearGradientBrush.GradientStops>
+                            <GradientStop Offset="0" Color="Red"/>
+                            <GradientStop Offset="1" Color="Green"/>
+                          </LinearGradientBrush.GradientStops>
+                        </LinearGradientBrush>
+                      </Pen.Brush>
+                    </Pen>
+                  </TextDecoration.Pen>
+                </TextDecoration>
+                <TextDecoration
+                  Location="Strikethrough"
+                  PenThicknessUnit="Pixel">
+                  <TextDecoration.Pen>
+                    <Pen Thickness="1">
+                      <Pen.Brush>
+                        <LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
+                          <LinearGradientBrush.GradientStops>
+                            <GradientStop Offset="0" Color="Green"/>
+                            <GradientStop Offset="1" Color="Blue"/>
+                          </LinearGradientBrush.GradientStops>
+                        </LinearGradientBrush>
+                      </Pen.Brush>
+                    </Pen>
+                  </TextDecoration.Pen>
+                </TextDecoration>
+                <TextDecoration
+                  Location="Underline"
+                  PenThicknessUnit="Pixel">
+                  <TextDecoration.Pen>
+                    <Pen Thickness="2">
+                      <Pen.Brush>
+                        <LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
+                          <LinearGradientBrush.GradientStops>
+                            <GradientStop Offset="0" Color="Blue"/>
+                            <GradientStop Offset="1" Color="Red"/>
+                          </LinearGradientBrush.GradientStops>
+                        </LinearGradientBrush>
+                      </Pen.Brush>
+                    </Pen>
+                  </TextDecoration.Pen>
+                </TextDecoration>
+              </TextDecorationCollection>
+            </TextBlock.TextDecorations>
+          </TextBlock>
+        </StackPanel>
+      </Border>
+      <Border>
+        <StackPanel Width="200" Spacing="8">
+          <TextBlock Text="🏻 👌🏻"/>
+          <TextBlock Text="🏼 👌🏼" />
+          <TextBlock Text="🏽 👌🏽"/>
+          <TextBlock Text="🏾 👌🏾"/>
+          <TextBlock Text="🏿 👌🏿"/>
+        </StackPanel>
+      </Border>
+      <Border>
+        <StackPanel Width="200" Spacing="8">
+          <TextBlock Text="👪 👨‍👩‍👧 👨‍👩‍👧‍👦"/>
+        </StackPanel>
+      </Border>
+    </StackPanel>
+  </StackPanel>
+</UserControl>

+ 18 - 0
samples/ControlCatalog/Pages/TextBlockPage.xaml.cs

@@ -0,0 +1,18 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+    public class TextBlockPage : UserControl
+    {
+        public TextBlockPage()
+        {
+            this.InitializeComponent();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+    }
+}

+ 1 - 1
samples/RenderDemo/Pages/RenderTargetBitmapPage.cs

@@ -39,7 +39,7 @@ namespace RenderDemo.Pages
                 ctx.FillRectangle(Brushes.Fuchsia, new Rect(50, 50, 100, 100));
             }
 
-            context.DrawImage(_bitmap, 1, 
+            context.DrawImage(_bitmap,
                 new Rect(0, 0, 200, 200), 
                 new Rect(0, 0, 200, 200));
             Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);

+ 1 - 0
samples/RenderDemo/RenderDemo.csproj

@@ -4,6 +4,7 @@
     <TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
   </PropertyGroup>
   <ItemGroup>
+    <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
     <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
   </ItemGroup>

+ 1 - 0
samples/VirtualizationDemo/VirtualizationDemo.csproj

@@ -4,6 +4,7 @@
     <TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
   </PropertyGroup>
   <ItemGroup>
+    <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
     <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
   </ItemGroup>

+ 0 - 64
scripts/avalonia-rename.ps1

@@ -1,64 +0,0 @@
-function Get-NewDirectoryName {
-    param ([System.IO.DirectoryInfo]$item)
-
-    $name = $item.Name.Replace("perspex", "avalonia")
-    $name = $name.Replace("Perspex", "Avalonia")
-    Join-Path $item.Parent.FullName $name
-}
-
-function Get-NewFileName {
-    param ([System.IO.FileInfo]$item)
-
-    $name = $item.Name.Replace("perspex", "avalonia")
-    $name = $name.Replace("Perspex", "Avalonia")
-    Join-Path $item.DirectoryName $name
-}
-
-function Rename-Contents {
-    param ([System.IO.FileInfo] $file)
-
-    $extensions = @(".cs",".xaml",".csproj",".sln",".md",".json",".yml",".partial",".ps1",".nuspec",".htm",".html",".gitmodules".".xml",".plist",".targets",".projitems",".shproj",".xib")
-
-    if ($extensions.Contains($file.Extension)) {
-        $text = [IO.File]::ReadAllText($file.FullName)
-        $text = $text.Replace("github.com/perspex", "github.com/avaloniaui")
-        $text = $text.Replace("github.com/Perspex", "github.com/AvaloniaUI")
-        $text = $text.Replace("perspex", "avalonia")
-        $text = $text.Replace("Perspex", "Avalonia")
-        $text = $text.Replace("PERSPEX", "AVALONIA")
-        [IO.File]::WriteAllText($file.FullName, $text)
-    }
-}
-
-function Process-Files {
-    param ([System.IO.DirectoryInfo] $item)
-
-    $dirs = Get-ChildItem -Path $item.FullName -Directory
-    $files = Get-ChildItem -Path $item.FullName -File
-
-    foreach ($dir in $dirs) {
-        Process-Files $dir.FullName
-    }
-
-    foreach ($file in $files) {
-        Rename-Contents $file
-
-        $renamed = Get-NewFileName $file
-
-        if ($file.FullName -ne $renamed) {
-            Write-Host git mv $file.FullName $renamed
-            & git mv $file.FullName $renamed
-        }
-    }
-
-    $renamed = Get-NewDirectoryName $item
-
-    if ($item.FullName -ne $renamed) {
-        Write-Host git mv $item.FullName $renamed
-        & git mv $item.FullName $renamed
-    }
-}
-
-& git submodule deinit .
-& git clean -xdf
-Process-Files .

+ 15 - 11
src/Avalonia.Animation/Animatable.cs

@@ -65,26 +65,30 @@ namespace Avalonia.Animation
             }
         }
 
-        /// <summary>
-        /// Reacts to a change in a <see cref="AvaloniaProperty"/> value in 
-        /// order to animate the change if a <see cref="ITransition"/> is set for the property.
-        /// </summary>
-        /// <param name="e">The event args.</param>
-        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        protected override void OnPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
         {
-            if (_transitions is null || _previousTransitions is null || e.Priority == BindingPriority.Animation) return;
+            if (_transitions is null || _previousTransitions is null || priority == BindingPriority.Animation)
+                return;
 
             // PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations).
             foreach (var transition in _transitions)
             {
-                if (transition.Property == e.Property)
+                if (transition.Property == property)
                 {
-                    if (_previousTransitions.TryGetValue(e.Property, out var dispose))
+                    if (_previousTransitions.TryGetValue(property, out var dispose))
                         dispose.Dispose();
 
-                    var instance = transition.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue);
+                    var instance = transition.Apply(
+                        this,
+                        Clock ?? Avalonia.Animation.Clock.GlobalClock,
+                        oldValue.GetValueOrDefault(),
+                        newValue.GetValueOrDefault());
 
-                    _previousTransitions[e.Property] = instance;
+                    _previousTransitions[property] = instance;
                     return;
                 }
             }

+ 1 - 1
src/Avalonia.Animation/IterationCount.cs

@@ -63,7 +63,7 @@ namespace Avalonia.Animation
         public IterationType RepeatType => _type;
 
         /// <summary>
-        /// Gets a value that indicates whether the <see cref="IterationCount"/> is set to loop.
+        /// Gets a value that indicates whether the <see cref="IterationCount"/> is set to Infinite.
         /// </summary>
         public bool IsInfinite => _type == IterationType.Infinite;
 

+ 5 - 3
src/Avalonia.Base/AttachedProperty.cs

@@ -18,12 +18,14 @@ namespace Avalonia
         /// <param name="ownerType">The class that is registering the property.</param>
         /// <param name="metadata">The property metadata.</param>
         /// <param name="inherits">Whether the property inherits its value.</param>
+        /// <param name="validate">A value validation callback.</param>
         public AttachedProperty(
             string name,
-            Type ownerType,            
+            Type ownerType,
             StyledPropertyMetadata<TValue> metadata,
-            bool inherits = false)
-            : base(name, ownerType, metadata, inherits)
+            bool inherits = false,
+            Func<TValue, bool> validate = null)
+            : base(name, ownerType, metadata, inherits, validate)
         {
         }
 

+ 364 - 360
src/Avalonia.Base/AvaloniaObject.cs

@@ -4,13 +4,11 @@
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
-using System.Linq;
-using System.Reactive.Linq;
 using Avalonia.Data;
 using Avalonia.Diagnostics;
 using Avalonia.Logging;
+using Avalonia.PropertyStore;
 using Avalonia.Threading;
-using Avalonia.Utilities;
 
 namespace Avalonia
 {
@@ -20,13 +18,13 @@ namespace Avalonia
     /// <remarks>
     /// This class is analogous to DependencyObject in WPF.
     /// </remarks>
-    public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged
+    public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IValueSink
     {
         private IAvaloniaObject _inheritanceParent;
-        private List<DirectBindingSubscription> _directBindings;
+        private List<IDisposable> _directBindings;
         private PropertyChangedEventHandler _inpcChanged;
         private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
-        private EventHandler<AvaloniaPropertyChangedEventArgs> _inheritablePropertyChanged;
+        private List<IAvaloniaObject> _inheritanceChildren;
         private ValueStore _values;
         private ValueStore Values => _values ?? (_values = new ValueStore(this));
 
@@ -36,7 +34,6 @@ namespace Avalonia
         public AvaloniaObject()
         {
             VerifyAccess();
-            AvaloniaPropertyRegistry.Instance.NotifyInitialized(this);
         }
 
         /// <summary>
@@ -57,15 +54,6 @@ namespace Avalonia
             remove { _inpcChanged -= value; }
         }
 
-        /// <summary>
-        /// Raised when an inheritable <see cref="AvaloniaProperty"/> value changes on this object.
-        /// </summary>
-        event EventHandler<AvaloniaPropertyChangedEventArgs> IAvaloniaObject.InheritablePropertyChanged
-        {
-            add { _inheritablePropertyChanged += value; }
-            remove { _inheritablePropertyChanged -= value; }
-        }
-
         /// <summary>
         /// Gets or sets the parent object that inherited <see cref="AvaloniaProperty"/> values
         /// are inherited from.
@@ -83,47 +71,27 @@ namespace Avalonia
             set
             {
                 VerifyAccess();
+
                 if (_inheritanceParent != value)
                 {
-                    if (_inheritanceParent != null)
-                    {
-                        _inheritanceParent.InheritablePropertyChanged -= ParentPropertyChanged;
-                    }
+                    var oldParent = _inheritanceParent;
+                    var valuestore = _values;
 
-                    var oldInheritanceParent = _inheritanceParent;
+                    _inheritanceParent?.RemoveInheritanceChild(this);
                     _inheritanceParent = value;
-                    var valuestore = _values;
 
                     foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType()))
                     {
-                        if (valuestore != null && valuestore.GetValue(property) != AvaloniaProperty.UnsetValue)
+                        if (valuestore?.IsSet(property) == true)
                         {
-                            // if local value set there can be no change
+                            // If local value set there can be no change.
                             continue;
                         }
-                        // get the value as it would have been with the previous InheritanceParent
-                        object oldValue;
-                        if (oldInheritanceParent is AvaloniaObject aobj)
-                        {
-                            oldValue = aobj.GetValueOrDefaultUnchecked(property);
-                        }
-                        else
-                        {
-                            oldValue = ((IStyledPropertyAccessor)property).GetDefaultValue(GetType());
-                        }
 
-                        object newValue = GetDefaultValue(property);
-
-                        if (!Equals(oldValue, newValue))
-                        {
-                            RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue);
-                        }
+                        property.RouteInheritanceParentChanged(this, oldParent);
                     }
 
-                    if (_inheritanceParent != null)
-                    {
-                        _inheritanceParent.InheritablePropertyChanged += ParentPropertyChanged;
-                    }
+                    _inheritanceParent?.AddInheritanceChild(this);
                 }
             }
         }
@@ -166,10 +134,56 @@ namespace Avalonia
         /// <param name="property">The property.</param>
         public void ClearValue(AvaloniaProperty property)
         {
-            Contract.Requires<ArgumentNullException>(property != null);
+            property = property ?? throw new ArgumentNullException(nameof(property));
+
+            property.RouteClearValue(this);
+        }
+
+        /// <summary>
+        /// Clears a <see cref="AvaloniaProperty"/>'s local value.
+        /// </summary>
+        /// <param name="property">The property.</param>
+        public void ClearValue<T>(AvaloniaProperty<T> property)
+        {
+            property = property ?? throw new ArgumentNullException(nameof(property));
+            VerifyAccess();
+
+            switch (property)
+            {
+                case StyledPropertyBase<T> styled:
+                    ClearValue(styled);
+                    break;
+                case DirectPropertyBase<T> direct:
+                    ClearValue(direct);
+                    break;
+                default:
+                    throw new NotSupportedException("Unsupported AvaloniaProperty type.");
+            }
+        }
+
+        /// <summary>
+        /// Clears a <see cref="AvaloniaProperty"/>'s local value.
+        /// </summary>
+        /// <param name="property">The property.</param>
+        public void ClearValue<T>(StyledPropertyBase<T> property)
+        {
+            property = property ?? throw new ArgumentNullException(nameof(property));
+            VerifyAccess();
+
+            _values?.ClearLocalValue(property);
+        }
+
+        /// <summary>
+        /// Clears a <see cref="AvaloniaProperty"/>'s local value.
+        /// </summary>
+        /// <param name="property">The property.</param>
+        public void ClearValue<T>(DirectPropertyBase<T> property)
+        {
+            property = property ?? throw new ArgumentNullException(nameof(property));
             VerifyAccess();
 
-            SetValue(property, AvaloniaProperty.UnsetValue);
+            var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
+            p.InvokeSetter(this, p.GetUnsetValue(GetType()));
         }
 
         /// <summary>
@@ -210,21 +224,23 @@ namespace Avalonia
         /// <returns>The value.</returns>
         public object GetValue(AvaloniaProperty property)
         {
-            if (property is null)
-            {
-                throw new ArgumentNullException(nameof(property));
-            }
+            property = property ?? throw new ArgumentNullException(nameof(property));
 
+            return property.RouteGetValue(this);
+        }
+
+        /// <summary>
+        /// Gets a <see cref="AvaloniaProperty"/> value.
+        /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
+        /// <param name="property">The property.</param>
+        /// <returns>The value.</returns>
+        public T GetValue<T>(StyledPropertyBase<T> property)
+        {
+            property = property ?? throw new ArgumentNullException(nameof(property));
             VerifyAccess();
 
-            if (property.IsDirect)
-            {
-                return ((IDirectPropertyAccessor)GetRegistered(property)).GetValue(this);
-            }
-            else
-            {
-                return GetValueOrDefaultUnchecked(property);
-            }
+            return GetValueOrInheritedOrDefault(property);
         }
 
         /// <summary>
@@ -233,14 +249,13 @@ namespace Avalonia
         /// <typeparam name="T">The type of the property.</typeparam>
         /// <param name="property">The property.</param>
         /// <returns>The value.</returns>
-        public T GetValue<T>(AvaloniaProperty<T> property)
+        public T GetValue<T>(DirectPropertyBase<T> property)
         {
-            if (property is null)
-            {
-                throw new ArgumentNullException(nameof(property));
-            }
+            property = property ?? throw new ArgumentNullException(nameof(property));
+            VerifyAccess();
 
-            return (T)GetValue((AvaloniaProperty)property);
+            var registered = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
+            return registered.InvokeGetter(this);
         }
 
         /// <summary>
@@ -284,16 +299,43 @@ namespace Avalonia
             object value,
             BindingPriority priority = BindingPriority.LocalValue)
         {
-            Contract.Requires<ArgumentNullException>(property != null);
+            property = property ?? throw new ArgumentNullException(nameof(property));
+
+            property.RouteSetValue(this, value, priority);
+        }
+
+        /// <summary>
+        /// Sets a <see cref="AvaloniaProperty"/> value.
+        /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
+        /// <param name="property">The property.</param>
+        /// <param name="value">The value.</param>
+        /// <param name="priority">The priority of the value.</param>
+        public void SetValue<T>(
+            StyledPropertyBase<T> property,
+            T value,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            property = property ?? throw new ArgumentNullException(nameof(property));
             VerifyAccess();
 
-            if (property.IsDirect)
+            LogPropertySet(property, value, priority);
+
+            if (value is UnsetValueType)
             {
-                SetDirectValue(property, value);
+                if (priority == BindingPriority.LocalValue)
+                {
+                    Values.ClearLocalValue(property);
+                }
+                else
+                {
+                    throw new NotSupportedException(
+                        "Cannot set property to Unset at non-local value priority.");
+                }
             }
-            else
+            else if (!(value is DoNothingType))
             {
-                SetStyledValue(property, value, priority);
+                Values.SetValue(property, value, priority);
             }
         }
 
@@ -303,69 +345,35 @@ namespace Avalonia
         /// <typeparam name="T">The type of the property.</typeparam>
         /// <param name="property">The property.</param>
         /// <param name="value">The value.</param>
-        /// <param name="priority">The priority of the value.</param>
-        public void SetValue<T>(
-            AvaloniaProperty<T> property,
-            T value,
-            BindingPriority priority = BindingPriority.LocalValue)
+        public void SetValue<T>(DirectPropertyBase<T> property, T value)
         {
-            Contract.Requires<ArgumentNullException>(property != null);
+            property = property ?? throw new ArgumentNullException(nameof(property));
+            VerifyAccess();
 
-            SetValue((AvaloniaProperty)property, value, priority);
+            LogPropertySet(property, value, BindingPriority.LocalValue);
+            SetDirectValueUnchecked(property, value);
         }
 
         /// <summary>
         /// Binds a <see cref="AvaloniaProperty"/> to an observable.
         /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
         /// <param name="property">The property.</param>
         /// <param name="source">The observable.</param>
         /// <param name="priority">The priority of the binding.</param>
         /// <returns>
         /// A disposable which can be used to terminate the binding.
         /// </returns>
-        public IDisposable Bind(
-            AvaloniaProperty property,
-            IObservable<object> source,
+        public IDisposable Bind<T>(
+            StyledPropertyBase<T> property,
+            IObservable<BindingValue<T>> source,
             BindingPriority priority = BindingPriority.LocalValue)
         {
-            Contract.Requires<ArgumentNullException>(property != null);
-            Contract.Requires<ArgumentNullException>(source != null);
-
+            property = property ?? throw new ArgumentNullException(nameof(property));
+            source = source ?? throw new ArgumentNullException(nameof(source));
             VerifyAccess();
 
-            if (property.IsDirect)
-            {
-                if (property.IsReadOnly)
-                {
-                    throw new ArgumentException($"The property {property.Name} is readonly.");
-                }
-
-                Logger.TryGet(LogEventLevel.Verbose)?.Log(
-                    LogArea.Property,
-                    this,
-                    "Bound {Property} to {Binding} with priority LocalValue",
-                    property,
-                    GetDescription(source));
-
-                if (_directBindings == null)
-                {
-                    _directBindings = new List<DirectBindingSubscription>();
-                }
-
-                return new DirectBindingSubscription(this, property, source);
-            }
-            else
-            {
-                Logger.TryGet(LogEventLevel.Verbose)?.Log(
-                    LogArea.Property,
-                    this,
-                    "Bound {Property} to {Binding} with priority {Priority}",
-                    property,
-                    GetDescription(source),
-                    priority);
-
-                return Values.AddBinding(property, source, priority);
-            }
+            return Values.AddBinding(property, source, priority);
         }
 
         /// <summary>
@@ -374,42 +382,90 @@ namespace Avalonia
         /// <typeparam name="T">The type of the property.</typeparam>
         /// <param name="property">The property.</param>
         /// <param name="source">The observable.</param>
-        /// <param name="priority">The priority of the binding.</param>
         /// <returns>
         /// A disposable which can be used to terminate the binding.
         /// </returns>
         public IDisposable Bind<T>(
-            AvaloniaProperty<T> property,
-            IObservable<T> source,
-            BindingPriority priority = BindingPriority.LocalValue)
+            DirectPropertyBase<T> property,
+            IObservable<BindingValue<T>> source)
         {
-            Contract.Requires<ArgumentNullException>(property != null);
+            property = property ?? throw new ArgumentNullException(nameof(property));
+            source = source ?? throw new ArgumentNullException(nameof(source));
+            VerifyAccess();
+
+            property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
 
-            return Bind(property, source.Select(x => (object)x), priority);
+            if (property.IsReadOnly)
+            {
+                throw new ArgumentException($"The property {property.Name} is readonly.");
+            }
+
+            Logger.TryGet(LogEventLevel.Verbose)?.Log(
+                LogArea.Property,
+                this,
+                "Bound {Property} to {Binding} with priority LocalValue",
+                property,
+                GetDescription(source));
+
+            _directBindings ??= new List<IDisposable>();
+
+            return new DirectBindingSubscription<T>(this, property, source);
         }
 
         /// <summary>
-        /// Forces the specified property to be revalidated.
+        /// Coerces the specified <see cref="AvaloniaProperty"/>.
         /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
         /// <param name="property">The property.</param>
-        public void Revalidate(AvaloniaProperty property)
+        public void CoerceValue<T>(StyledPropertyBase<T> property)
         {
-            VerifyAccess();
-            _values?.Revalidate(property);
+            _values?.CoerceValue(property);
+        }
+
+        /// <inheritdoc/>
+        void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child)
+        {
+            _inheritanceChildren ??= new List<IAvaloniaObject>();
+            _inheritanceChildren.Add(child);
         }
         
-        internal void PriorityValueChanged(AvaloniaProperty property, int priority, object oldValue, object newValue)
+        /// <inheritdoc/>
+        void IAvaloniaObject.RemoveInheritanceChild(IAvaloniaObject child)
+        {
+            _inheritanceChildren?.Remove(child);
+        }
+
+        void IAvaloniaObject.InheritedPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            Optional<T> newValue)
+        {
+            if (property.Inherits && (_values == null || !_values.IsSet(property)))
+            {
+                RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue);
+            }
+        }
+
+        /// <inheritdoc/>
+        Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
         {
-            oldValue = (oldValue == AvaloniaProperty.UnsetValue) ?
-                GetDefaultValue(property) :
-                oldValue;
-            newValue = (newValue == AvaloniaProperty.UnsetValue) ?
-                GetDefaultValue(property) :
-                newValue;
+            return _propertyChanged?.GetInvocationList();
+        }
+
+        void IValueSink.ValueChanged<T>(
+            StyledPropertyBase<T> property,
+            BindingPriority priority,
+            Optional<T> oldValue,
+            BindingValue<T> newValue)
+        {
+            oldValue = oldValue.HasValue ? oldValue : GetInheritedOrDefault(property);
+            newValue = newValue.HasValue ? newValue : newValue.WithValue(GetInheritedOrDefault(property));
+
+            LogIfError(property, newValue);
 
-            if (!Equals(oldValue, newValue))
+            if (!EqualityComparer<T>.Default.Equals(oldValue.Value, newValue.Value))
             {
-                RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)priority);
+                RaisePropertyChanged(property, oldValue, newValue, priority);
 
                 Logger.TryGet(LogEventLevel.Verbose)?.Log(
                     LogArea.Property,
@@ -421,39 +477,65 @@ namespace Avalonia
                     (BindingPriority)priority);
             }
         }
-        
-        internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
-        {
-            LogIfError(property, notification);
-            UpdateDataValidation(property, notification);
-        }
 
-        /// <inheritdoc/>
-        Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
+        void IValueSink.Completed<T>(
+            StyledPropertyBase<T> property,
+            IPriorityValueEntry entry,
+            Optional<T> oldValue) 
         {
-            return _propertyChanged?.GetInvocationList();
+            ((IValueSink)this).ValueChanged(property, BindingPriority.Unset, oldValue, default);
         }
 
         /// <summary>
-        /// Gets all priority values set on the object.
+        /// Called for each inherited property when the <see cref="InheritanceParent"/> changes.
         /// </summary>
-        /// <returns>A collection of property/value tuples.</returns>
-        internal IDictionary<AvaloniaProperty, object> GetSetValues() => Values?.GetSetValues();
+        /// <typeparam name="T">The type of the property value.</typeparam>
+        /// <param name="property">The property.</param>
+        /// <param name="oldParent">The old inheritance parent.</param>
+        internal void InheritanceParentChanged<T>(
+            StyledPropertyBase<T> property,
+            IAvaloniaObject oldParent)
+        {
+            var oldValue = oldParent switch
+            {
+                AvaloniaObject o => o.GetValueOrInheritedOrDefault(property),
+                null => property.GetDefaultValue(GetType()),
+                _ => oldParent.GetValue(property)
+            };
 
-        /// <summary>
-        /// Forces revalidation of properties when a property value changes.
-        /// </summary>
-        /// <param name="property">The property to that affects validation.</param>
-        /// <param name="affected">The affected properties.</param>
-        protected static void AffectsValidation(AvaloniaProperty property, params AvaloniaProperty[] affected)
+            var newValue = GetInheritedOrDefault(property);
+
+            if (!EqualityComparer<T>.Default.Equals(oldValue, newValue))
+            {
+                RaisePropertyChanged(property, oldValue, newValue);
+            }
+        }
+
+        internal AvaloniaPropertyValue GetDiagnosticInternal(AvaloniaProperty property)
         {
-            property.Changed.Subscribe(e =>
+            if (property.IsDirect)
             {
-                foreach (var p in affected)
+                return new AvaloniaPropertyValue(
+                    property,
+                    GetValue(property),
+                    BindingPriority.Unset,
+                    "Local Value");
+            }
+            else if (_values != null)
+            {
+                var result = _values.GetDiagnostic(property);
+
+                if (result != null)
                 {
-                    e.Sender.Revalidate(p);
+                    return result;
                 }
-            });
+            }
+
+            return new AvaloniaPropertyValue(
+                property,
+                GetValue(property),
+                BindingPriority.Unset,
+                "Unset");
         }
 
         /// <summary>
@@ -477,18 +559,25 @@ namespace Avalonia
         /// enabled.
         /// </summary>
         /// <param name="property">The property.</param>
-        /// <param name="status">The new validation status.</param>
-        protected virtual void UpdateDataValidation(
-            AvaloniaProperty property,
-            BindingNotification status)
+        /// <param name="value">The new binding value for the property.</param>
+        protected virtual void UpdateDataValidation<T>(
+            AvaloniaProperty<T> property,
+            BindingValue<T> value)
         {
         }
 
         /// <summary>
         /// Called when a avalonia property changes on the object.
         /// </summary>
-        /// <param name="e">The event arguments.</param>
-        protected virtual void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        /// <param name="property">The property whose value has changed.</param>
+        /// <param name="oldValue">The old value of the property.</param>
+        /// <param name="newValue">The new value of the property.</param>
+        /// <param name="priority">The priority of the new value.</param>
+        protected virtual void OnPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
         {
         }
 
@@ -499,40 +588,57 @@ namespace Avalonia
         /// <param name="oldValue">The old property value.</param>
         /// <param name="newValue">The new property value.</param>
         /// <param name="priority">The priority of the binding that produced the value.</param>
-        protected internal void RaisePropertyChanged(
-            AvaloniaProperty property,
-            object oldValue,
-            object newValue,
+        protected internal void RaisePropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
             BindingPriority priority = BindingPriority.LocalValue)
         {
-            Contract.Requires<ArgumentNullException>(property != null);
-            VerifyAccess();
+            property = property ?? throw new ArgumentNullException(nameof(property));
 
-            AvaloniaPropertyChangedEventArgs e = new AvaloniaPropertyChangedEventArgs(
-                this,
-                property,
-                oldValue,
-                newValue,
-                priority);
+            VerifyAccess();
 
             property.Notifying?.Invoke(this, true);
 
             try
             {
-                OnPropertyChanged(e);
-                property.NotifyChanged(e);
+                AvaloniaPropertyChangedEventArgs<T> e = null;
+                var hasChanged = property.HasChangedSubscriptions;
+
+                if (hasChanged || _propertyChanged != null)
+                {
+                    e = new AvaloniaPropertyChangedEventArgs<T>(
+                        this,
+                        property,
+                        oldValue,
+                        newValue,
+                        priority);
+                }
+
+                OnPropertyChanged(property, oldValue, newValue, priority);
+
+                if (hasChanged)
+                {
+                    property.NotifyChanged(e);
+                }
 
                 _propertyChanged?.Invoke(this, e);
 
                 if (_inpcChanged != null)
                 {
-                    PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name);
-                    _inpcChanged(this, e2);
+                    var inpce = new PropertyChangedEventArgs(property.Name);
+                    _inpcChanged(this, inpce);
                 }
 
-                if (property.Inherits)
+                if (property.Inherits && _inheritanceChildren != null)
                 {
-                    _inheritablePropertyChanged?.Invoke(this, e);
+                    foreach (var child in _inheritanceChildren)
+                    {
+                        child.InheritedPropertyChanged(
+                            property,
+                            oldValue,
+                            newValue.ToOptional());
+                    }
                 }
             }
             finally
@@ -561,216 +667,103 @@ namespace Avalonia
                 return false;
             }
 
-            DeferredSetter<T> setter = Values.GetDirectDeferredSetter(property);
-
-            return setter.SetAndNotify(this, property, ref field, value);
+            var old = field;
+            field = value;
+            RaisePropertyChanged(property, old, value);
+            return true;
         }
 
-        /// <summary>
-        /// Tries to cast a value to a type, taking into account that the value may be a
-        /// <see cref="BindingNotification"/>.
-        /// </summary>
-        /// <param name="value">The value.</param>
-        /// <param name="type">The type.</param>
-        /// <returns>The cast value, or a <see cref="BindingNotification"/>.</returns>
-        private static object CastOrDefault(object value, Type type)
+        private T GetInheritedOrDefault<T>(StyledPropertyBase<T> property)
         {
-            var notification = value as BindingNotification;
-
-            if (notification == null)
-            {
-                return TypeUtilities.ConvertImplicitOrDefault(value, type);
-            }
-            else
+            if (property.Inherits && InheritanceParent is AvaloniaObject o)
             {
-                if (notification.HasValue)
-                {
-                    notification.SetValue(TypeUtilities.ConvertImplicitOrDefault(notification.Value, type));
-                }
-
-                return notification;
+                return o.GetValueOrInheritedOrDefault(property);
             }
-        }
 
-        /// <summary>
-        /// Gets the default value for a property.
-        /// </summary>
-        /// <param name="property">The property.</param>
-        /// <returns>The default value.</returns>
-        private object GetDefaultValue(AvaloniaProperty property)
-        {
-            if (property.Inherits && InheritanceParent is AvaloniaObject aobj)
-                return aobj.GetValueOrDefaultUnchecked(property);
-            return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType());
+            return property.GetDefaultValue(GetType());
         }
 
-        /// <summary>
-        /// Gets the value or default value for a property.
-        /// </summary>
-        /// <param name="property">The property.</param>
-        /// <returns>The default value.</returns>
-        private object GetValueOrDefaultUnchecked(AvaloniaProperty property)
+        private T GetValueOrInheritedOrDefault<T>(StyledPropertyBase<T> property)
         {
-            var aobj = this;
-            var valuestore = aobj._values;
-            if (valuestore != null)
-            {
-                var result = valuestore.GetValue(property);
-                if (result != AvaloniaProperty.UnsetValue)
-                {
-                    return result;
-                }
-            }
-            if (property.Inherits)
-            {
-                while (aobj.InheritanceParent is AvaloniaObject parent)
-                {
-                    aobj = parent;
-                    valuestore = aobj._values;
-                    if (valuestore != null)
-                    {
-                        var result = valuestore.GetValue(property);
-                        if (result != AvaloniaProperty.UnsetValue)
-                        {
-                            return result;
-                        }
-                    }
-                }
-            }
-            return ((IStyledPropertyAccessor)property).GetDefaultValue(GetType());
-        }
+            var o = this;
+            var inherits = property.Inherits;
+            var value = default(T);
 
-        /// <summary>
-        /// Sets the value of a direct property.
-        /// </summary>
-        /// <param name="property">The property.</param>
-        /// <param name="value">The value.</param>
-        private void SetDirectValue(AvaloniaProperty property, object value)
-        {
-            void Set()
+            while (o != null)
             {
-                var notification = value as BindingNotification;
+                var values = o._values;
 
-                if (notification != null)
+                if (values?.TryGetValue(property, out value) == true)
                 {
-                    LogIfError(property, notification);
-                    value = notification.Value;
+                    return value;
                 }
 
-                if (notification == null || notification.ErrorType == BindingErrorType.Error || notification.HasValue)
+                if (!inherits)
                 {
-                    var metadata = (IDirectPropertyMetadata)property.GetMetadata(GetType());
-                    var accessor = (IDirectPropertyAccessor)GetRegistered(property);
-                    var finalValue = value == AvaloniaProperty.UnsetValue ?
-                        metadata.UnsetValue : value;
-
-                    LogPropertySet(property, value, BindingPriority.LocalValue);
-
-                    accessor.SetValue(this, finalValue);
+                    break;
                 }
 
-                if (notification != null)
-                {
-                    UpdateDataValidation(property, notification);
-                }
+                o = o.InheritanceParent as AvaloniaObject;
             }
 
-            if (Dispatcher.UIThread.CheckAccess())
-            {
-                Set();
-            }
-            else
-            {
-                Dispatcher.UIThread.Post(Set);
-            }
+            return property.GetDefaultValue(GetType());
         }
 
         /// <summary>
-        /// Sets the value of a styled property.
+        /// Sets the value of a direct property.
         /// </summary>
         /// <param name="property">The property.</param>
         /// <param name="value">The value.</param>
-        /// <param name="priority">The priority of the value.</param>
-        private void SetStyledValue(AvaloniaProperty property, object value, BindingPriority priority)
+        private void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, T value)
         {
-            var notification = value as BindingNotification;
+            var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
 
-            // We currently accept BindingNotifications for non-direct properties but we just
-            // strip them to their underlying value.
-            if (notification != null)
+            if (value is UnsetValueType)
             {
-                if (!notification.HasValue)
-                {
-                    return;
-                }
-                else
-                {
-                    value = notification.Value;
-                }
+                p.InvokeSetter(this, p.GetUnsetValue(GetType()));
             }
-
-            var originalValue = value;
-
-            if (!TypeUtilities.TryConvertImplicit(property.PropertyType, value, out value))
+            else if (!(value is DoNothingType))
             {
-                throw new ArgumentException(string.Format(
-                    "Invalid value for Property '{0}': '{1}' ({2})",
-                    property.Name,
-                    originalValue,
-                    originalValue?.GetType().FullName ?? "(null)"));
+                p.InvokeSetter(this, value);
             }
-
-            LogPropertySet(property, value, priority);
-            Values.AddValue(property, value, (int)priority);
         }
 
         /// <summary>
-        /// Given a direct property, returns a registered avalonia property that is equivalent or
-        /// throws if not found.
+        /// Sets the value of a direct property.
         /// </summary>
         /// <param name="property">The property.</param>
-        /// <returns>The registered property.</returns>
-        private AvaloniaProperty GetRegistered(AvaloniaProperty property)
+        /// <param name="value">The value.</param>
+        private void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, BindingValue<T> value)
         {
-            var direct = property as IDirectPropertyAccessor;
-
-            if (direct == null)
-            {
-                throw new AvaloniaInternalException(
-                    "AvaloniaObject.GetRegistered should only be called for direct properties");
-            }
+            var p = AvaloniaPropertyRegistry.Instance.FindRegisteredDirect(this, property);
 
-            if (property.OwnerType.IsAssignableFrom(GetType()))
+            if (p == null)
             {
-                return property;
+                throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}");
             }
 
-            var result =  AvaloniaPropertyRegistry.Instance.GetRegistered(this)
-                .FirstOrDefault(x => x == property);
+            LogIfError(property, value);
 
-            if (result == null)
+            switch (value.Type)
             {
-                throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}");
+                case BindingValueType.UnsetValue:
+                case BindingValueType.BindingError:
+                    var fallback = value.HasValue ? value : value.WithValue(property.GetUnsetValue(GetType()));
+                    property.InvokeSetter(this, fallback);
+                    break;
+                case BindingValueType.DataValidationError:
+                    property.InvokeSetter(this, value);
+                    break;
+                case BindingValueType.Value:
+                case BindingValueType.BindingErrorWithFallback:
+                case BindingValueType.DataValidationErrorWithFallback:
+                    property.InvokeSetter(this, value);
+                    break;
             }
 
-            return result;
-        }
-
-        /// <summary>
-        /// Called when a property is changed on the current <see cref="InheritanceParent"/>.
-        /// </summary>
-        /// <param name="sender">The event sender.</param>
-        /// <param name="e">The event args.</param>
-        /// <remarks>
-        /// Checks for changes in an inherited property value.
-        /// </remarks>
-        private void ParentPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
-        {
-            Contract.Requires<ArgumentNullException>(e != null);
-
-            if (e.Property.Inherits && !IsSet(e.Property))
+            if (p.IsDataValidationEnabled)
             {
-                RaisePropertyChanged(e.Property, e.OldValue, e.NewValue, BindingPriority.LocalValue);
+                UpdateDataValidation(property, value);
             }
         }
 
@@ -779,7 +772,7 @@ namespace Avalonia
         /// </summary>
         /// <param name="o">The observable.</param>
         /// <returns>The description.</returns>
-        private string GetDescription(IObservable<object> o)
+        private string GetDescription(object o)
         {
             var description = o as IDescription;
             return description?.Description ?? o.ToString();
@@ -789,12 +782,12 @@ namespace Avalonia
         /// Logs a mesage if the notification represents a binding error.
         /// </summary>
         /// <param name="property">The property being bound.</param>
-        /// <param name="notification">The binding notification.</param>
-        private void LogIfError(AvaloniaProperty property, BindingNotification notification)
+        /// <param name="value">The binding notification.</param>
+        private void LogIfError<T>(AvaloniaProperty property, BindingValue<T> value)
         {
-            if (notification.ErrorType == BindingErrorType.Error)
+            if (value.HasError)
             {
-                if (notification.Error is AggregateException aggregate)
+                if (value.Error is AggregateException aggregate)
                 {
                     foreach (var inner in aggregate.InnerExceptions)
                     {
@@ -803,7 +796,7 @@ namespace Avalonia
                 }
                 else
                 {
-                    LogBindingError(property, notification.Error);
+                    LogBindingError(property, value.Error);
                 }
             }
         }
@@ -814,7 +807,7 @@ namespace Avalonia
         /// <param name="property">The property.</param>
         /// <param name="value">The new value.</param>
         /// <param name="priority">The priority.</param>
-        private void LogPropertySet(AvaloniaProperty property, object value, BindingPriority priority)
+        private void LogPropertySet<T>(AvaloniaProperty<T> property, T value, BindingPriority priority)
         {
             Logger.TryGet(LogEventLevel.Verbose)?.Log(
                 LogArea.Property,
@@ -825,16 +818,16 @@ namespace Avalonia
                 priority);
         }
 
-        private class DirectBindingSubscription : IObserver<object>, IDisposable
+        private class DirectBindingSubscription<T> : IObserver<BindingValue<T>>, IDisposable
         {
-            readonly AvaloniaObject _owner;
-            readonly AvaloniaProperty _property;
-            IDisposable _subscription;
+            private readonly AvaloniaObject _owner;
+            private readonly DirectPropertyBase<T> _property;
+            private readonly IDisposable _subscription;
 
             public DirectBindingSubscription(
                 AvaloniaObject owner,
-                AvaloniaProperty property,
-                IObservable<object> source)
+                DirectPropertyBase<T> property,
+                IObservable<BindingValue<T>> source)
             {
                 _owner = owner;
                 _property = property;
@@ -850,11 +843,22 @@ namespace Avalonia
 
             public void OnCompleted() => Dispose();
             public void OnError(Exception error) => Dispose();
-
-            public void OnNext(object value)
+            public void OnNext(BindingValue<T> value)
             {
-                var castValue = CastOrDefault(value, _property.PropertyType);
-                _owner.SetDirectValue(_property, castValue);
+                if (Dispatcher.UIThread.CheckAccess())
+                {
+                    _owner.SetDirectValueUnchecked(_property, value);
+                }
+                else
+                {
+                    // To avoid allocating closure in the outer scope we need to capture variables
+                    // locally. This allows us to skip most of the allocations when on UI thread.
+                    var instance = _owner;
+                    var property = _property;
+                    var newValue = value;
+
+                    Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, newValue));
+                }
             }
         }
     }

+ 330 - 5
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@@ -68,6 +68,51 @@ namespace Avalonia
             return new AvaloniaPropertyObservable<T>(o, property);
         }
 
+        /// <summary>
+        /// Gets an observable for a <see cref="AvaloniaProperty"/>.
+        /// </summary>
+        /// <param name="o">The object.</param>
+        /// <param name="property">The property.</param>
+        /// <returns>
+        /// An observable which fires immediately with the current value of the property on the
+        /// object and subsequently each time the property value changes.
+        /// </returns>
+        /// <remarks>
+        /// The subscription to <paramref name="o"/> is created using a weak reference.
+        /// </remarks>
+        public static IObservable<BindingValue<object>> GetBindingObservable(
+            this IAvaloniaObject o,
+            AvaloniaProperty property)
+        {
+            Contract.Requires<ArgumentNullException>(o != null);
+            Contract.Requires<ArgumentNullException>(property != null);
+
+            return new AvaloniaPropertyBindingObservable<object>(o, property);
+        }
+
+        /// <summary>
+        /// Gets an observable for a <see cref="AvaloniaProperty"/>.
+        /// </summary>
+        /// <param name="o">The object.</param>
+        /// <typeparam name="T">The property type.</typeparam>
+        /// <param name="property">The property.</param>
+        /// <returns>
+        /// An observable which fires immediately with the current value of the property on the
+        /// object and subsequently each time the property value changes.
+        /// </returns>
+        /// <remarks>
+        /// The subscription to <paramref name="o"/> is created using a weak reference.
+        /// </remarks>
+        public static IObservable<BindingValue<T>> GetBindingObservable<T>(
+            this IAvaloniaObject o,
+            AvaloniaProperty<T> property)
+        {
+            Contract.Requires<ArgumentNullException>(o != null);
+            Contract.Requires<ArgumentNullException>(property != null);
+
+            return new AvaloniaPropertyBindingObservable<T>(o, property);
+        }
+
         /// <summary>
         /// Gets an observable that listens for property changed events for an
         /// <see cref="AvaloniaProperty"/>.
@@ -80,7 +125,7 @@ namespace Avalonia
         /// for the specified property.
         /// </returns>
         public static IObservable<AvaloniaPropertyChangedEventArgs> GetPropertyChangedObservable(
-            this IAvaloniaObject o, 
+            this IAvaloniaObject o,
             AvaloniaProperty property)
         {
             Contract.Requires<ArgumentNullException>(o != null);
@@ -134,6 +179,167 @@ namespace Avalonia
                 o.GetObservable(property));
         }
 
+        /// <summary>
+        /// Gets a subject for a <see cref="AvaloniaProperty"/>.
+        /// </summary>
+        /// <param name="o">The object.</param>
+        /// <param name="property">The property.</param>
+        /// <param name="priority">
+        /// The priority with which binding values are written to the object.
+        /// </param>
+        /// <returns>
+        /// An <see cref="ISubject{Object}"/> which can be used for two-way binding to/from the 
+        /// property.
+        /// </returns>
+        public static ISubject<BindingValue<object>> GetBindingSubject(
+            this IAvaloniaObject o,
+            AvaloniaProperty property,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            return Subject.Create<BindingValue<object>>(
+                Observer.Create<BindingValue<object>>(x =>
+                {
+                    if (x.HasValue)
+                    {
+                        o.SetValue(property, x.Value, priority);
+                    }
+                }),
+                o.GetBindingObservable(property));
+        }
+
+        /// <summary>
+        /// Gets a subject for a <see cref="AvaloniaProperty"/>.
+        /// </summary>
+        /// <typeparam name="T">The property type.</typeparam>
+        /// <param name="o">The object.</param>
+        /// <param name="property">The property.</param>
+        /// <param name="priority">
+        /// The priority with which binding values are written to the object.
+        /// </param>
+        /// <returns>
+        /// An <see cref="ISubject{T}"/> which can be used for two-way binding to/from the 
+        /// property.
+        /// </returns>
+        public static ISubject<BindingValue<T>> GetBindingSubject<T>(
+            this IAvaloniaObject o,
+            AvaloniaProperty<T> property,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            return Subject.Create<BindingValue<T>>(
+                Observer.Create<BindingValue<T>>(x =>
+                {
+                    if (x.HasValue)
+                    {
+                        o.SetValue(property, x.Value, priority);
+                    }
+                }),
+                o.GetBindingObservable(property));
+        }
+
+        /// <summary>
+        /// Binds a <see cref="AvaloniaProperty"/> to an observable.
+        /// </summary>
+        /// <param name="target">The object.</param>
+        /// <param name="property">The property.</param>
+        /// <param name="source">The observable.</param>
+        /// <param name="priority">The priority of the binding.</param>
+        /// <returns>
+        /// A disposable which can be used to terminate the binding.
+        /// </returns>
+        public static IDisposable Bind(
+            this IAvaloniaObject target,
+            AvaloniaProperty property,
+            IObservable<BindingValue<object>> source,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            target = target ?? throw new ArgumentNullException(nameof(target));
+            property = property ?? throw new ArgumentNullException(nameof(property));
+            source = source ?? throw new ArgumentNullException(nameof(source));
+
+            return property.RouteBind(target, source, priority);
+        }
+
+        /// <summary>
+        /// Binds a <see cref="AvaloniaProperty"/> to an observable.
+        /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
+        /// <param name="target">The object.</param>
+        /// <param name="property">The property.</param>
+        /// <param name="source">The observable.</param>
+        /// <param name="priority">The priority of the binding.</param>
+        /// <returns>
+        /// A disposable which can be used to terminate the binding.
+        /// </returns>
+        public static IDisposable Bind<T>(
+            this IAvaloniaObject target,
+            AvaloniaProperty<T> property,
+            IObservable<BindingValue<T>> source,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            target = target ?? throw new ArgumentNullException(nameof(target));
+            property = property ?? throw new ArgumentNullException(nameof(property));
+            source = source ?? throw new ArgumentNullException(nameof(source));
+
+            return property switch
+            {
+                StyledPropertyBase<T> styled => target.Bind(styled, source, priority),
+                DirectPropertyBase<T> direct => target.Bind(direct, source),
+                _ => throw new NotSupportedException("Unsupported AvaloniaProperty type."),
+            };
+        }
+
+        /// <summary>
+        /// Binds a <see cref="AvaloniaProperty"/> to an observable.
+        /// </summary>
+        /// <param name="target">The object.</param>
+        /// <param name="property">The property.</param>
+        /// <param name="source">The observable.</param>
+        /// <param name="priority">The priority of the binding.</param>
+        /// <returns>
+        /// A disposable which can be used to terminate the binding.
+        /// </returns>
+        public static IDisposable Bind(
+            this IAvaloniaObject target,
+            AvaloniaProperty property,
+            IObservable<object> source,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            target = target ?? throw new ArgumentNullException(nameof(target));
+            property = property ?? throw new ArgumentNullException(nameof(property));
+            source = source ?? throw new ArgumentNullException(nameof(source));
+
+            return target.Bind(
+                property,
+                source.ToBindingValue(),
+                priority);
+        }
+
+        /// <summary>
+        /// Binds a <see cref="AvaloniaProperty"/> to an observable.
+        /// </summary>
+        /// <param name="target">The object.</param>
+        /// <param name="property">The property.</param>
+        /// <param name="source">The observable.</param>
+        /// <param name="priority">The priority of the binding.</param>
+        /// <returns>
+        /// A disposable which can be used to terminate the binding.
+        /// </returns>
+        public static IDisposable Bind<T>(
+            this IAvaloniaObject target,
+            AvaloniaProperty<T> property,
+            IObservable<T> source,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            target = target ?? throw new ArgumentNullException(nameof(target));
+            property = property ?? throw new ArgumentNullException(nameof(property));
+            source = source ?? throw new ArgumentNullException(nameof(source));
+
+            return target.Bind(
+                property,
+                source.ToBindingValue(),
+                priority);
+        }
+
         /// <summary>
         /// Binds a property on an <see cref="IAvaloniaObject"/> to an <see cref="IBinding"/>.
         /// </summary>
@@ -153,16 +359,16 @@ namespace Avalonia
             IBinding binding,
             object anchor = null)
         {
-            Contract.Requires<ArgumentNullException>(target != null);
-            Contract.Requires<ArgumentNullException>(property != null);
-            Contract.Requires<ArgumentNullException>(binding != null);
+            target = target ?? throw new ArgumentNullException(nameof(target));
+            property = property ?? throw new ArgumentNullException(nameof(property));
+            binding = binding ?? throw new ArgumentNullException(nameof(binding));
 
             var metadata = property.GetMetadata(target.GetType()) as IDirectPropertyMetadata;
 
             var result = binding.Initiate(
                 target,
                 property,
-                anchor, 
+                anchor,
                 metadata?.EnableDataValidation ?? false);
 
             if (result != null)
@@ -175,6 +381,125 @@ namespace Avalonia
             }
         }
 
+        /// <summary>
+        /// Clears a <see cref="AvaloniaProperty"/>'s local value.
+        /// </summary>
+        /// <param name="target">The object.</param>
+        /// <param name="property">The property.</param>
+        public static void ClearValue(this IAvaloniaObject target, AvaloniaProperty property)
+        {
+            target = target ?? throw new ArgumentNullException(nameof(target));
+            property = property ?? throw new ArgumentNullException(nameof(property));
+
+            property.RouteClearValue(target);
+        }
+
+        /// <summary>
+        /// Clears a <see cref="AvaloniaProperty"/>'s local value.
+        /// </summary>
+        /// <param name="target">The object.</param>
+        /// <param name="property">The property.</param>
+        public static void ClearValue<T>(this IAvaloniaObject target, AvaloniaProperty<T> property)
+        {
+            target = target ?? throw new ArgumentNullException(nameof(target));
+            property = property ?? throw new ArgumentNullException(nameof(property));
+
+            switch (property)
+            {
+                case StyledPropertyBase<T> styled:
+                    target.ClearValue(styled);
+                    break;
+                case DirectPropertyBase<T> direct:
+                    target.ClearValue(direct);
+                    break;
+                default:
+                    throw new NotSupportedException("Unsupported AvaloniaProperty type.");
+            }
+        }
+
+        /// <summary>
+        /// Gets a <see cref="AvaloniaProperty"/> value.
+        /// </summary>
+        /// <param name="target">The object.</param>
+        /// <param name="property">The property.</param>
+        /// <returns>The value.</returns>
+        public static object GetValue(this IAvaloniaObject target, AvaloniaProperty property)
+        {
+            target = target ?? throw new ArgumentNullException(nameof(target));
+            property = property ?? throw new ArgumentNullException(nameof(property));
+
+            return property.RouteGetValue(target);
+        }
+
+        /// <summary>
+        /// Gets a <see cref="AvaloniaProperty"/> value.
+        /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
+        /// <param name="target">The object.</param>
+        /// <param name="property">The property.</param>
+        /// <returns>The value.</returns>
+        public static T GetValue<T>(this IAvaloniaObject target, AvaloniaProperty<T> property)
+        {
+            target = target ?? throw new ArgumentNullException(nameof(target));
+            property = property ?? throw new ArgumentNullException(nameof(property));
+
+            return property switch
+            {
+                StyledPropertyBase<T> styled => target.GetValue(styled),
+                DirectPropertyBase<T> direct => target.GetValue(direct),
+                _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.")
+            };
+        }
+
+        /// <summary>
+        /// Sets a <see cref="AvaloniaProperty"/> value.
+        /// </summary>
+        /// <param name="target">The object.</param>
+        /// <param name="property">The property.</param>
+        /// <param name="value">The value.</param>
+        /// <param name="priority">The priority of the value.</param>
+        public static void SetValue(
+            this IAvaloniaObject target,
+            AvaloniaProperty property,
+            object value,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            target = target ?? throw new ArgumentNullException(nameof(target));
+            property = property ?? throw new ArgumentNullException(nameof(property));
+
+            property.RouteSetValue(target, value, priority);
+        }
+
+        /// <summary>
+        /// Sets a <see cref="AvaloniaProperty"/> value.
+        /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
+        /// <param name="target">The object.</param>
+        /// <param name="property">The property.</param>
+        /// <param name="value">The value.</param>
+        /// <param name="priority">The priority of the value.</param>
+        public static void SetValue<T>(
+            this IAvaloniaObject target,
+            AvaloniaProperty<T> property,
+            T value,
+            BindingPriority priority = BindingPriority.LocalValue)
+        {
+            target = target ?? throw new ArgumentNullException(nameof(target));
+            property = property ?? throw new ArgumentNullException(nameof(property));
+
+            switch (property)
+            {
+                case StyledPropertyBase<T> styled:
+                    target.SetValue(styled, value, priority);
+                    break;
+                case DirectPropertyBase<T> direct:
+                    target.SetValue(direct, value);
+                    break;
+                default:
+                    throw new NotSupportedException("Unsupported AvaloniaProperty type.");
+            }
+        }
+
         /// <summary>
         /// Subscribes to a property changed notifications for changes that originate from a
         /// <typeparamref name="TTarget"/>.

+ 74 - 74
src/Avalonia.Base/AvaloniaProperty.cs

@@ -3,9 +3,7 @@
 
 using System;
 using System.Collections.Generic;
-using System.Diagnostics;
 using System.Reactive.Subjects;
-using System.Reflection;
 using Avalonia.Data;
 using Avalonia.Utilities;
 
@@ -14,7 +12,7 @@ namespace Avalonia
     /// <summary>
     /// Base class for avalonia properties.
     /// </summary>
-    public class AvaloniaProperty : IEquatable<AvaloniaProperty>
+    public abstract class AvaloniaProperty : IEquatable<AvaloniaProperty>
     {
         /// <summary>
         /// Represents an unset property value.
@@ -22,7 +20,6 @@ namespace Avalonia
         public static readonly object UnsetValue = new UnsetValueType();
 
         private static int s_nextId;
-        private readonly Subject<AvaloniaPropertyChangedEventArgs> _initialized;
         private readonly Subject<AvaloniaPropertyChangedEventArgs> _changed;
         private readonly PropertyMetadata _defaultMetadata;
         private readonly Dictionary<Type, PropertyMetadata> _metadata;
@@ -55,7 +52,6 @@ namespace Avalonia
                 throw new ArgumentException("'name' may not contain periods.");
             }
 
-            _initialized = new Subject<AvaloniaPropertyChangedEventArgs>();
             _changed = new Subject<AvaloniaPropertyChangedEventArgs>();
             _metadata = new Dictionary<Type, PropertyMetadata>();
 
@@ -83,7 +79,6 @@ namespace Avalonia
             Contract.Requires<ArgumentNullException>(source != null);
             Contract.Requires<ArgumentNullException>(ownerType != null);
 
-            _initialized = source._initialized;
             _changed = source._changed;
             _metadata = new Dictionary<Type, PropertyMetadata>();
 
@@ -138,22 +133,6 @@ namespace Avalonia
         /// </summary>
         public virtual bool IsReadOnly => false;
 
-        /// <summary>
-        /// Gets an observable that is fired when this property is initialized on a
-        /// new <see cref="AvaloniaObject"/> instance.
-        /// </summary>
-        /// <remarks>
-        /// This observable is fired each time a new <see cref="AvaloniaObject"/> is constructed
-        /// for all properties registered on the object's type. The default value of the property
-        /// for the object is passed in the args' NewValue (OldValue will always be
-        /// <see cref="UnsetValue"/>.
-        /// </remarks>
-        /// <value>
-        /// An observable that is fired when this property is initialized on a new
-        /// <see cref="AvaloniaObject"/> instance.
-        /// </value>
-        public IObservable<AvaloniaPropertyChangedEventArgs> Initialized => _initialized;
-
         /// <summary>
         /// Gets an observable that is fired when this property changes on any
         /// <see cref="AvaloniaObject"/> instance.
@@ -183,6 +162,8 @@ namespace Avalonia
         /// </summary>
         internal int Id { get; }
 
+        internal bool HasChangedSubscriptions => _changed?.HasObservers ?? false;
+
         /// <summary>
         /// Provides access to a property's binding via the <see cref="AvaloniaObject"/>
         /// indexer.
@@ -255,7 +236,8 @@ namespace Avalonia
         /// <param name="defaultValue">The default value of the property.</param>
         /// <param name="inherits">Whether the property inherits its value.</param>
         /// <param name="defaultBindingMode">The default binding mode for the property.</param>
-        /// <param name="validate">A validation function.</param>
+         /// <param name="validate">A value validation callback.</param>
+        /// <param name="coerce">A value coercion callback.</param>
         /// <param name="notifying">
         /// A method that gets called before and after the property starts being notified on an
         /// object; the bool argument will be true before and false afterwards. This callback is
@@ -267,7 +249,8 @@ namespace Avalonia
             TValue defaultValue = default(TValue),
             bool inherits = false,
             BindingMode defaultBindingMode = BindingMode.OneWay,
-            Func<TOwner, TValue, TValue> validate = null,
+            Func<TValue, bool> validate = null,
+            Func<IAvaloniaObject, TValue, TValue> coerce = null,
             Action<IAvaloniaObject, bool> notifying = null)
                 where TOwner : IAvaloniaObject
         {
@@ -275,14 +258,15 @@ namespace Avalonia
 
             var metadata = new StyledPropertyMetadata<TValue>(
                 defaultValue,
-                validate: Cast(validate),
-                defaultBindingMode: defaultBindingMode);
+                defaultBindingMode: defaultBindingMode,
+                coerce: coerce);
 
             var result = new StyledProperty<TValue>(
                 name,
                 typeof(TOwner),
                 metadata,
                 inherits,
+                validate,
                 notifying);
             AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result);
             return result;
@@ -298,24 +282,26 @@ namespace Avalonia
         /// <param name="defaultValue">The default value of the property.</param>
         /// <param name="inherits">Whether the property inherits its value.</param>
         /// <param name="defaultBindingMode">The default binding mode for the property.</param>
-        /// <param name="validate">A validation function.</param>
+        /// <param name="validate">A value validation callback.</param>
+        /// <param name="coerce">A value coercion callback.</param>
         /// <returns>A <see cref="AvaloniaProperty{TValue}"/></returns>
         public static AttachedProperty<TValue> RegisterAttached<TOwner, THost, TValue>(
             string name,
             TValue defaultValue = default(TValue),
             bool inherits = false,
             BindingMode defaultBindingMode = BindingMode.OneWay,
-            Func<THost, TValue, TValue> validate = null)
+            Func<TValue, bool> validate = null,
+            Func<IAvaloniaObject, TValue, TValue> coerce = null)
                 where THost : IAvaloniaObject
         {
             Contract.Requires<ArgumentNullException>(name != null);
 
             var metadata = new StyledPropertyMetadata<TValue>(
                 defaultValue,
-                validate: Cast(validate),
-                defaultBindingMode: defaultBindingMode);
+                defaultBindingMode: defaultBindingMode,
+                coerce: coerce);
 
-            var result = new AttachedProperty<TValue>(name, typeof(TOwner), metadata, inherits);
+            var result = new AttachedProperty<TValue>(name, typeof(TOwner), metadata, inherits, validate);
             var registry = AvaloniaPropertyRegistry.Instance;
             registry.Register(typeof(TOwner), result);
             registry.RegisterAttached(typeof(THost), result);
@@ -332,7 +318,8 @@ namespace Avalonia
         /// <param name="defaultValue">The default value of the property.</param>
         /// <param name="inherits">Whether the property inherits its value.</param>
         /// <param name="defaultBindingMode">The default binding mode for the property.</param>
-        /// <param name="validate">A validation function.</param>
+        /// <param name="validate">A value validation callback.</param>
+        /// <param name="coerce">A value coercion callback.</param>
         /// <returns>A <see cref="AvaloniaProperty{TValue}"/></returns>
         public static AttachedProperty<TValue> RegisterAttached<THost, TValue>(
             string name,
@@ -340,17 +327,18 @@ namespace Avalonia
             TValue defaultValue = default(TValue),
             bool inherits = false,
             BindingMode defaultBindingMode = BindingMode.OneWay,
-            Func<THost, TValue, TValue> validate = null)
+            Func<TValue, bool> validate = null,
+            Func<IAvaloniaObject, TValue, TValue> coerce = null)
                 where THost : IAvaloniaObject
         {
             Contract.Requires<ArgumentNullException>(name != null);
 
             var metadata = new StyledPropertyMetadata<TValue>(
                 defaultValue,
-                validate: Cast(validate),
-                defaultBindingMode: defaultBindingMode);
+                defaultBindingMode: defaultBindingMode,
+                coerce: coerce);
 
-            var result = new AttachedProperty<TValue>(name, ownerType, metadata, inherits);
+            var result = new AttachedProperty<TValue>(name, ownerType, metadata, inherits, validate);
             var registry = AvaloniaPropertyRegistry.Instance;
             registry.Register(ownerType, result);
             registry.RegisterAttached(typeof(THost), result);
@@ -365,9 +353,7 @@ namespace Avalonia
         /// <param name="name">The name of the property.</param>
         /// <param name="getter">Gets the current value of the property.</param>
         /// <param name="setter">Sets the value of the property.</param>
-        /// <param name="unsetValue">
-        /// The value to use when the property is set to <see cref="AvaloniaProperty.UnsetValue"/>
-        /// </param>
+        /// <param name="unsetValue">The value to use when the property is cleared.</param>
         /// <param name="defaultBindingMode">The default binding mode for the property.</param>
         /// <param name="enableDataValidation">
         /// Whether the property is interested in data validation.
@@ -383,13 +369,18 @@ namespace Avalonia
                 where TOwner : IAvaloniaObject
         {
             Contract.Requires<ArgumentNullException>(name != null);
+            Contract.Requires<ArgumentNullException>(getter != null);
 
             var metadata = new DirectPropertyMetadata<TValue>(
                 unsetValue: unsetValue,
-                defaultBindingMode: defaultBindingMode,
-                enableDataValidation: enableDataValidation);
+                defaultBindingMode: defaultBindingMode);
 
-            var result = new DirectProperty<TOwner, TValue>(name, getter, setter, metadata);
+            var result = new DirectProperty<TOwner, TValue>(
+                name,
+                getter,
+                setter,
+                metadata,
+                enableDataValidation);
             AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result);
             return result;
         }
@@ -478,20 +469,6 @@ namespace Avalonia
             return Name;
         }
 
-        /// <summary>
-        /// True if <see cref="Initialized"/> has any observers.
-        /// </summary>
-        internal bool HasNotifyInitializedObservers => _initialized.HasObservers;
-
-        /// <summary>
-        /// Notifies the <see cref="Initialized"/> observable.
-        /// </summary>
-        /// <param name="e">The observable arguments.</param>
-        internal void NotifyInitialized(AvaloniaPropertyChangedEventArgs e)
-        {
-            _initialized.OnNext(e);
-        }
-
         /// <summary>
         /// Notifies the <see cref="Changed"/> observable.
         /// </summary>
@@ -501,6 +478,42 @@ namespace Avalonia
             _changed.OnNext(e);
         }
 
+        /// <summary>
+        /// Routes an untyped ClearValue call to a typed call.
+        /// </summary>
+        /// <param name="o">The object instance.</param>
+        internal abstract void RouteClearValue(IAvaloniaObject o);
+
+        /// <summary>
+        /// Routes an untyped GetValue call to a typed call.
+        /// </summary>
+        /// <param name="o">The object instance.</param>
+        internal abstract object RouteGetValue(IAvaloniaObject o);
+
+        /// <summary>
+        /// Routes an untyped SetValue call to a typed call.
+        /// </summary>
+        /// <param name="o">The object instance.</param>
+        /// <param name="value">The value.</param>
+        /// <param name="priority">The priority.</param>
+        internal abstract void RouteSetValue(
+            IAvaloniaObject o,
+            object value,
+            BindingPriority priority);
+
+        /// <summary>
+        /// Routes an untyped Bind call to a typed call.
+        /// </summary>
+        /// <param name="o">The object instance.</param>
+        /// <param name="source">The binding source.</param>
+        /// <param name="priority">The priority.</param>
+        internal abstract IDisposable RouteBind(
+            IAvaloniaObject o,
+            IObservable<BindingValue<object>> source,
+            BindingPriority priority);
+
+        internal abstract void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent);
+
         /// <summary>
         /// Overrides the metadata for the property on the specified type.
         /// </summary>
@@ -548,35 +561,22 @@ namespace Avalonia
                     return result;
                 }
 
-                currentType = currentType.GetTypeInfo().BaseType;
+                currentType = currentType.BaseType;
             }
 
             _metadataCache[type] = _defaultMetadata;
 
             return _defaultMetadata;
         }
-
-        [DebuggerHidden]
-        private static Func<IAvaloniaObject, TValue, TValue> Cast<TOwner, TValue>(Func<TOwner, TValue, TValue> f)
-            where TOwner : IAvaloniaObject
-        {
-            if (f != null)
-            {
-                return (o, v) => (o is TOwner) ? f((TOwner)o, v) : v;
-            }
-            else
-            {
-                return null;
-            }
-        }
-
-        
     }
+
     /// <summary>
     /// Class representing the <see cref="AvaloniaProperty.UnsetValue"/>.
     /// </summary>
-    public class UnsetValueType
+    public sealed class UnsetValueType
     {
+        internal UnsetValueType() { }
+
         /// <summary>
         /// Returns the string representation of the <see cref="AvaloniaProperty.UnsetValue"/>.
         /// </summary>

+ 17 - 23
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs

@@ -4,32 +4,20 @@
 using System;
 using Avalonia.Data;
 
+#nullable enable
+
 namespace Avalonia
 {
     /// <summary>
     /// Provides information for a avalonia property change.
     /// </summary>
-    public class AvaloniaPropertyChangedEventArgs : EventArgs
+    public abstract class AvaloniaPropertyChangedEventArgs : EventArgs
     {
-        /// <summary>
-        /// Initializes a new instance of the <see cref="AvaloniaPropertyChangedEventArgs"/> class.
-        /// </summary>
-        /// <param name="sender">The object that the property changed on.</param>
-        /// <param name="property">The property that changed.</param>
-        /// <param name="oldValue">The old value of the property.</param>
-        /// <param name="newValue">The new value of the property.</param>
-        /// <param name="priority">The priority of the binding that produced the value.</param>
         public AvaloniaPropertyChangedEventArgs(
-            AvaloniaObject sender,
-            AvaloniaProperty property,
-            object oldValue,
-            object newValue,
+            IAvaloniaObject sender,
             BindingPriority priority)
         {
             Sender = sender;
-            Property = property;
-            OldValue = oldValue;
-            NewValue = newValue;
             Priority = priority;
         }
 
@@ -37,7 +25,7 @@ namespace Avalonia
         /// Gets the <see cref="AvaloniaObject"/> that the property changed on.
         /// </summary>
         /// <value>The sender object.</value>
-        public AvaloniaObject Sender { get; private set; }
+        public IAvaloniaObject Sender { get; }
 
         /// <summary>
         /// Gets the property that changed.
@@ -45,30 +33,36 @@ namespace Avalonia
         /// <value>
         /// The property that changed.
         /// </value>
-        public AvaloniaProperty Property { get; private set; }
+        public AvaloniaProperty Property => GetProperty();
 
         /// <summary>
         /// Gets the old value of the property.
         /// </summary>
         /// <value>
-        /// The old value of the property.
+        /// The old value of the property or <see cref="AvaloniaProperty.UnsetValue"/> if the
+        /// property previously had no value.
         /// </value>
-        public object OldValue { get; private set; }
+        public object? OldValue => GetOldValue();
 
         /// <summary>
         /// Gets the new value of the property.
         /// </summary>
         /// <value>
-        /// The new value of the property.
+        /// The new value of the property or <see cref="AvaloniaProperty.UnsetValue"/> if the
+        /// property previously had no value.
         /// </value>
-        public object NewValue { get; private set; }
+        public object? NewValue => GetNewValue();
 
         /// <summary>
         /// Gets the priority of the binding that produced the value.
         /// </summary>
         /// <value>
-        /// The priority of the binding that produced the value.
+        /// The priority of the new value.
         /// </value>
         public BindingPriority Priority { get; private set; }
+
+        protected abstract AvaloniaProperty GetProperty();
+        protected abstract object? GetOldValue();
+        protected abstract object? GetNewValue();
     }
 }

+ 67 - 0
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs

@@ -0,0 +1,67 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia
+{
+    /// <summary>
+    /// Provides information for a avalonia property change.
+    /// </summary>
+    public class AvaloniaPropertyChangedEventArgs<T> : AvaloniaPropertyChangedEventArgs
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AvaloniaPropertyChangedEventArgs"/> class.
+        /// </summary>
+        /// <param name="sender">The object that the property changed on.</param>
+        /// <param name="property">The property that changed.</param>
+        /// <param name="oldValue">The old value of the property.</param>
+        /// <param name="newValue">The new value of the property.</param>
+        /// <param name="priority">The priority of the binding that produced the value.</param>
+        public AvaloniaPropertyChangedEventArgs(
+            IAvaloniaObject sender,
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
+            : base(sender, priority)
+        {
+            Property = property;
+            OldValue = oldValue;
+            NewValue = newValue;
+        }
+
+        /// <summary>
+        /// Gets the property that changed.
+        /// </summary>
+        /// <value>
+        /// The property that changed.
+        /// </value>
+        public new AvaloniaProperty<T> Property { get; }
+
+        /// <summary>
+        /// Gets the old value of the property.
+        /// </summary>
+        /// <value>
+        /// The old value of the property.
+        /// </value>
+        public new Optional<T> OldValue { get; private set; }
+
+        /// <summary>
+        /// Gets the new value of the property.
+        /// </summary>
+        /// <value>
+        /// The new value of the property.
+        /// </value>
+        public new BindingValue<T> NewValue { get; private set; }
+
+        protected override AvaloniaProperty GetProperty() => Property;
+
+        protected override object? GetOldValue() => OldValue.GetValueOrDefault(AvaloniaProperty.UnsetValue);
+
+        protected override object? GetNewValue() => NewValue.GetValueOrDefault(AvaloniaProperty.UnsetValue);
+    }
+}

+ 97 - 66
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@@ -20,10 +20,14 @@ namespace Avalonia
             new Dictionary<Type, Dictionary<int, AvaloniaProperty>>();
         private readonly Dictionary<Type, Dictionary<int, AvaloniaProperty>> _attached =
             new Dictionary<Type, Dictionary<int, AvaloniaProperty>>();
+        private readonly Dictionary<Type, Dictionary<int, AvaloniaProperty>> _direct =
+            new Dictionary<Type, Dictionary<int, AvaloniaProperty>>();
         private readonly Dictionary<Type, List<AvaloniaProperty>> _registeredCache =
             new Dictionary<Type, List<AvaloniaProperty>>();
         private readonly Dictionary<Type, List<AvaloniaProperty>> _attachedCache =
             new Dictionary<Type, List<AvaloniaProperty>>();
+        private readonly Dictionary<Type, List<AvaloniaProperty>> _directCache =
+            new Dictionary<Type, List<AvaloniaProperty>>();
         private readonly Dictionary<Type, List<PropertyInitializationData>> _initializedCache =
             new Dictionary<Type, List<PropertyInitializationData>>();
         private readonly Dictionary<Type, List<AvaloniaProperty>> _inheritedCache =
@@ -105,6 +109,37 @@ namespace Avalonia
             return result;
         }
 
+        /// <summary>
+        /// Gets all direct <see cref="AvaloniaProperty"/>s registered on a type.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
+        public IEnumerable<AvaloniaProperty> GetRegisteredDirect(Type type)
+        {
+            Contract.Requires<ArgumentNullException>(type != null);
+
+            if (_directCache.TryGetValue(type, out var result))
+            {
+                return result;
+            }
+
+            var t = type;
+            result = new List<AvaloniaProperty>();
+
+            while (t != null)
+            {
+                if (_direct.TryGetValue(t, out var direct))
+                {
+                    result.AddRange(direct.Values);
+                }
+
+                t = t.BaseType;
+            }
+
+            _directCache.Add(type, result);
+            return result;
+        }
+
         /// <summary>
         /// Gets all inherited <see cref="AvaloniaProperty"/>s registered on a type.
         /// </summary>
@@ -150,13 +185,29 @@ namespace Avalonia
         /// </summary>
         /// <param name="o">The object.</param>
         /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
-        public IEnumerable<AvaloniaProperty> GetRegistered(AvaloniaObject o)
+        public IEnumerable<AvaloniaProperty> GetRegistered(IAvaloniaObject o)
         {
             Contract.Requires<ArgumentNullException>(o != null);
 
             return GetRegistered(o.GetType());
         }
 
+        /// <summary>
+        /// Finds a direct property as registered on an object.
+        /// </summary>
+        /// <param name="o">The object.</param>
+        /// <param name="property">The direct property.</param>
+        /// <returns>
+        /// The registered property or null if no matching property found.
+        /// </returns>
+        public DirectPropertyBase<T> GetRegisteredDirect<T>(
+            IAvaloniaObject o,
+            DirectPropertyBase<T> property)
+        {
+            return FindRegisteredDirect(o, property) ??
+                throw new ArgumentException($"Property '{property.Name} not registered on '{o.GetType()}");
+        }
+
         /// <summary>
         /// Finds a registered property on a type by name.
         /// </summary>
@@ -200,7 +251,7 @@ namespace Avalonia
         /// <exception cref="InvalidOperationException">
         /// The property name contains a '.'.
         /// </exception>
-        public AvaloniaProperty FindRegistered(AvaloniaObject o, string name)
+        public AvaloniaProperty FindRegistered(IAvaloniaObject o, string name)
         {
             Contract.Requires<ArgumentNullException>(o != null);
             Contract.Requires<ArgumentNullException>(name != null);
@@ -208,6 +259,34 @@ namespace Avalonia
             return FindRegistered(o.GetType(), name);
         }
 
+        /// <summary>
+        /// Finds a direct property as registered on an object.
+        /// </summary>
+        /// <param name="o">The object.</param>
+        /// <param name="property">The direct property.</param>
+        /// <returns>
+        /// The registered property or null if no matching property found.
+        /// </returns>
+        public DirectPropertyBase<T> FindRegisteredDirect<T>(
+            IAvaloniaObject o,
+            DirectPropertyBase<T> property)
+        {
+            if (property.Owner == o.GetType())
+            {
+                return property;
+            }
+
+            foreach (var p in GetRegisteredDirect(o.GetType()))
+            {
+                if (p == property)
+                {
+                    return (DirectPropertyBase<T>)p;
+                }
+            }
+
+            return null;
+        }
+
         /// <summary>
         /// Finds a registered property by Id.
         /// </summary>
@@ -273,6 +352,22 @@ namespace Avalonia
                 inner.Add(property.Id, property);
             }
 
+            if (property.IsDirect)
+            {
+                if (!_direct.TryGetValue(type, out inner))
+                {
+                    inner = new Dictionary<int, AvaloniaProperty>();
+                    inner.Add(property.Id, property);
+                    _direct.Add(type, inner);
+                }
+                else if (!inner.ContainsKey(property.Id))
+                {
+                    inner.Add(property.Id, property);
+                }
+
+                _directCache.Clear();
+            }
+
             if (!_properties.ContainsKey(property.Id))
             {
                 _properties.Add(property.Id, property);
@@ -320,70 +415,6 @@ namespace Avalonia
             _inheritedCache.Clear();
         }
 
-        internal void NotifyInitialized(AvaloniaObject o)
-        {
-            Contract.Requires<ArgumentNullException>(o != null);
-
-            var type = o.GetType();
-
-            void Notify(AvaloniaProperty property, object value)
-            {
-                var e = new AvaloniaPropertyChangedEventArgs(
-                    o,
-                    property,
-                    AvaloniaProperty.UnsetValue,
-                    value,
-                    BindingPriority.Unset);
-
-                property.NotifyInitialized(e);
-            }
-
-            if (!_initializedCache.TryGetValue(type, out var initializationData))
-            {
-                var visited = new HashSet<AvaloniaProperty>();
-
-                initializationData = new List<PropertyInitializationData>();
-
-                foreach (AvaloniaProperty property in GetRegistered(type))
-                {
-                    if (property.IsDirect)
-                    {
-                        initializationData.Add(new PropertyInitializationData(property, (IDirectPropertyAccessor)property));
-                    }
-                    else
-                    {
-                        initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type));
-                    }
-
-                    visited.Add(property);
-                }
-
-                foreach (AvaloniaProperty property in GetRegisteredAttached(type))
-                {
-                    if (!visited.Contains(property))
-                    {
-                        initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type));
-
-                        visited.Add(property);
-                    }
-                }
-
-                _initializedCache.Add(type, initializationData);
-            }
-
-            foreach (PropertyInitializationData data in initializationData)
-            {
-                if (!data.Property.HasNotifyInitializedObservers)
-                {
-                    continue;
-                }
-
-                object value = data.IsDirect ? data.DirectAccessor.GetValue(o) : data.Value;
-
-                Notify(data.Property, value);
-            }
-        }
-
         private readonly struct PropertyInitializationData
         {
             public AvaloniaProperty Property { get; }

+ 27 - 1
src/Avalonia.Base/AvaloniaProperty`1.cs

@@ -2,6 +2,8 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using Avalonia.Data;
+using Avalonia.Utilities;
 
 namespace Avalonia
 {
@@ -9,7 +11,7 @@ namespace Avalonia
     /// A typed avalonia property.
     /// </summary>
     /// <typeparam name="TValue">The value type of the property.</typeparam>
-    public class AvaloniaProperty<TValue> : AvaloniaProperty
+    public abstract class AvaloniaProperty<TValue> : AvaloniaProperty
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="AvaloniaProperty{TValue}"/> class.
@@ -40,5 +42,29 @@ namespace Avalonia
             : base(source, ownerType, metadata)
         {
         }
+
+        protected BindingValue<object> TryConvert(object value)
+        {
+            if (value == UnsetValue)
+            {
+                return BindingValue<object>.Unset;
+            }
+            else if (value == BindingOperations.DoNothing)
+            {
+                return BindingValue<object>.DoNothing;
+            }
+
+            if (!TypeUtilities.TryConvertImplicit(PropertyType, value, out var converted))
+            {
+                var error = new ArgumentException(string.Format(
+                    "Invalid value for Property '{0}': '{1}' ({2})",
+                    Name,
+                    value,
+                    value?.GetType().FullName ?? "(null)"));
+                return BindingValue<object>.BindingError(error);
+            }
+
+            return converted;
+        }
     }
 }

+ 0 - 28
src/Avalonia.Base/BoxedValue.cs

@@ -1,28 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-namespace Avalonia
-{
-    /// <summary>
-    /// Represents boxed value of type <typeparamref name="T"/>.
-    /// </summary>
-    /// <typeparam name="T">Type of stored value.</typeparam>
-    internal readonly struct BoxedValue<T>
-    {
-        public BoxedValue(T value)
-        {
-            Boxed = value;
-            Typed = value;
-        }
-
-        /// <summary>
-        /// Boxed value.
-        /// </summary>
-        public object Boxed { get; }
-
-        /// <summary>
-        /// Typed value.
-        /// </summary>
-        public T Typed { get; }
-    }
-}

+ 20 - 0
src/Avalonia.Base/Collections/AvaloniaList.cs

@@ -327,6 +327,8 @@ namespace Avalonia.Collections
                     }
                     else
                     {
+                        EnsureCapacity(_inner.Count + list.Count);
+
                         using (IEnumerator<T> en = items.GetEnumerator())
                         {
                             int insertIndex = index;
@@ -550,6 +552,24 @@ namespace Avalonia.Collections
         /// <inheritdoc/>
         Delegate[] INotifyCollectionChangedDebug.GetCollectionChangedSubscribers() => _collectionChanged?.GetInvocationList();
 
+        private void EnsureCapacity(int capacity)
+        {
+            // Adapted from List<T> implementation.
+            var currentCapacity = _inner.Capacity;
+
+            if (currentCapacity < capacity)
+            {
+                var newCapacity = currentCapacity == 0 ? 4 : currentCapacity * 2;
+
+                if (newCapacity < capacity)
+                {
+                    newCapacity = capacity;
+                }
+
+                _inner.Capacity = newCapacity;
+            }
+        }
+
         /// <summary>
         /// Raises the <see cref="CollectionChanged"/> event with an add action.
         /// </summary>

+ 40 - 0
src/Avalonia.Base/Collections/Pooled/ClearMode.cs

@@ -0,0 +1,40 @@
+// This source file is adapted from the Collections.Pooled.
+// (https://github.com/jtmueller/Collections.Pooled/tree/master/Collections.Pooled/)
+
+namespace Avalonia.Collections.Pooled
+{
+    /// <summary>
+    /// This enum allows control over how data is treated when internal
+    /// arrays are returned to the ArrayPool. Be careful to understand 
+    /// what each option does before using anything other than the default
+    /// of Auto.
+    /// </summary>
+    public enum ClearMode
+    {
+        /// <summary>
+        /// <para><code>Auto</code> has different behavior depending on the host project's target framework.</para>
+        /// <para>.NET Core 2.1: Reference types and value types that contain reference types are cleared
+        /// when the internal arrays are returned to the pool. Value types that do not contain reference
+        /// types are not cleared when returned to the pool.</para>
+        /// <para>.NET Standard 2.0: All user types are cleared before returning to the pool, in case they
+        /// contain reference types.
+        /// For .NET Standard, Auto and Always have the same behavior.</para>
+        /// </summary>
+        Auto = 0,
+        /// <summary>
+        /// The <para><code>Always</code> setting has the effect of always clearing user types before returning to the pool.
+        /// This is the default behavior on .NET Standard.</para><para>You might want to turn this on in a .NET Core project
+        /// if you were concerned about sensitive data stored in value types leaking to other pars of your application.</para> 
+        /// </summary>
+        Always = 1,
+        /// <summary>
+        /// <para><code>Never</code> will cause pooled collections to never clear user types before returning them to the pool.</para>
+        /// <para>You might want to use this setting in a .NET Standard project when you know that a particular collection stores
+        /// only value types and you want the performance benefit of not taking time to reset array items to their default value.</para>
+        /// <para>Be careful with this setting: if used for a collection that contains reference types, or value types that contain
+        /// reference types, this setting could cause memory issues by making the garbage collector unable to clean up instances
+        /// that are still being referenced by arrays sitting in the ArrayPool.</para>
+        /// </summary>
+        Never = 2
+    }
+}

+ 31 - 0
src/Avalonia.Base/Collections/Pooled/ICollectionDebugView.cs

@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace Avalonia.Collections.Pooled
+{
+    internal sealed class ICollectionDebugView<T>
+    {
+        private readonly ICollection<T> _collection;
+
+        public ICollectionDebugView(ICollection<T> collection)
+        {
+            _collection = collection ?? throw new ArgumentNullException(nameof(collection));
+        }
+
+        [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+        public T[] Items
+        {
+            get
+            {
+                T[] items = new T[_collection.Count];
+                _collection.CopyTo(items, 0);
+                return items;
+            }
+        }
+    }
+}

+ 21 - 0
src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs

@@ -0,0 +1,21 @@
+// This source file is adapted from the Collections.Pooled.
+// (https://github.com/jtmueller/Collections.Pooled/tree/master/Collections.Pooled/)
+
+using System;
+using System.Collections.Generic;
+
+namespace Avalonia.Collections.Pooled
+{
+    /// <summary>
+    /// Represents a read-only collection of pooled elements that can be accessed by index
+    /// </summary>
+    /// <typeparam name="T">The type of elements in the read-only pooled list.</typeparam>
+
+    public interface IReadOnlyPooledList<T> : IReadOnlyList<T>
+    {
+        /// <summary>
+        /// Gets a <see cref="System.ReadOnlySpan{T}"/> for the items currently in the collection.
+        /// </summary>
+        ReadOnlySpan<T> Span { get; }
+    }
+}

+ 1531 - 0
src/Avalonia.Base/Collections/Pooled/PooledList.cs

@@ -0,0 +1,1531 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Buffers;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.Serialization;
+using System.Threading;
+
+namespace Avalonia.Collections.Pooled
+{
+    /// <summary>
+    /// Implements a variable-size list that uses a pooled array to store the
+    /// elements. A PooledList has a capacity, which is the allocated length
+    /// of the internal array. As elements are added to a PooledList, the capacity
+    /// of the PooledList is automatically increased as required by reallocating the
+    /// internal array.
+    /// </summary>
+    /// <remarks>
+    /// This class is based on the code for <see cref="List{T}"/> but it supports <see cref="Span{T}"/>
+    /// and uses <see cref="ArrayPool{T}"/> when allocating internal arrays.
+    /// </remarks>
+    [DebuggerDisplay("Count = {Count}")]
+    [DebuggerTypeProxy(typeof(ICollectionDebugView<>))]
+    [Serializable]
+    public class PooledList<T> : IList<T>, IReadOnlyPooledList<T>, IList, IDisposable, IDeserializationCallback
+    {
+        // internal constant copied from Array.MaxArrayLength
+        private const int MaxArrayLength = 0x7FEFFFFF;
+        private const int DefaultCapacity = 4;
+        private static readonly T[] s_emptyArray = Array.Empty<T>();
+
+        [NonSerialized]
+        private ArrayPool<T> _pool;
+        [NonSerialized]
+        private object _syncRoot;
+
+        private T[] _items; // Do not rename (binary serialization)
+        private int _size; // Do not rename (binary serialization)
+        private int _version; // Do not rename (binary serialization)
+        private readonly bool _clearOnFree;
+
+        #region Constructors
+
+        /// <summary>
+        /// Constructs a PooledList. The list is initially empty and has a capacity
+        /// of zero. Upon adding the first element to the list the capacity is
+        /// increased to DefaultCapacity, and then increased in multiples of two
+        /// as required.
+        /// </summary>
+        public PooledList() : this(ClearMode.Auto, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Constructs a PooledList. The list is initially empty and has a capacity
+        /// of zero. Upon adding the first element to the list the capacity is
+        /// increased to DefaultCapacity, and then increased in multiples of two
+        /// as required.
+        /// </summary>
+        public PooledList(ClearMode clearMode) : this(clearMode, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Constructs a PooledList. The list is initially empty and has a capacity
+        /// of zero. Upon adding the first element to the list the capacity is
+        /// increased to DefaultCapacity, and then increased in multiples of two
+        /// as required.
+        /// </summary>
+        public PooledList(ArrayPool<T> customPool) : this(ClearMode.Auto, customPool) { }
+
+        /// <summary>
+        /// Constructs a PooledList. The list is initially empty and has a capacity
+        /// of zero. Upon adding the first element to the list the capacity is
+        /// increased to DefaultCapacity, and then increased in multiples of two
+        /// as required.
+        /// </summary>
+        public PooledList(ClearMode clearMode, ArrayPool<T> customPool)
+        {
+            _items = s_emptyArray;
+            _pool = customPool ?? ArrayPool<T>.Shared;
+            _clearOnFree = ShouldClear(clearMode);
+        }
+
+        /// <summary>
+        /// Constructs a List with a given initial capacity. The list is
+        /// initially empty, but will have room for the given number of elements
+        /// before any reallocations are required.
+        /// </summary>
+        public PooledList(int capacity) : this(capacity, ClearMode.Auto, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Constructs a List with a given initial capacity. The list is
+        /// initially empty, but will have room for the given number of elements
+        /// before any reallocations are required.
+        /// </summary>
+        public PooledList(int capacity, bool sizeToCapacity) : this(capacity, ClearMode.Auto, ArrayPool<T>.Shared, sizeToCapacity) { }
+
+        /// <summary>
+        /// Constructs a List with a given initial capacity. The list is
+        /// initially empty, but will have room for the given number of elements
+        /// before any reallocations are required.
+        /// </summary>
+        public PooledList(int capacity, ClearMode clearMode) : this(capacity, clearMode, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Constructs a List with a given initial capacity. The list is
+        /// initially empty, but will have room for the given number of elements
+        /// before any reallocations are required.
+        /// </summary>
+        public PooledList(int capacity, ClearMode clearMode, bool sizeToCapacity) : this(capacity, clearMode, ArrayPool<T>.Shared, sizeToCapacity) { }
+
+        /// <summary>
+        /// Constructs a List with a given initial capacity. The list is
+        /// initially empty, but will have room for the given number of elements
+        /// before any reallocations are required.
+        /// </summary>
+        public PooledList(int capacity, ArrayPool<T> customPool) : this(capacity, ClearMode.Auto, customPool) { }
+
+        /// <summary>
+        /// Constructs a List with a given initial capacity. The list is
+        /// initially empty, but will have room for the given number of elements
+        /// before any reallocations are required.
+        /// </summary>
+        public PooledList(int capacity, ArrayPool<T> customPool, bool sizeToCapacity) : this(capacity, ClearMode.Auto, customPool, sizeToCapacity) { }
+
+        /// <summary>
+        /// Constructs a List with a given initial capacity. The list is
+        /// initially empty, but will have room for the given number of elements
+        /// before any reallocations are required.
+        /// </summary>
+        public PooledList(int capacity, ClearMode clearMode, ArrayPool<T> customPool) : this(capacity, clearMode, customPool, false) { }
+
+        /// <summary>
+        /// Constructs a List with a given initial capacity. The list is
+        /// initially empty, but will have room for the given number of elements
+        /// before any reallocations are required.
+        /// </summary>
+        /// <param name="sizeToCapacity">If true, Count of list equals capacity. Depending on ClearMode, rented items may or may not hold dirty values.</param>
+        public PooledList(int capacity, ClearMode clearMode, ArrayPool<T> customPool, bool sizeToCapacity)
+        {
+            if (capacity < 0)
+                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
+
+            _pool = customPool ?? ArrayPool<T>.Shared;
+            _clearOnFree = ShouldClear(clearMode);
+
+            if (capacity == 0)
+            {
+                _items = s_emptyArray;
+            }
+            else
+            {
+                _items = _pool.Rent(capacity);
+            }
+
+            if (sizeToCapacity)
+            {
+                _size = capacity;
+                if (clearMode != ClearMode.Never)
+                {
+                    Array.Clear(_items, 0, _size);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Constructs a PooledList, copying the contents of the given collection. The
+        /// size and capacity of the new list will both be equal to the size of the
+        /// given collection.
+        /// </summary>
+        public PooledList(T[] array) : this(array.AsSpan(), ClearMode.Auto, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Constructs a PooledList, copying the contents of the given collection. The
+        /// size and capacity of the new list will both be equal to the size of the
+        /// given collection.
+        /// </summary>
+        public PooledList(T[] array, ClearMode clearMode) : this(array.AsSpan(), clearMode, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Constructs a PooledList, copying the contents of the given collection. The
+        /// size and capacity of the new list will both be equal to the size of the
+        /// given collection.
+        /// </summary>
+        public PooledList(T[] array, ArrayPool<T> customPool) : this(array.AsSpan(), ClearMode.Auto, customPool) { }
+
+        /// <summary>
+        /// Constructs a PooledList, copying the contents of the given collection. The
+        /// size and capacity of the new list will both be equal to the size of the
+        /// given collection.
+        /// </summary>
+        public PooledList(T[] array, ClearMode clearMode, ArrayPool<T> customPool) : this(array.AsSpan(), clearMode, customPool) { }
+
+        /// <summary>
+        /// Constructs a PooledList, copying the contents of the given collection. The
+        /// size and capacity of the new list will both be equal to the size of the
+        /// given collection.
+        /// </summary>
+        public PooledList(ReadOnlySpan<T> span) : this(span, ClearMode.Auto, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Constructs a PooledList, copying the contents of the given collection. The
+        /// size and capacity of the new list will both be equal to the size of the
+        /// given collection.
+        /// </summary>
+        public PooledList(ReadOnlySpan<T> span, ClearMode clearMode) : this(span, clearMode, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Constructs a PooledList, copying the contents of the given collection. The
+        /// size and capacity of the new list will both be equal to the size of the
+        /// given collection.
+        /// </summary>
+        public PooledList(ReadOnlySpan<T> span, ArrayPool<T> customPool) : this(span, ClearMode.Auto, customPool) { }
+
+        /// <summary>
+        /// Constructs a PooledList, copying the contents of the given collection. The
+        /// size and capacity of the new list will both be equal to the size of the
+        /// given collection.
+        /// </summary>
+        public PooledList(ReadOnlySpan<T> span, ClearMode clearMode, ArrayPool<T> customPool)
+        {
+            _pool = customPool ?? ArrayPool<T>.Shared;
+            _clearOnFree = ShouldClear(clearMode);
+
+            int count = span.Length;
+            if (count == 0)
+            {
+                _items = s_emptyArray;
+            }
+            else
+            {
+                _items = _pool.Rent(count);
+                span.CopyTo(_items);
+                _size = count;
+            }
+        }
+
+        /// <summary>
+        /// Constructs a PooledList, copying the contents of the given collection. The
+        /// size and capacity of the new list will both be equal to the size of the
+        /// given collection.
+        /// </summary>
+        public PooledList(IEnumerable<T> collection) : this(collection, ClearMode.Auto, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Constructs a PooledList, copying the contents of the given collection. The
+        /// size and capacity of the new list will both be equal to the size of the
+        /// given collection.
+        /// </summary>
+        public PooledList(IEnumerable<T> collection, ClearMode clearMode) : this(collection, clearMode, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Constructs a PooledList, copying the contents of the given collection. The
+        /// size and capacity of the new list will both be equal to the size of the
+        /// given collection.
+        /// </summary>
+        public PooledList(IEnumerable<T> collection, ArrayPool<T> customPool) : this(collection, ClearMode.Auto, customPool) { }
+
+        /// <summary>
+        /// Constructs a PooledList, copying the contents of the given collection. The
+        /// size and capacity of the new list will both be equal to the size of the
+        /// given collection.
+        /// </summary>
+        public PooledList(IEnumerable<T> collection, ClearMode clearMode, ArrayPool<T> customPool)
+        {
+            _pool = customPool ?? ArrayPool<T>.Shared;
+            _clearOnFree = ShouldClear(clearMode);
+
+            switch (collection)
+            {
+                case null:
+                    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection);
+                    break;
+
+                case ICollection<T> c:
+                    int count = c.Count;
+                    if (count == 0)
+                    {
+                        _items = s_emptyArray;
+                    }
+                    else
+                    {
+                        _items = _pool.Rent(count);
+                        c.CopyTo(_items, 0);
+                        _size = count;
+                    }
+                    break;
+
+                default:
+                    _size = 0;
+                    _items = s_emptyArray;
+                    using (var en = collection.GetEnumerator())
+                    {
+                        while (en.MoveNext())
+                            Add(en.Current);
+                    }
+                    break;
+            }
+        }
+
+        #endregion
+
+        /// <summary>
+        /// Gets a <see cref="System.Span{T}"/> for the items currently in the collection.
+        /// </summary>
+        public Span<T> Span => _items.AsSpan(0, _size);
+
+        /// <inheritdoc/>
+        ReadOnlySpan<T> IReadOnlyPooledList<T>.Span => Span;
+
+        /// <summary>
+        /// Gets and sets the capacity of this list.  The capacity is the size of
+        /// the internal array used to hold items.  When set, the internal 
+        /// Memory of the list is reallocated to the given capacity.
+        /// Note that the return value for this property may be larger than the property was set to.
+        /// </summary>
+        public int Capacity
+        {
+            get => _items.Length;
+            set
+            {
+                if (value < _size)
+                {
+                    ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity);
+                }
+
+                if (value != _items.Length)
+                {
+                    if (value > 0)
+                    {
+                        var newItems = _pool.Rent(value);
+                        if (_size > 0)
+                        {
+                            Array.Copy(_items, newItems, _size);
+                        }
+                        ReturnArray();
+                        _items = newItems;
+                    }
+                    else
+                    {
+                        ReturnArray();
+                        _size = 0;
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Read-only property describing how many elements are in the List.
+        /// </summary>
+        public int Count => _size;
+
+        /// <summary>
+        /// Returns the ClearMode behavior for the collection, denoting whether values are
+        /// cleared from internal arrays before returning them to the pool.
+        /// </summary>
+        public ClearMode ClearMode => _clearOnFree ? ClearMode.Always : ClearMode.Never;
+
+        bool IList.IsFixedSize => false;
+
+        bool ICollection<T>.IsReadOnly => false;
+
+        bool IList.IsReadOnly => false;
+
+        int ICollection.Count => _size;
+
+        bool ICollection.IsSynchronized => false;
+
+        // Synchronization root for this object.
+        object ICollection.SyncRoot
+        {
+            get
+            {
+                if (_syncRoot == null)
+                {
+                    Interlocked.CompareExchange<object>(ref _syncRoot, new object(), null);
+                }
+                return _syncRoot;
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the element at the given index.
+        /// </summary>
+        public T this[int index]
+        {
+            get
+            {
+                // Following trick can reduce the range check by one
+                if ((uint)index >= (uint)_size)
+                {
+                    ThrowHelper.ThrowArgumentOutOfRange_IndexException();
+                }
+                return _items[index];
+            }
+
+            set
+            {
+                if ((uint)index >= (uint)_size)
+                {
+                    ThrowHelper.ThrowArgumentOutOfRange_IndexException();
+                }
+                _items[index] = value;
+                _version++;
+            }
+        }
+
+        private static bool IsCompatibleObject(object value)
+        {
+            // Non-null values are fine.  Only accept nulls if T is a class or Nullable<U>.
+            // Note that default(T) is not equal to null for value types except when T is Nullable<U>. 
+            return ((value is T) || (value == null && default(T) == null));
+        }
+
+        object IList.this[int index]
+        {
+            get
+            {
+                return this[index];
+            }
+            set
+            {
+                ThrowHelper.IfNullAndNullsAreIllegalThenThrow<T>(value, ExceptionArgument.value);
+
+                try
+                {
+                    this[index] = (T)value;
+                }
+                catch (InvalidCastException)
+                {
+                    ThrowHelper.ThrowWrongValueTypeArgumentException(value, typeof(T));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Adds the given object to the end of this list. The size of the list is
+        /// increased by one. If required, the capacity of the list is doubled
+        /// before adding the new element.
+        /// </summary>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public void Add(T item)
+        {
+            _version++;
+            int size = _size;
+            if ((uint)size < (uint)_items.Length)
+            {
+                _size = size + 1;
+                _items[size] = item;
+            }
+            else
+            {
+                AddWithResize(item);
+            }
+        }
+
+        // Non-inline from List.Add to improve its code quality as uncommon path
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        private void AddWithResize(T item)
+        {
+            int size = _size;
+            EnsureCapacity(size + 1);
+            _size = size + 1;
+            _items[size] = item;
+        }
+
+        int IList.Add(object item)
+        {
+            ThrowHelper.IfNullAndNullsAreIllegalThenThrow<T>(item, ExceptionArgument.item);
+
+            try
+            {
+                Add((T)item);
+            }
+            catch (InvalidCastException)
+            {
+                ThrowHelper.ThrowWrongValueTypeArgumentException(item, typeof(T));
+            }
+
+            return Count - 1;
+        }
+
+        /// <summary>
+        /// Adds the elements of the given collection to the end of this list. If
+        /// required, the capacity of the list is increased to twice the previous
+        /// capacity or the new size, whichever is larger.
+        /// </summary>
+        public void AddRange(IEnumerable<T> collection)
+            => InsertRange(_size, collection);
+
+        /// <summary>
+        /// Adds the elements of the given array to the end of this list. If
+        /// required, the capacity of the list is increased to twice the previous
+        /// capacity or the new size, whichever is larger.
+        /// </summary>
+        public void AddRange(T[] array)
+            => AddRange(array.AsSpan());
+
+        /// <summary>
+        /// Adds the elements of the given <see cref="ReadOnlySpan{T}"/> to the end of this list. If
+        /// required, the capacity of the list is increased to twice the previous
+        /// capacity or the new size, whichever is larger.
+        /// </summary>
+        public void AddRange(ReadOnlySpan<T> span)
+        {
+            var newSpan = InsertSpan(_size, span.Length, false);
+            span.CopyTo(newSpan);
+        }
+
+        /// <summary>
+        /// Advances the <see cref="Count"/> by the number of items specified,
+        /// increasing the capacity if required, then returns a Span representing
+        /// the set of items to be added, allowing direct writes to that section
+        /// of the collection.
+        /// </summary>
+        /// <param name="count">The number of items to add.</param>
+        public Span<T> AddSpan(int count)
+            => InsertSpan(_size, count);
+
+        public ReadOnlyCollection<T> AsReadOnly()
+            => new ReadOnlyCollection<T>(this);
+
+        /// <summary>
+        /// Searches a section of the list for a given element using a binary search
+        /// algorithm. 
+        /// </summary>
+        /// 
+        /// <remarks><para>Elements of the list are compared to the search value using
+        /// the given IComparer interface. If comparer is null, elements of
+        /// the list are compared to the search value using the IComparable
+        /// interface, which in that case must be implemented by all elements of the
+        /// list and the given search value. This method assumes that the given
+        /// section of the list is already sorted; if this is not the case, the
+        /// result will be incorrect.</para>
+        ///
+        /// <para>The method returns the index of the given value in the list. If the
+        /// list does not contain the given value, the method returns a negative
+        /// integer. The bitwise complement operator (~) can be applied to a
+        /// negative result to produce the index of the first element (if any) that
+        /// is larger than the given search value. This is also the index at which
+        /// the search value should be inserted into the list in order for the list
+        /// to remain sorted.
+        /// </para></remarks>
+        public int BinarySearch(int index, int count, T item, IComparer<T> comparer)
+        {
+            if (index < 0)
+                ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException();
+            if (count < 0)
+                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
+            if (_size - index < count)
+                ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen);
+
+            return Array.BinarySearch(_items, index, count, item, comparer);
+        }
+
+        /// <summary>
+        /// Searches the list for a given element using a binary search
+        /// algorithm. If the item implements <see cref="IComparable{T}"/>
+        /// then that is used for comparison, otherwise <see cref="Comparer{T}.Default"/> is used.
+        /// </summary>
+        public int BinarySearch(T item)
+            => BinarySearch(0, Count, item, null);
+
+        /// <summary>
+        /// Searches the list for a given element using a binary search
+        /// algorithm. If the item implements <see cref="IComparable{T}"/>
+        /// then that is used for comparison, otherwise <see cref="Comparer{T}.Default"/> is used.
+        /// </summary>
+        public int BinarySearch(T item, IComparer<T> comparer)
+            => BinarySearch(0, Count, item, comparer);
+
+        /// <summary>
+        /// Clears the contents of the PooledList.
+        /// </summary>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public void Clear()
+        {
+            _version++;
+            int size = _size;
+            _size = 0;
+
+            if (size > 0 && _clearOnFree)
+            {
+                // Clear the elements so that the gc can reclaim the references.
+                Array.Clear(_items, 0, _size);
+            }
+        }
+
+        /// <summary>
+        /// Contains returns true if the specified element is in the List.
+        /// It does a linear, O(n) search.  Equality is determined by calling
+        /// EqualityComparer{T}.Default.Equals.
+        /// </summary>
+        public bool Contains(T item)
+        {
+            // PERF: IndexOf calls Array.IndexOf, which internally
+            // calls EqualityComparer<T>.Default.IndexOf, which
+            // is specialized for different types. This
+            // boosts performance since instead of making a
+            // virtual method call each iteration of the loop,
+            // via EqualityComparer<T>.Default.Equals, we
+            // only make one virtual call to EqualityComparer.IndexOf.
+
+            return _size != 0 && IndexOf(item) != -1;
+        }
+
+        bool IList.Contains(object item)
+        {
+            if (IsCompatibleObject(item))
+            {
+                return Contains((T)item);
+            }
+            return false;
+        }
+
+        public PooledList<TOutput> ConvertAll<TOutput>(Func<T, TOutput> converter)
+        {
+            if (converter == null)
+            {
+                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.converter);
+            }
+
+            var list = new PooledList<TOutput>(_size);
+            for (int i = 0; i < _size; i++)
+            {
+                list._items[i] = converter(_items[i]);
+            }
+            list._size = _size;
+            return list;
+        }
+
+        /// <summary>
+        /// Copies this list to the given span.
+        /// </summary>
+        public void CopyTo(Span<T> span)
+        {
+            if (span.Length < Count)
+                throw new ArgumentException("Destination span is shorter than the list to be copied.");
+
+            Span.CopyTo(span);
+        }
+
+        void ICollection<T>.CopyTo(T[] array, int arrayIndex)
+        {
+            Array.Copy(_items, 0, array, arrayIndex, _size);
+        }
+
+        // Copies this List into array, which must be of a 
+        // compatible array type.  
+        void ICollection.CopyTo(Array array, int arrayIndex)
+        {
+            if ((array != null) && (array.Rank != 1))
+            {
+                ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_RankMultiDimNotSupported);
+            }
+
+            try
+            {
+                // Array.Copy will check for NULL.
+                Array.Copy(_items, 0, array, arrayIndex, _size);
+            }
+            catch (ArrayTypeMismatchException)
+            {
+                ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType();
+            }
+        }
+
+        /// <summary>
+        /// Ensures that the capacity of this list is at least the given minimum
+        /// value. If the current capacity of the list is less than min, the
+        /// capacity is increased to twice the current capacity or to min,
+        /// whichever is larger.
+        /// </summary>
+        private void EnsureCapacity(int min)
+        {
+            if (_items.Length < min)
+            {
+                int newCapacity = _items.Length == 0 ? DefaultCapacity : _items.Length * 2;
+                // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow.
+                // Note that this check works even when _items.Length overflowed thanks to the (uint) cast
+                if ((uint)newCapacity > MaxArrayLength)
+                    newCapacity = MaxArrayLength;
+                if (newCapacity < min)
+                    newCapacity = min;
+                Capacity = newCapacity;
+            }
+        }
+
+        public bool Exists(Func<T, bool> match)
+            => FindIndex(match) != -1;
+
+        public bool TryFind(Func<T, bool> match, out T result)
+        {
+            if (match == null)
+                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
+
+            for (int i = 0; i < _size; i++)
+            {
+                if (match(_items[i]))
+                {
+                    result = _items[i];
+                    return true;
+                }
+            }
+
+            result = default;
+            return false;
+        }
+
+        public PooledList<T> FindAll(Func<T, bool> match)
+        {
+            if (match == null)
+                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
+
+            var list = new PooledList<T>();
+            for (int i = 0; i < _size; i++)
+            {
+                if (match(_items[i]))
+                {
+                    list.Add(_items[i]);
+                }
+            }
+            return list;
+        }
+
+        public int FindIndex(Func<T, bool> match)
+            => FindIndex(0, _size, match);
+
+        public int FindIndex(int startIndex, Func<T, bool> match)
+            => FindIndex(startIndex, _size - startIndex, match);
+
+        public int FindIndex(int startIndex, int count, Func<T, bool> match)
+        {
+            if ((uint)startIndex > (uint)_size)
+                ThrowHelper.ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index();
+
+            if (count < 0 || startIndex > _size - count)
+                ThrowHelper.ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count();
+
+            if (match is null)
+                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
+
+            int endIndex = startIndex + count;
+            for (int i = startIndex; i < endIndex; i++)
+            {
+                if (match(_items[i]))
+                    return i;
+            }
+            return -1;
+        }
+
+        public bool TryFindLast(Func<T, bool> match, out T result)
+        {
+            if (match is null)
+            {
+                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
+            }
+
+            for (int i = _size - 1; i >= 0; i--)
+            {
+                if (match(_items[i]))
+                {
+                    result = _items[i];
+                    return true;
+                }
+            }
+
+            result = default;
+            return false;
+        }
+
+        public int FindLastIndex(Func<T, bool> match)
+            => FindLastIndex(_size - 1, _size, match);
+
+        public int FindLastIndex(int startIndex, Func<T, bool> match)
+            => FindLastIndex(startIndex, startIndex + 1, match);
+
+        public int FindLastIndex(int startIndex, int count, Func<T, bool> match)
+        {
+            if (match == null)
+            {
+                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
+            }
+
+            if (_size == 0)
+            {
+                // Special case for 0 length List
+                if (startIndex != -1)
+                {
+                    ThrowHelper.ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index();
+                }
+            }
+            else
+            {
+                // Make sure we're not out of range
+                if ((uint)startIndex >= (uint)_size)
+                {
+                    ThrowHelper.ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index();
+                }
+            }
+
+            // 2nd half of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0.
+            if (count < 0 || startIndex - count + 1 < 0)
+            {
+                ThrowHelper.ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count();
+            }
+
+            int endIndex = startIndex - count;
+            for (int i = startIndex; i > endIndex; i--)
+            {
+                if (match(_items[i]))
+                {
+                    return i;
+                }
+            }
+            return -1;
+        }
+
+        public void ForEach(Action<T> action)
+        {
+            if (action == null)
+            {
+                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.action);
+            }
+
+            int version = _version;
+            for (int i = 0; i < _size; i++)
+            {
+                if (version != _version)
+                {
+                    break;
+                }
+                action(_items[i]);
+            }
+
+            if (version != _version)
+                ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion();
+        }
+
+        /// <summary>
+        /// Returns an enumerator for this list with the given
+        /// permission for removal of elements. If modifications made to the list 
+        /// while an enumeration is in progress, the MoveNext and 
+        /// GetObject methods of the enumerator will throw an exception.
+        /// </summary>
+        public Enumerator GetEnumerator()
+            => new Enumerator(this);
+
+        IEnumerator<T> IEnumerable<T>.GetEnumerator()
+            => new Enumerator(this);
+
+        IEnumerator IEnumerable.GetEnumerator()
+            => new Enumerator(this);
+
+        /// <summary>
+        /// Equivalent to PooledList.Span.Slice(index, count).
+        /// </summary>
+        public Span<T> GetRange(int index, int count)
+        {
+            if (index < 0)
+            {
+                ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException();
+            }
+
+            if (count < 0)
+            {
+                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
+            }
+
+            if (_size - index < count)
+            {
+                ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen);
+            }
+
+            return Span.Slice(index, count);
+        }
+
+        /// <summary>
+        /// Returns the index of the first occurrence of a given value in
+        /// this list. The list is searched forwards from beginning to end.
+        /// </summary>
+        public int IndexOf(T item)
+            => Array.IndexOf(_items, item, 0, _size);
+
+        int IList.IndexOf(object item)
+        {
+            if (IsCompatibleObject(item))
+            {
+                return IndexOf((T)item);
+            }
+            return -1;
+        }
+
+        /// <summary>
+        /// Returns the index of the first occurrence of a given value in a range of
+        /// this list. The list is searched forwards, starting at index
+        /// index and ending at count number of elements. 
+        /// </summary>
+        public int IndexOf(T item, int index)
+        {
+            if (index > _size)
+                ThrowHelper.ThrowArgumentOutOfRange_IndexException();
+            return Array.IndexOf(_items, item, index, _size - index);
+        }
+
+        /// <summary>
+        /// Returns the index of the first occurrence of a given value in a range of
+        /// this list. The list is searched forwards, starting at index
+        /// index and upto count number of elements. 
+        /// </summary>
+        public int IndexOf(T item, int index, int count)
+        {
+            if (index > _size)
+                ThrowHelper.ThrowArgumentOutOfRange_IndexException();
+
+            if (count < 0 || index > _size - count)
+                ThrowHelper.ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count();
+
+            return Array.IndexOf(_items, item, index, count);
+        }
+
+        /// <summary>
+        /// Inserts an element into this list at a given index. The size of the list
+        /// is increased by one. If required, the capacity of the list is doubled
+        /// before inserting the new element.
+        /// </summary>
+        public void Insert(int index, T item)
+        {
+            // Note that insertions at the end are legal.
+            if ((uint)index > (uint)_size)
+            {
+                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_ListInsert);
+            }
+
+            if (_size == _items.Length)
+                EnsureCapacity(_size + 1);
+            if (index < _size)
+            {
+                Array.Copy(_items, index, _items, index + 1, _size - index);
+            }
+            _items[index] = item;
+            _size++;
+            _version++;
+        }
+
+        void IList.Insert(int index, object item)
+        {
+            ThrowHelper.IfNullAndNullsAreIllegalThenThrow<T>(item, ExceptionArgument.item);
+
+            try
+            {
+                Insert(index, (T)item);
+            }
+            catch (InvalidCastException)
+            {
+                ThrowHelper.ThrowWrongValueTypeArgumentException(item, typeof(T));
+            }
+        }
+
+        /// <summary>
+        /// Inserts the elements of the given collection at a given index. If
+        /// required, the capacity of the list is increased to twice the previous
+        /// capacity or the new size, whichever is larger.  Ranges may be added
+        /// to the end of the list by setting index to the List's size.
+        /// </summary>
+        public void InsertRange(int index, IEnumerable<T> collection)
+        {
+            if ((uint)index > (uint)_size)
+            {
+                ThrowHelper.ThrowArgumentOutOfRange_IndexException();
+            }
+
+            switch (collection)
+            {
+                case null:
+                    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection);
+                    break;
+
+                case ICollection<T> c:
+                    int count = c.Count;
+                    if (count > 0)
+                    {
+                        EnsureCapacity(_size + count);
+                        if (index < _size)
+                        {
+                            Array.Copy(_items, index, _items, index + count, _size - index);
+                        }
+
+                        // If we're inserting a List into itself, we want to be able to deal with that.
+                        if (this == c)
+                        {
+                            // Copy first part of _items to insert location
+                            Array.Copy(_items, 0, _items, index, index);
+                            // Copy last part of _items back to inserted location
+                            Array.Copy(_items, index + count, _items, index * 2, _size - index);
+                        }
+                        else
+                        {
+                            c.CopyTo(_items, index);
+                        }
+                        _size += count;
+                    }
+                    break;
+
+                default:
+                    using (var en = collection.GetEnumerator())
+                    {
+                        while (en.MoveNext())
+                        {
+                            Insert(index++, en.Current);
+                        }
+                    }
+                    break;
+            }
+
+            _version++;
+        }
+
+        /// <summary>
+        /// Inserts the elements of the given collection at a given index. If
+        /// required, the capacity of the list is increased to twice the previous
+        /// capacity or the new size, whichever is larger.  Ranges may be added
+        /// to the end of the list by setting index to the List's size.
+        /// </summary>
+        public void InsertRange(int index, ReadOnlySpan<T> span)
+        {
+            var newSpan = InsertSpan(index, span.Length, false);
+            span.CopyTo(newSpan);
+        }
+
+        /// <summary>
+        /// Inserts the elements of the given collection at a given index. If
+        /// required, the capacity of the list is increased to twice the previous
+        /// capacity or the new size, whichever is larger.  Ranges may be added
+        /// to the end of the list by setting index to the List's size.
+        /// </summary>
+        public void InsertRange(int index, T[] array)
+        {
+            if (array is null)
+                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array);
+            InsertRange(index, array.AsSpan());
+        }
+
+        /// <summary>
+        /// Advances the <see cref="Count"/> by the number of items specified,
+        /// increasing the capacity if required, then returns a Span representing
+        /// the set of items to be added, allowing direct writes to that section
+        /// of the collection.
+        /// </summary>
+        public Span<T> InsertSpan(int index, int count)
+            => InsertSpan(index, count, true);
+
+        private Span<T> InsertSpan(int index, int count, bool clearOutput)
+        {
+            EnsureCapacity(_size + count);
+
+            if (index < _size)
+            {
+                Array.Copy(_items, index, _items, index + count, _size - index);
+            }
+
+            _size += count;
+            _version++;
+
+            var output = _items.AsSpan(index, count);
+
+            if (clearOutput && _clearOnFree)
+            {
+                output.Clear();
+            }
+
+            return output;
+        }
+
+        /// <summary>
+        /// Returns the index of the last occurrence of a given value in a range of
+        /// this list. The list is searched backwards, starting at the end 
+        /// and ending at the first element in the list.
+        /// </summary>
+        public int LastIndexOf(T item)
+        {
+            if (_size == 0)
+            {  // Special case for empty list
+                return -1;
+            }
+            else
+            {
+                return LastIndexOf(item, _size - 1, _size);
+            }
+        }
+
+        /// <summary>
+        /// Returns the index of the last occurrence of a given value in a range of
+        /// this list. The list is searched backwards, starting at index
+        /// index and ending at the first element in the list.
+        /// </summary>
+        public int LastIndexOf(T item, int index)
+        {
+            if (index >= _size)
+                ThrowHelper.ThrowArgumentOutOfRange_IndexException();
+            return LastIndexOf(item, index, index + 1);
+        }
+
+        /// <summary>
+        /// Returns the index of the last occurrence of a given value in a range of
+        /// this list. The list is searched backwards, starting at index
+        /// index and upto count elements
+        /// </summary>
+        public int LastIndexOf(T item, int index, int count)
+        {
+            if (Count != 0 && index < 0)
+            {
+                ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException();
+            }
+
+            if (Count != 0 && count < 0)
+            {
+                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
+            }
+
+            if (_size == 0)
+            {
+                // Special case for empty list
+                return -1;
+            }
+
+            if (index >= _size)
+            {
+                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_BiggerThanCollection);
+            }
+
+            if (count > index + 1)
+            {
+                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_BiggerThanCollection);
+            }
+
+            return Array.LastIndexOf(_items, item, index, count);
+        }
+
+        // Removes the element at the given index. The size of the list is
+        // decreased by one.
+        public bool Remove(T item)
+        {
+            int index = IndexOf(item);
+            if (index >= 0)
+            {
+                RemoveAt(index);
+                return true;
+            }
+
+            return false;
+        }
+
+        void IList.Remove(object item)
+        {
+            if (IsCompatibleObject(item))
+            {
+                Remove((T)item);
+            }
+        }
+
+        /// <summary>
+        /// This method removes all items which match the predicate.
+        /// The complexity is O(n).
+        /// </summary>
+        public int RemoveAll(Func<T, bool> match)
+        {
+            if (match == null)
+                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
+
+            int freeIndex = 0;   // the first free slot in items array
+
+            // Find the first item which needs to be removed.
+            while (freeIndex < _size && !match(_items[freeIndex]))
+                freeIndex++;
+            if (freeIndex >= _size)
+                return 0;
+
+            int current = freeIndex + 1;
+            while (current < _size)
+            {
+                // Find the first item which needs to be kept.
+                while (current < _size && match(_items[current]))
+                    current++;
+
+                if (current < _size)
+                {
+                    // copy item to the free slot.
+                    _items[freeIndex++] = _items[current++];
+                }
+            }
+
+            if (_clearOnFree)
+            {
+                // Clear the removed elements so that the gc can reclaim the references.
+                Array.Clear(_items, freeIndex, _size - freeIndex);
+            }
+
+            int result = _size - freeIndex;
+            _size = freeIndex;
+            _version++;
+            return result;
+        }
+
+        /// <summary>
+        /// Removes the element at the given index. The size of the list is
+        /// decreased by one.
+        /// </summary>
+        public void RemoveAt(int index)
+        {
+            if ((uint)index >= (uint)_size)
+                ThrowHelper.ThrowArgumentOutOfRange_IndexException();
+
+            _size--;
+            if (index < _size)
+            {
+                Array.Copy(_items, index + 1, _items, index, _size - index);
+            }
+            _version++;
+
+            if (_clearOnFree)
+            {
+                // Clear the removed element so that the gc can reclaim the reference.
+                _items[_size] = default;
+            }
+        }
+
+        /// <summary>
+        /// Removes a range of elements from this list.
+        /// </summary>
+        public void RemoveRange(int index, int count)
+        {
+            if (index < 0)
+                ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException();
+
+            if (count < 0)
+                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
+
+            if (_size - index < count)
+                ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen);
+
+            if (count > 0)
+            {
+                _size -= count;
+                if (index < _size)
+                {
+                    Array.Copy(_items, index + count, _items, index, _size - index);
+                }
+
+                _version++;
+
+                if (_clearOnFree)
+                {
+                    // Clear the removed elements so that the gc can reclaim the references.
+                    Array.Clear(_items, _size, count);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Reverses the elements in this list.
+        /// </summary>
+        public void Reverse()
+            => Reverse(0, _size);
+
+        /// <summary>
+        /// Reverses the elements in a range of this list. Following a call to this
+        /// method, an element in the range given by index and count
+        /// which was previously located at index i will now be located at
+        /// index index + (index + count - i - 1).
+        /// </summary>
+        public void Reverse(int index, int count)
+        {
+            if (index < 0)
+                ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException();
+
+            if (count < 0)
+                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
+
+            if (_size - index < count)
+                ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen);
+
+            if (count > 1)
+            {
+                Array.Reverse(_items, index, count);
+            }
+            _version++;
+        }
+
+        /// <summary>
+        /// Sorts the elements in this list.  Uses the default comparer and 
+        /// Array.Sort.
+        /// </summary>
+        public void Sort()
+            => Sort(0, Count, null);
+
+        /// <summary>
+        /// Sorts the elements in this list.  Uses Array.Sort with the
+        /// provided comparer.
+        /// </summary>
+        /// <param name="comparer"></param>
+        public void Sort(IComparer<T> comparer)
+            => Sort(0, Count, comparer);
+
+        /// <summary>
+        /// Sorts the elements in a section of this list. The sort compares the
+        /// elements to each other using the given IComparer interface. If
+        /// comparer is null, the elements are compared to each other using
+        /// the IComparable interface, which in that case must be implemented by all
+        /// elements of the list.
+        /// 
+        /// This method uses the Array.Sort method to sort the elements.
+        /// </summary>
+        public void Sort(int index, int count, IComparer<T> comparer)
+        {
+            if (index < 0)
+                ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException();
+
+            if (count < 0)
+                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
+
+            if (_size - index < count)
+                ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen);
+
+            if (count > 1)
+            {
+                Array.Sort(_items, index, count, comparer);
+            }
+            _version++;
+        }
+
+        public void Sort(Func<T, T, int> comparison)
+        {
+            if (comparison == null)
+            {
+                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.comparison);
+            }
+
+            if (_size > 1)
+            {
+                // List<T> uses ArraySortHelper here but since it's an internal class,
+                // we're creating an IComparer<T> using the comparison function to avoid
+                // duplicating all that code.
+                Array.Sort(_items, 0, _size, new Comparer(comparison));
+            }
+            _version++;
+        }
+
+        /// <summary>
+        /// ToArray returns an array containing the contents of the List.
+        /// This requires copying the List, which is an O(n) operation.
+        /// </summary>
+        public T[] ToArray()
+        {
+            if (_size == 0)
+            {
+                return s_emptyArray;
+            }
+
+            return Span.ToArray();
+        }
+
+        /// <summary>
+        /// Sets the capacity of this list to the size of the list. This method can
+        /// be used to minimize a list's memory overhead once it is known that no
+        /// new elements will be added to the list. To completely clear a list and
+        /// release all memory referenced by the list, execute the following
+        /// statements:
+        /// <code>
+        /// list.Clear();
+        /// list.TrimExcess();
+        /// </code>
+        /// </summary>
+        public void TrimExcess()
+        {
+            int threshold = (int)(_items.Length * 0.9);
+            if (_size < threshold)
+            {
+                Capacity = _size;
+            }
+        }
+
+        public bool TrueForAll(Func<T, bool> match)
+        {
+            if (match == null)
+            {
+                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
+            }
+
+            for (int i = 0; i < _size; i++)
+            {
+                if (!match(_items[i]))
+                {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        private void ReturnArray()
+        {
+            if (_items.Length == 0)
+                return;
+
+            try
+            {
+                // Clear the elements so that the gc can reclaim the references.
+                _pool.Return(_items, clearArray: _clearOnFree);
+            }
+            catch (ArgumentException)
+            {
+                // oh well, the array pool didn't like our array
+            }
+
+            _items = s_emptyArray;
+        }
+
+        private static bool ShouldClear(ClearMode mode)
+        {
+#if NETCOREAPP2_1
+            return mode == ClearMode.Always
+                || (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences<T>());
+#else
+            return mode != ClearMode.Never;
+#endif
+        }
+
+        /// <summary>
+        /// Returns the internal buffers to the ArrayPool.
+        /// </summary>
+        public void Dispose()
+        {
+            ReturnArray();
+            _size = 0;
+            _version++;
+        }
+
+        void IDeserializationCallback.OnDeserialization(object sender)
+        {
+            // We can't serialize array pools, so deserialized PooledLists will
+            // have to use the shared pool, even if they were using a custom pool
+            // before serialization.
+            _pool = ArrayPool<T>.Shared;
+        }
+
+        public struct Enumerator : IEnumerator<T>, IEnumerator
+        {
+            private readonly PooledList<T> _list;
+            private int _index;
+            private readonly int _version;
+            private T _current;
+
+            internal Enumerator(PooledList<T> list)
+            {
+                _list = list;
+                _index = 0;
+                _version = list._version;
+                _current = default;
+            }
+
+            public void Dispose()
+            {
+            }
+
+            public bool MoveNext()
+            {
+                var localList = _list;
+
+                if (_version == localList._version && ((uint)_index < (uint)localList._size))
+                {
+                    _current = localList._items[_index];
+                    _index++;
+                    return true;
+                }
+                return MoveNextRare();
+            }
+
+            private bool MoveNextRare()
+            {
+                if (_version != _list._version)
+                {
+                    ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion();
+                }
+
+                _index = _list._size + 1;
+                _current = default;
+                return false;
+            }
+
+            public T Current => _current;
+
+            object IEnumerator.Current
+            {
+                get
+                {
+                    if (_index == 0 || _index == _list._size + 1)
+                    {
+                        ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen();
+                    }
+                    return Current;
+                }
+            }
+
+            void IEnumerator.Reset()
+            {
+                if (_version != _list._version)
+                {
+                    ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion();
+                }
+
+                _index = 0;
+                _current = default;
+            }
+        }
+
+        private readonly struct Comparer : IComparer<T>
+        {
+            private readonly Func<T, T, int> _comparison;
+
+            public Comparer(Func<T, T, int> comparison)
+            {
+                _comparison = comparison;
+            }
+
+            public int Compare(T x, T y) => _comparison(x, y);
+        }
+    }
+}

+ 699 - 0
src/Avalonia.Base/Collections/Pooled/PooledStack.cs

@@ -0,0 +1,699 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+/*=============================================================================
+**
+**
+** Purpose: An array implementation of a generic stack.
+**
+**
+=============================================================================*/
+
+using System;
+using System.Buffers;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Runtime.Serialization;
+using System.Threading;
+
+namespace Avalonia.Collections.Pooled
+{
+    /// <summary>
+    /// A simple stack of objects.  Internally it is implemented as an array,
+    /// so Push can be O(n).  Pop is O(1).
+    /// </summary>
+    [DebuggerTypeProxy(typeof(StackDebugView<>))]
+    [DebuggerDisplay("Count = {Count}")]
+    [Serializable]
+    public class PooledStack<T> : IEnumerable<T>, ICollection, IReadOnlyCollection<T>, IDisposable, IDeserializationCallback
+    {
+        [NonSerialized]
+        private ArrayPool<T> _pool;
+        [NonSerialized]
+        private object _syncRoot;
+
+        private T[] _array; // Storage for stack elements. Do not rename (binary serialization)
+        private int _size; // Number of items in the stack. Do not rename (binary serialization)
+        private int _version; // Used to keep enumerator in sync w/ collection. Do not rename (binary serialization)
+        private readonly bool _clearOnFree;
+
+        private const int DefaultCapacity = 4;
+
+        #region Constructors
+
+        /// <summary>
+        /// Create a stack with the default initial capacity. 
+        /// </summary>
+        public PooledStack() : this(ClearMode.Auto, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Create a stack with the default initial capacity. 
+        /// </summary>
+        public PooledStack(ClearMode clearMode) : this(clearMode, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Create a stack with the default initial capacity. 
+        /// </summary>
+        public PooledStack(ArrayPool<T> customPool) : this(ClearMode.Auto, customPool) { }
+
+        /// <summary>
+        /// Create a stack with the default initial capacity and a custom ArrayPool.
+        /// </summary>
+        public PooledStack(ClearMode clearMode, ArrayPool<T> customPool)
+        {
+            _pool = customPool ?? ArrayPool<T>.Shared;
+            _array = Array.Empty<T>();
+            _clearOnFree = ShouldClear(clearMode);
+        }
+
+        /// <summary>
+        /// Create a stack with a specific initial capacity.  The initial capacity
+        /// must be a non-negative number.
+        /// </summary>
+        public PooledStack(int capacity) : this(capacity, ClearMode.Auto, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Create a stack with a specific initial capacity.  The initial capacity
+        /// must be a non-negative number.
+        /// </summary>
+        public PooledStack(int capacity, ClearMode clearMode) : this(capacity, clearMode, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Create a stack with a specific initial capacity.  The initial capacity
+        /// must be a non-negative number.
+        /// </summary>
+        public PooledStack(int capacity, ArrayPool<T> customPool) : this(capacity, ClearMode.Auto, customPool) { }
+
+        /// <summary>
+        /// Create a stack with a specific initial capacity.  The initial capacity
+        /// must be a non-negative number.
+        /// </summary>
+        public PooledStack(int capacity, ClearMode clearMode, ArrayPool<T> customPool)
+        {
+            if (capacity < 0)
+            {
+                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity,
+                    ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
+            }
+            _pool = customPool ?? ArrayPool<T>.Shared;
+            _array = _pool.Rent(capacity);
+            _clearOnFree = ShouldClear(clearMode);
+        }
+
+        /// <summary>
+        /// Fills a Stack with the contents of a particular collection.  The items are
+        /// pushed onto the stack in the same order they are read by the enumerator.
+        /// </summary>
+        public PooledStack(IEnumerable<T> enumerable) : this(enumerable, ClearMode.Auto, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Fills a Stack with the contents of a particular collection.  The items are
+        /// pushed onto the stack in the same order they are read by the enumerator.
+        /// </summary>
+        public PooledStack(IEnumerable<T> enumerable, ClearMode clearMode) : this(enumerable, clearMode, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Fills a Stack with the contents of a particular collection.  The items are
+        /// pushed onto the stack in the same order they are read by the enumerator.
+        /// </summary>
+        public PooledStack(IEnumerable<T> enumerable, ArrayPool<T> customPool) : this(enumerable, ClearMode.Auto, customPool) { }
+
+        /// <summary>
+        /// Fills a Stack with the contents of a particular collection.  The items are
+        /// pushed onto the stack in the same order they are read by the enumerator.
+        /// </summary>
+        public PooledStack(IEnumerable<T> enumerable, ClearMode clearMode, ArrayPool<T> customPool)
+        {
+            _pool = customPool ?? ArrayPool<T>.Shared;
+            _clearOnFree = ShouldClear(clearMode);
+
+            switch (enumerable)
+            {
+                case null:
+                    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.enumerable);
+                    break;
+
+                case ICollection<T> collection:
+                    if (collection.Count == 0)
+                    {
+                        _array = Array.Empty<T>();
+                    }
+                    else
+                    {
+                        _array = _pool.Rent(collection.Count);
+                        collection.CopyTo(_array, 0);
+                        _size = collection.Count;
+                    }
+                    break;
+
+                default:
+                    using (var list = new PooledList<T>(enumerable))
+                    {
+                        _array = _pool.Rent(list.Count);
+                        list.Span.CopyTo(_array);
+                        _size = list.Count;
+                    }
+                    break;
+            }
+        }
+
+        /// <summary>
+        /// Fills a Stack with the contents of a particular collection.  The items are
+        /// pushed onto the stack in the same order they are read by the enumerator.
+        /// </summary>
+        public PooledStack(T[] array) : this(array.AsSpan(), ClearMode.Auto, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Fills a Stack with the contents of a particular collection.  The items are
+        /// pushed onto the stack in the same order they are read by the enumerator.
+        /// </summary>
+        public PooledStack(T[] array, ClearMode clearMode) : this(array.AsSpan(), clearMode, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Fills a Stack with the contents of a particular collection.  The items are
+        /// pushed onto the stack in the same order they are read by the enumerator.
+        /// </summary>
+        public PooledStack(T[] array, ArrayPool<T> customPool) : this(array.AsSpan(), ClearMode.Auto, customPool) { }
+
+        /// <summary>
+        /// Fills a Stack with the contents of a particular collection.  The items are
+        /// pushed onto the stack in the same order they are read by the enumerator.
+        /// </summary>
+        public PooledStack(T[] array, ClearMode clearMode, ArrayPool<T> customPool) : this(array.AsSpan(), clearMode, customPool) { }
+
+        /// <summary>
+        /// Fills a Stack with the contents of a particular collection.  The items are
+        /// pushed onto the stack in the same order they are read by the enumerator.
+        /// </summary>
+        public PooledStack(ReadOnlySpan<T> span) : this(span, ClearMode.Auto, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Fills a Stack with the contents of a particular collection.  The items are
+        /// pushed onto the stack in the same order they are read by the enumerator.
+        /// </summary>
+        public PooledStack(ReadOnlySpan<T> span, ClearMode clearMode) : this(span, clearMode, ArrayPool<T>.Shared) { }
+
+        /// <summary>
+        /// Fills a Stack with the contents of a particular collection.  The items are
+        /// pushed onto the stack in the same order they are read by the enumerator.
+        /// </summary>
+        public PooledStack(ReadOnlySpan<T> span, ArrayPool<T> customPool) : this(span, ClearMode.Auto, customPool) { }
+
+        /// <summary>
+        /// Fills a Stack with the contents of a particular collection.  The items are
+        /// pushed onto the stack in the same order they are read by the enumerator.
+        /// </summary>
+        public PooledStack(ReadOnlySpan<T> span, ClearMode clearMode, ArrayPool<T> customPool)
+        {
+            _pool = customPool ?? ArrayPool<T>.Shared;
+            _clearOnFree = ShouldClear(clearMode);
+            _array = _pool.Rent(span.Length);
+            span.CopyTo(_array);
+            _size = span.Length;
+        }
+
+        #endregion
+
+        /// <summary>
+        /// The number of items in the stack.
+        /// </summary>
+        public int Count => _size;
+
+        /// <summary>
+        /// Returns the ClearMode behavior for the collection, denoting whether values are
+        /// cleared from internal arrays before returning them to the pool.
+        /// </summary>
+        public ClearMode ClearMode => _clearOnFree ? ClearMode.Always : ClearMode.Never;
+
+        bool ICollection.IsSynchronized => false;
+
+        object ICollection.SyncRoot
+        {
+            get
+            {
+                if (_syncRoot == null)
+                {
+                    Interlocked.CompareExchange<object>(ref _syncRoot, new object(), null);
+                }
+                return _syncRoot;
+            }
+        }
+
+        /// <summary>
+        /// Removes all Objects from the Stack.
+        /// </summary>
+        public void Clear()
+        {
+            if (_clearOnFree)
+            {
+                Array.Clear(_array, 0, _size); // clear the elements so that the gc can reclaim the references.
+            }
+            _size = 0;
+            _version++;
+        }
+
+        /// <summary>
+        /// Compares items using the default equality comparer
+        /// </summary>
+        public bool Contains(T item)
+        {
+            // PERF: Internally Array.LastIndexOf calls
+            // EqualityComparer<T>.Default.LastIndexOf, which
+            // is specialized for different types. This
+            // boosts performance since instead of making a
+            // virtual method call each iteration of the loop,
+            // via EqualityComparer<T>.Default.Equals, we
+            // only make one virtual call to EqualityComparer.LastIndexOf.
+
+            return _size != 0 && Array.LastIndexOf(_array, item, _size - 1) != -1;
+        }
+
+        /// <summary>
+        /// This method removes all items which match the predicate.
+        /// The complexity is O(n).
+        /// </summary>
+        public int RemoveWhere(Func<T, bool> match)
+        {
+            if (match == null)
+                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
+
+            int freeIndex = 0;   // the first free slot in items array
+
+            // Find the first item which needs to be removed.
+            while (freeIndex < _size && !match(_array[freeIndex]))
+                freeIndex++;
+            if (freeIndex >= _size)
+                return 0;
+
+            int current = freeIndex + 1;
+            while (current < _size)
+            {
+                // Find the first item which needs to be kept.
+                while (current < _size && match(_array[current]))
+                    current++;
+
+                if (current < _size)
+                {
+                    // copy item to the free slot.
+                    _array[freeIndex++] = _array[current++];
+                }
+            }
+
+            if (_clearOnFree)
+            {
+                // Clear the removed elements so that the gc can reclaim the references.
+                Array.Clear(_array, freeIndex, _size - freeIndex);
+            }
+
+            int result = _size - freeIndex;
+            _size = freeIndex;
+            _version++;
+            return result;
+        }
+
+        // Copies the stack into an array.
+        public void CopyTo(T[] array, int arrayIndex)
+        {
+            if (array == null)
+            {
+                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array);
+            }
+
+            if (arrayIndex < 0 || arrayIndex > array.Length)
+            {
+                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.arrayIndex);
+            }
+
+            if (array.Length - arrayIndex < _size)
+            {
+                ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall);
+            }
+
+            Debug.Assert(array != _array);
+            int srcIndex = 0;
+            int dstIndex = arrayIndex + _size;
+            while (srcIndex < _size)
+            {
+                array[--dstIndex] = _array[srcIndex++];
+            }
+        }
+
+        public void CopyTo(Span<T> span)
+        {
+            if (span.Length < _size)
+            {
+                ThrowHelper.ThrowArgumentException_DestinationTooShort();
+            }
+
+            int srcIndex = 0;
+            int dstIndex = _size;
+            while (srcIndex < _size)
+            {
+                span[--dstIndex] = _array[srcIndex++];
+            }
+        }
+
+        void ICollection.CopyTo(Array array, int arrayIndex)
+        {
+            if (array == null)
+            {
+                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array);
+            }
+
+            if (array.Rank != 1)
+            {
+                ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_RankMultiDimNotSupported);
+            }
+
+            if (array.GetLowerBound(0) != 0)
+            {
+                ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_NonZeroLowerBound, ExceptionArgument.array);
+            }
+
+            if (arrayIndex < 0 || arrayIndex > array.Length)
+            {
+                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.arrayIndex);
+            }
+
+            if (array.Length - arrayIndex < _size)
+            {
+                ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen);
+            }
+
+            try
+            {
+                Array.Copy(_array, 0, array, arrayIndex, _size);
+                Array.Reverse(array, arrayIndex, _size);
+            }
+            catch (ArrayTypeMismatchException)
+            {
+                ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType();
+            }
+        }
+
+        /// <summary>
+        /// Returns an IEnumerator for this PooledStack.
+        /// </summary>
+        /// <returns></returns>
+        public Enumerator GetEnumerator()
+            => new Enumerator(this);
+
+        /// <internalonly/>
+        IEnumerator<T> IEnumerable<T>.GetEnumerator()
+            => new Enumerator(this);
+
+        IEnumerator IEnumerable.GetEnumerator()
+            => new Enumerator(this);
+
+        public void TrimExcess()
+        {
+            if (_size == 0)
+            {
+                ReturnArray(replaceWith: Array.Empty<T>());
+                _version++;
+                return;
+            }
+
+            int threshold = (int)(_array.Length * 0.9);
+            if (_size < threshold)
+            {
+                var newArray = _pool.Rent(_size);
+                if (newArray.Length < _array.Length)
+                {
+                    Array.Copy(_array, newArray, _size);
+                    ReturnArray(replaceWith: newArray);
+                    _version++;
+                }
+                else
+                {
+                    // The array from the pool wasn't any smaller than the one we already had,
+                    // (we can only control minimum size) so return it and do nothing.
+                    // If we create an exact-sized array not from the pool, we'll
+                    // get an exception when returning it to the pool.
+                    _pool.Return(newArray);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Returns the top object on the stack without removing it.  If the stack
+        /// is empty, Peek throws an InvalidOperationException.
+        /// </summary>
+        public T Peek()
+        {
+            int size = _size - 1;
+            T[] array = _array;
+
+            if ((uint)size >= (uint)array.Length)
+            {
+                ThrowForEmptyStack();
+            }
+
+            return array[size];
+        }
+
+        public bool TryPeek(out T result)
+        {
+            int size = _size - 1;
+            T[] array = _array;
+
+            if ((uint)size >= (uint)array.Length)
+            {
+                result = default;
+                return false;
+            }
+            result = array[size];
+            return true;
+        }
+
+        /// <summary>
+        /// Pops an item from the top of the stack.  If the stack is empty, Pop
+        /// throws an InvalidOperationException.
+        /// </summary>
+        public T Pop()
+        {
+            int size = _size - 1;
+            T[] array = _array;
+
+            // if (_size == 0) is equivalent to if (size == -1), and this case
+            // is covered with (uint)size, thus allowing bounds check elimination 
+            // https://github.com/dotnet/coreclr/pull/9773
+            if ((uint)size >= (uint)array.Length)
+            {
+                ThrowForEmptyStack();
+            }
+
+            _version++;
+            _size = size;
+            T item = array[size];
+            if (_clearOnFree)
+            {
+                array[size] = default;     // Free memory quicker.
+            }
+            return item;
+        }
+
+        public bool TryPop(out T result)
+        {
+            int size = _size - 1;
+            T[] array = _array;
+
+            if ((uint)size >= (uint)array.Length)
+            {
+                result = default;
+                return false;
+            }
+
+            _version++;
+            _size = size;
+            result = array[size];
+            if (_clearOnFree)
+            {
+                array[size] = default;     // Free memory quicker.
+            }
+            return true;
+        }
+
+        /// <summary>
+        /// Pushes an item to the top of the stack.
+        /// </summary>
+        public void Push(T item)
+        {
+            int size = _size;
+            T[] array = _array;
+
+            if ((uint)size < (uint)array.Length)
+            {
+                array[size] = item;
+                _version++;
+                _size = size + 1;
+            }
+            else
+            {
+                PushWithResize(item);
+            }
+        }
+
+        // Non-inline from Stack.Push to improve its code quality as uncommon path
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        private void PushWithResize(T item)
+        {
+            var newArray = _pool.Rent((_array.Length == 0) ? DefaultCapacity : 2 * _array.Length);
+            Array.Copy(_array, newArray, _size);
+            ReturnArray(replaceWith: newArray);
+            _array[_size] = item;
+            _version++;
+            _size++;
+        }
+
+        /// <summary>
+        /// Copies the Stack to an array, in the same order Pop would return the items.
+        /// </summary>
+        public T[] ToArray()
+        {
+            if (_size == 0)
+                return Array.Empty<T>();
+
+            T[] objArray = new T[_size];
+            int i = 0;
+            while (i < _size)
+            {
+                objArray[i] = _array[_size - i - 1];
+                i++;
+            }
+            return objArray;
+        }
+
+        private void ThrowForEmptyStack()
+        {
+            Debug.Assert(_size == 0);
+            throw new InvalidOperationException("Stack was empty.");
+        }
+
+        private void ReturnArray(T[] replaceWith = null)
+        {
+            if (_array?.Length > 0)
+            {
+                try
+                {
+                    _pool.Return(_array, clearArray: _clearOnFree);
+                }
+                catch (ArgumentException)
+                {
+                    // oh well, the array pool didn't like our array
+                }
+            }
+
+            if (!(replaceWith is null))
+            {
+                _array = replaceWith;
+            }
+        }
+
+        private static bool ShouldClear(ClearMode mode)
+        {
+#if NETCOREAPP2_1
+            return mode == ClearMode.Always
+                || (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences<T>());
+#else
+            return mode != ClearMode.Never;
+#endif
+        }
+
+        public void Dispose()
+        {
+            ReturnArray(replaceWith: Array.Empty<T>());
+            _size = 0;
+            _version++;
+        }
+
+        void IDeserializationCallback.OnDeserialization(object sender)
+        {
+            // We can't serialize array pools, so deserialized PooledStacks will
+            // have to use the shared pool, even if they were using a custom pool
+            // before serialization.
+            _pool = ArrayPool<T>.Shared;
+        }
+
+        [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "not an expected scenario")]
+        public struct Enumerator : IEnumerator<T>, IEnumerator
+        {
+            private readonly PooledStack<T> _stack;
+            private readonly int _version;
+            private int _index;
+            private T _currentElement;
+
+            internal Enumerator(PooledStack<T> stack)
+            {
+                _stack = stack;
+                _version = stack._version;
+                _index = -2;
+                _currentElement = default;
+            }
+
+            public void Dispose()
+            {
+                _index = -1;
+            }
+
+            public bool MoveNext()
+            {
+                bool retval;
+                if (_version != _stack._version)
+                    throw new InvalidOperationException("Collection was modified during enumeration.");
+                if (_index == -2)
+                {  // First call to enumerator.
+                    _index = _stack._size - 1;
+                    retval = (_index >= 0);
+                    if (retval)
+                        _currentElement = _stack._array[_index];
+                    return retval;
+                }
+                if (_index == -1)
+                {  // End of enumeration.
+                    return false;
+                }
+
+                retval = (--_index >= 0);
+                if (retval)
+                    _currentElement = _stack._array[_index];
+                else
+                    _currentElement = default;
+                return retval;
+            }
+
+            public T Current
+            {
+                get
+                {
+                    if (_index < 0)
+                        ThrowEnumerationNotStartedOrEnded();
+                    return _currentElement;
+                }
+            }
+
+            private void ThrowEnumerationNotStartedOrEnded()
+            {
+                Debug.Assert(_index == -1 || _index == -2);
+                throw new InvalidOperationException(_index == -2 ? "Enumeration was not started." : "Enumeration has ended.");
+            }
+
+            object IEnumerator.Current
+            {
+                get { return Current; }
+            }
+
+            void IEnumerator.Reset()
+            {
+                if (_version != _stack._version)
+                    throw new InvalidOperationException("Collection was modified during enumeration.");
+                _index = -2;
+                _currentElement = default;
+            }
+        }
+    }
+}

+ 28 - 0
src/Avalonia.Base/Collections/Pooled/StackDebugView.cs

@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Diagnostics;
+
+namespace Avalonia.Collections.Pooled
+{
+    internal sealed class StackDebugView<T>
+    {
+        private readonly PooledStack<T> _stack;
+
+        public StackDebugView(PooledStack<T> stack)
+        {
+            _stack = stack ?? throw new ArgumentNullException(nameof(stack));
+        }
+
+        [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+        public T[] Items
+        {
+            get
+            {
+                return _stack.ToArray();
+            }
+        }
+    }
+}

+ 691 - 0
src/Avalonia.Base/Collections/Pooled/ThrowHelper.cs

@@ -0,0 +1,691 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+// This file defines an internal class used to throw exceptions in BCL code.
+// The main purpose is to reduce code size.
+//
+// The old way to throw an exception generates quite a lot IL code and assembly code.
+// Following is an example:
+//     C# source
+//          throw new ArgumentNullException(nameof(key), SR.ArgumentNull_Key);
+//     IL code:
+//          IL_0003:  ldstr      "key"
+//          IL_0008:  ldstr      "ArgumentNull_Key"
+//          IL_000d:  call       string System.Environment::GetResourceString(string)
+//          IL_0012:  newobj     instance void System.ArgumentNullException::.ctor(string,string)
+//          IL_0017:  throw
+//    which is 21bytes in IL.
+//
+// So we want to get rid of the ldstr and call to Environment.GetResource in IL.
+// In order to do that, I created two enums: ExceptionResource, ExceptionArgument to represent the
+// argument name and resource name in a small integer. The source code will be changed to
+//    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key, ExceptionResource.ArgumentNull_Key);
+//
+// The IL code will be 7 bytes.
+//    IL_0008:  ldc.i4.4
+//    IL_0009:  ldc.i4.4
+//    IL_000a:  call       void System.ThrowHelper::ThrowArgumentNullException(valuetype System.ExceptionArgument)
+//    IL_000f:  ldarg.0
+//
+// This will also reduce the Jitted code size a lot.
+//
+// It is very important we do this for generic classes because we can easily generate the same code
+// multiple times for different instantiation.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.Serialization;
+
+namespace Avalonia.Collections.Pooled
+{
+    internal static class ThrowHelper
+    {
+        internal static void ThrowArrayTypeMismatchException()
+        {
+            throw new ArrayTypeMismatchException();
+        }
+
+        internal static void ThrowIndexOutOfRangeException()
+        {
+            throw new IndexOutOfRangeException();
+        }
+
+        internal static void ThrowArgumentOutOfRangeException()
+        {
+            throw new ArgumentOutOfRangeException();
+        }
+
+        internal static void ThrowArgumentException_DestinationTooShort()
+        {
+            throw new ArgumentException("Destination too short.");
+        }
+
+        internal static void ThrowArgumentException_OverlapAlignmentMismatch()
+        {
+            throw new ArgumentException("Overlap alignment mismatch.");
+        }
+
+        internal static void ThrowArgumentOutOfRange_IndexException()
+        {
+            throw GetArgumentOutOfRangeException(ExceptionArgument.index,
+                                                    ExceptionResource.ArgumentOutOfRange_Index);
+        }
+
+        internal static void ThrowIndexArgumentOutOfRange_NeedNonNegNumException()
+        {
+            throw GetArgumentOutOfRangeException(ExceptionArgument.index,
+                                                    ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
+        }
+
+        internal static void ThrowValueArgumentOutOfRange_NeedNonNegNumException()
+        {
+            throw GetArgumentOutOfRangeException(ExceptionArgument.value,
+                                                    ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
+        }
+
+        internal static void ThrowLengthArgumentOutOfRange_ArgumentOutOfRange_NeedNonNegNum()
+        {
+            throw GetArgumentOutOfRangeException(ExceptionArgument.length,
+                                                    ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
+        }
+
+        internal static void ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index()
+        {
+            throw GetArgumentOutOfRangeException(ExceptionArgument.startIndex,
+                                                    ExceptionResource.ArgumentOutOfRange_Index);
+        }
+
+        internal static void ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count()
+        {
+            throw GetArgumentOutOfRangeException(ExceptionArgument.count,
+                                                    ExceptionResource.ArgumentOutOfRange_Count);
+        }
+
+        internal static void ThrowWrongKeyTypeArgumentException<T>(T key, Type targetType)
+        {
+            // Generic key to move the boxing to the right hand side of throw
+            throw GetWrongKeyTypeArgumentException((object)key, targetType);
+        }
+
+        internal static void ThrowWrongValueTypeArgumentException<T>(T value, Type targetType)
+        {
+            // Generic key to move the boxing to the right hand side of throw
+            throw GetWrongValueTypeArgumentException((object)value, targetType);
+        }
+
+        private static ArgumentException GetAddingDuplicateWithKeyArgumentException(object key)
+        {
+            return new ArgumentException($"Error adding duplicate with key: {key}.");
+        }
+
+        internal static void ThrowAddingDuplicateWithKeyArgumentException<T>(T key)
+        {
+            // Generic key to move the boxing to the right hand side of throw
+            throw GetAddingDuplicateWithKeyArgumentException((object)key);
+        }
+
+        internal static void ThrowKeyNotFoundException<T>(T key)
+        {
+            // Generic key to move the boxing to the right hand side of throw
+            throw GetKeyNotFoundException((object)key);
+        }
+
+        internal static void ThrowArgumentException(ExceptionResource resource)
+        {
+            throw GetArgumentException(resource);
+        }
+
+        internal static void ThrowArgumentException(ExceptionResource resource, ExceptionArgument argument)
+        {
+            throw GetArgumentException(resource, argument);
+        }
+
+        private static ArgumentNullException GetArgumentNullException(ExceptionArgument argument)
+        {
+            return new ArgumentNullException(GetArgumentName(argument));
+        }
+
+        internal static void ThrowArgumentNullException(ExceptionArgument argument)
+        {
+            throw GetArgumentNullException(argument);
+        }
+
+        internal static void ThrowArgumentNullException(ExceptionResource resource)
+        {
+            throw new ArgumentNullException(GetResourceString(resource));
+        }
+
+        internal static void ThrowArgumentNullException(ExceptionArgument argument, ExceptionResource resource)
+        {
+            throw new ArgumentNullException(GetArgumentName(argument), GetResourceString(resource));
+        }
+
+        internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument)
+        {
+            throw new ArgumentOutOfRangeException(GetArgumentName(argument));
+        }
+
+        internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource)
+        {
+            throw GetArgumentOutOfRangeException(argument, resource);
+        }
+
+        internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument, int paramNumber, ExceptionResource resource)
+        {
+            throw GetArgumentOutOfRangeException(argument, paramNumber, resource);
+        }
+
+        internal static void ThrowInvalidOperationException(ExceptionResource resource)
+        {
+            throw GetInvalidOperationException(resource);
+        }
+
+        internal static void ThrowInvalidOperationException(ExceptionResource resource, Exception e)
+        {
+            throw new InvalidOperationException(GetResourceString(resource), e);
+        }
+
+        internal static void ThrowSerializationException(ExceptionResource resource)
+        {
+            throw new SerializationException(GetResourceString(resource));
+        }
+
+        internal static void ThrowSecurityException(ExceptionResource resource)
+        {
+            throw new System.Security.SecurityException(GetResourceString(resource));
+        }
+
+        internal static void ThrowRankException(ExceptionResource resource)
+        {
+            throw new RankException(GetResourceString(resource));
+        }
+
+        internal static void ThrowNotSupportedException(ExceptionResource resource)
+        {
+            throw new NotSupportedException(GetResourceString(resource));
+        }
+
+        internal static void ThrowUnauthorizedAccessException(ExceptionResource resource)
+        {
+            throw new UnauthorizedAccessException(GetResourceString(resource));
+        }
+
+        internal static void ThrowObjectDisposedException(string objectName, ExceptionResource resource)
+        {
+            throw new ObjectDisposedException(objectName, GetResourceString(resource));
+        }
+
+        internal static void ThrowObjectDisposedException(ExceptionResource resource)
+        {
+            throw new ObjectDisposedException(null, GetResourceString(resource));
+        }
+
+        internal static void ThrowNotSupportedException()
+        {
+            throw new NotSupportedException();
+        }
+
+        internal static void ThrowAggregateException(List<Exception> exceptions)
+        {
+            throw new AggregateException(exceptions);
+        }
+
+        internal static void ThrowOutOfMemoryException()
+        {
+            throw new OutOfMemoryException();
+        }
+
+        internal static void ThrowArgumentException_Argument_InvalidArrayType()
+        {
+            throw new ArgumentException("Invalid array type.");
+        }
+
+        internal static void ThrowInvalidOperationException_InvalidOperation_EnumNotStarted()
+        {
+            throw new InvalidOperationException("Enumeration has not started.");
+        }
+
+        internal static void ThrowInvalidOperationException_InvalidOperation_EnumEnded()
+        {
+            throw new InvalidOperationException("Enumeration has ended.");
+        }
+
+        internal static void ThrowInvalidOperationException_EnumCurrent(int index)
+        {
+            throw GetInvalidOperationException_EnumCurrent(index);
+        }
+
+        internal static void ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion()
+        {
+            throw new InvalidOperationException("Collection was modified during enumeration.");
+        }
+
+        internal static void ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen()
+        {
+            throw new InvalidOperationException("Invalid enumerator state: enumeration cannot proceed.");
+        }
+
+        internal static void ThrowInvalidOperationException_InvalidOperation_NoValue()
+        {
+            throw new InvalidOperationException("No value provided.");
+        }
+
+        internal static void ThrowInvalidOperationException_ConcurrentOperationsNotSupported()
+        {
+            throw new InvalidOperationException("Concurrent operations are not supported.");
+        }
+
+        internal static void ThrowInvalidOperationException_HandleIsNotInitialized()
+        {
+            throw new InvalidOperationException("Handle is not initialized.");
+        }
+
+        internal static void ThrowFormatException_BadFormatSpecifier()
+        {
+            throw new FormatException("Bad format specifier.");
+        }
+
+        private static ArgumentException GetArgumentException(ExceptionResource resource)
+        {
+            return new ArgumentException(GetResourceString(resource));
+        }
+
+        private static InvalidOperationException GetInvalidOperationException(ExceptionResource resource)
+        {
+            return new InvalidOperationException(GetResourceString(resource));
+        }
+
+        private static ArgumentException GetWrongKeyTypeArgumentException(object key, Type targetType)
+        {
+            return new ArgumentException($"Wrong key type. Expected {targetType}, got: '{key}'.", nameof(key));
+        }
+
+        private static ArgumentException GetWrongValueTypeArgumentException(object value, Type targetType)
+        {
+            return new ArgumentException($"Wrong value type. Expected {targetType}, got: '{value}'.", nameof(value));
+        }
+
+        private static KeyNotFoundException GetKeyNotFoundException(object key)
+        {
+            return new KeyNotFoundException($"Key not found: {key}");
+        }
+
+        private static ArgumentOutOfRangeException GetArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource)
+        {
+            return new ArgumentOutOfRangeException(GetArgumentName(argument), GetResourceString(resource));
+        }
+
+        private static ArgumentException GetArgumentException(ExceptionResource resource, ExceptionArgument argument)
+        {
+            return new ArgumentException(GetResourceString(resource), GetArgumentName(argument));
+        }
+
+        private static ArgumentOutOfRangeException GetArgumentOutOfRangeException(ExceptionArgument argument, int paramNumber, ExceptionResource resource)
+        {
+            return new ArgumentOutOfRangeException(GetArgumentName(argument) + "[" + paramNumber.ToString() + "]", GetResourceString(resource));
+        }
+
+        private static InvalidOperationException GetInvalidOperationException_EnumCurrent(int index)
+        {
+            return new InvalidOperationException(
+                index < 0 ?
+                "Enumeration has not started" :
+                "Enumeration has ended");
+        }
+
+        // Allow nulls for reference types and Nullable<U>, but not for value types.
+        // Aggressively inline so the jit evaluates the if in place and either drops the call altogether
+        // Or just leaves null test and call to the Non-returning ThrowHelper.ThrowArgumentNullException
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal static void IfNullAndNullsAreIllegalThenThrow<T>(object value, ExceptionArgument argName)
+        {
+            // Note that default(T) is not equal to null for value types except when T is Nullable<U>.
+            if (!(default(T) == null) && value == null)
+                ThrowHelper.ThrowArgumentNullException(argName);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal static void ThrowForUnsupportedVectorBaseType<T>() where T : struct
+        {
+            if (typeof(T) != typeof(byte) && typeof(T) != typeof(sbyte) &&
+                typeof(T) != typeof(short) && typeof(T) != typeof(ushort) &&
+                typeof(T) != typeof(int) && typeof(T) != typeof(uint) &&
+                typeof(T) != typeof(long) && typeof(T) != typeof(ulong) &&
+                typeof(T) != typeof(float) && typeof(T) != typeof(double))
+            {
+                ThrowNotSupportedException(ExceptionResource.Arg_TypeNotSupported);
+            }
+        }
+
+#if false // Reflection-based implementation does not work for CoreRT/ProjectN
+        // This function will convert an ExceptionArgument enum value to the argument name string.
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        private static string GetArgumentName(ExceptionArgument argument)
+        {
+            Debug.Assert(Enum.IsDefined(typeof(ExceptionArgument), argument),
+                "The enum value is not defined, please check the ExceptionArgument Enum.");
+
+            return argument.ToString();
+        }
+#endif
+
+        private static string GetArgumentName(ExceptionArgument argument)
+        {
+            switch (argument)
+            {
+                case ExceptionArgument.obj:
+                    return "obj";
+                case ExceptionArgument.dictionary:
+                    return "dictionary";
+                case ExceptionArgument.array:
+                    return "array";
+                case ExceptionArgument.info:
+                    return "info";
+                case ExceptionArgument.key:
+                    return "key";
+                case ExceptionArgument.text:
+                    return "text";
+                case ExceptionArgument.values:
+                    return "values";
+                case ExceptionArgument.value:
+                    return "value";
+                case ExceptionArgument.startIndex:
+                    return "startIndex";
+                case ExceptionArgument.task:
+                    return "task";
+                case ExceptionArgument.ch:
+                    return "ch";
+                case ExceptionArgument.s:
+                    return "s";
+                case ExceptionArgument.input:
+                    return "input";
+                case ExceptionArgument.list:
+                    return "list";
+                case ExceptionArgument.index:
+                    return "index";
+                case ExceptionArgument.capacity:
+                    return "capacity";
+                case ExceptionArgument.collection:
+                    return "collection";
+                case ExceptionArgument.item:
+                    return "item";
+                case ExceptionArgument.converter:
+                    return "converter";
+                case ExceptionArgument.match:
+                    return "match";
+                case ExceptionArgument.count:
+                    return "count";
+                case ExceptionArgument.action:
+                    return "action";
+                case ExceptionArgument.comparison:
+                    return "comparison";
+                case ExceptionArgument.exceptions:
+                    return "exceptions";
+                case ExceptionArgument.exception:
+                    return "exception";
+                case ExceptionArgument.enumerable:
+                    return "enumerable";
+                case ExceptionArgument.start:
+                    return "start";
+                case ExceptionArgument.format:
+                    return "format";
+                case ExceptionArgument.culture:
+                    return "culture";
+                case ExceptionArgument.comparer:
+                    return "comparer";
+                case ExceptionArgument.comparable:
+                    return "comparable";
+                case ExceptionArgument.source:
+                    return "source";
+                case ExceptionArgument.state:
+                    return "state";
+                case ExceptionArgument.length:
+                    return "length";
+                case ExceptionArgument.comparisonType:
+                    return "comparisonType";
+                case ExceptionArgument.manager:
+                    return "manager";
+                case ExceptionArgument.sourceBytesToCopy:
+                    return "sourceBytesToCopy";
+                case ExceptionArgument.callBack:
+                    return "callBack";
+                case ExceptionArgument.creationOptions:
+                    return "creationOptions";
+                case ExceptionArgument.function:
+                    return "function";
+                case ExceptionArgument.delay:
+                    return "delay";
+                case ExceptionArgument.millisecondsDelay:
+                    return "millisecondsDelay";
+                case ExceptionArgument.millisecondsTimeout:
+                    return "millisecondsTimeout";
+                case ExceptionArgument.timeout:
+                    return "timeout";
+                case ExceptionArgument.type:
+                    return "type";
+                case ExceptionArgument.sourceIndex:
+                    return "sourceIndex";
+                case ExceptionArgument.sourceArray:
+                    return "sourceArray";
+                case ExceptionArgument.destinationIndex:
+                    return "destinationIndex";
+                case ExceptionArgument.destinationArray:
+                    return "destinationArray";
+                case ExceptionArgument.other:
+                    return "other";
+                case ExceptionArgument.newSize:
+                    return "newSize";
+                case ExceptionArgument.lowerBounds:
+                    return "lowerBounds";
+                case ExceptionArgument.lengths:
+                    return "lengths";
+                case ExceptionArgument.len:
+                    return "len";
+                case ExceptionArgument.keys:
+                    return "keys";
+                case ExceptionArgument.indices:
+                    return "indices";
+                case ExceptionArgument.endIndex:
+                    return "endIndex";
+                case ExceptionArgument.elementType:
+                    return "elementType";
+                case ExceptionArgument.arrayIndex:
+                    return "arrayIndex";
+                default:
+                    Debug.Fail("The enum value is not defined, please check the ExceptionArgument Enum.");
+                    return argument.ToString();
+            }
+        }
+
+#if false // Reflection-based implementation does not work for CoreRT/ProjectN
+        // This function will convert an ExceptionResource enum value to the resource string.
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        private static string GetResourceString(ExceptionResource resource)
+        {
+            Debug.Assert(Enum.IsDefined(typeof(ExceptionResource), resource),
+                "The enum value is not defined, please check the ExceptionResource Enum.");
+
+            return SR.GetResourceString(resource.ToString());
+        }
+#endif
+
+        private static string GetResourceString(ExceptionResource resource)
+        {
+            switch (resource)
+            {
+                case ExceptionResource.ArgumentOutOfRange_Index:
+                    return "Argument 'index' was out of the range of valid values.";
+                case ExceptionResource.ArgumentOutOfRange_Count:
+                    return "Argument 'count' was out of the range of valid values.";
+                case ExceptionResource.Arg_ArrayPlusOffTooSmall:
+                    return "Array plus offset too small.";
+                case ExceptionResource.NotSupported_ReadOnlyCollection:
+                    return "This operation is not supported on a read-only collection.";
+                case ExceptionResource.Arg_RankMultiDimNotSupported:
+                    return "Multi-dimensional arrays are not supported.";
+                case ExceptionResource.Arg_NonZeroLowerBound:
+                    return "Arrays with a non-zero lower bound are not supported.";
+                case ExceptionResource.ArgumentOutOfRange_ListInsert:
+                    return "Insertion index was out of the range of valid values.";
+                case ExceptionResource.ArgumentOutOfRange_NeedNonNegNum:
+                    return "The number must be non-negative.";
+                case ExceptionResource.ArgumentOutOfRange_SmallCapacity:
+                    return "The capacity cannot be set below the current Count.";
+                case ExceptionResource.Argument_InvalidOffLen:
+                    return "Invalid offset length.";
+                case ExceptionResource.ArgumentOutOfRange_BiggerThanCollection:
+                    return "The given value was larger than the size of the collection.";
+                case ExceptionResource.Serialization_MissingKeys:
+                    return "Serialization error: missing keys.";
+                case ExceptionResource.Serialization_NullKey:
+                    return "Serialization error: null key.";
+                case ExceptionResource.NotSupported_KeyCollectionSet:
+                    return "The KeyCollection does not support modification.";
+                case ExceptionResource.NotSupported_ValueCollectionSet:
+                    return "The ValueCollection does not support modification.";
+                case ExceptionResource.InvalidOperation_NullArray:
+                    return "Null arrays are not supported.";
+                case ExceptionResource.InvalidOperation_HSCapacityOverflow:
+                    return "Set hash capacity overflow. Cannot increase size.";
+                case ExceptionResource.NotSupported_StringComparison:
+                    return "String comparison not supported.";
+                case ExceptionResource.ConcurrentCollection_SyncRoot_NotSupported:
+                    return "SyncRoot not supported.";
+                case ExceptionResource.ArgumentException_OtherNotArrayOfCorrectLength:
+                    return "The other array is not of the correct length.";
+                case ExceptionResource.ArgumentOutOfRange_EndIndexStartIndex:
+                    return "The end index does not come after the start index.";
+                case ExceptionResource.ArgumentOutOfRange_HugeArrayNotSupported:
+                    return "Huge arrays are not supported.";
+                case ExceptionResource.Argument_AddingDuplicate:
+                    return "Duplicate item added.";
+                case ExceptionResource.Argument_InvalidArgumentForComparison:
+                    return "Invalid argument for comparison.";
+                case ExceptionResource.Arg_LowerBoundsMustMatch:
+                    return "Array lower bounds must match.";
+                case ExceptionResource.Arg_MustBeType:
+                    return "Argument must be of type: ";
+                case ExceptionResource.InvalidOperation_IComparerFailed:
+                    return "IComparer failed.";
+                case ExceptionResource.NotSupported_FixedSizeCollection:
+                    return "This operation is not suppored on a fixed-size collection.";
+                case ExceptionResource.Rank_MultiDimNotSupported:
+                    return "Multi-dimensional arrays are not supported.";
+                case ExceptionResource.Arg_TypeNotSupported:
+                    return "Type not supported.";
+                default:
+                    Debug.Assert(false,
+                        "The enum value is not defined, please check the ExceptionResource Enum.");
+                    return resource.ToString();
+            }
+        }
+    }
+
+    //
+    // The convention for this enum is using the argument name as the enum name
+    //
+    internal enum ExceptionArgument
+    {
+        obj,
+        dictionary,
+        array,
+        info,
+        key,
+        text,
+        values,
+        value,
+        startIndex,
+        task,
+        ch,
+        s,
+        input,
+        list,
+        index,
+        capacity,
+        collection,
+        item,
+        converter,
+        match,
+        count,
+        action,
+        comparison,
+        exceptions,
+        exception,
+        enumerable,
+        start,
+        format,
+        culture,
+        comparer,
+        comparable,
+        source,
+        state,
+        length,
+        comparisonType,
+        manager,
+        sourceBytesToCopy,
+        callBack,
+        creationOptions,
+        function,
+        delay,
+        millisecondsDelay,
+        millisecondsTimeout,
+        timeout,
+        type,
+        sourceIndex,
+        sourceArray,
+        destinationIndex,
+        destinationArray,
+        other,
+        newSize,
+        lowerBounds,
+        lengths,
+        len,
+        keys,
+        indices,
+        endIndex,
+        elementType,
+        arrayIndex
+    }
+
+    //
+    // The convention for this enum is using the resource name as the enum name
+    //
+    internal enum ExceptionResource
+    {
+        ArgumentOutOfRange_Index,
+        ArgumentOutOfRange_Count,
+        Arg_ArrayPlusOffTooSmall,
+        NotSupported_ReadOnlyCollection,
+        Arg_RankMultiDimNotSupported,
+        Arg_NonZeroLowerBound,
+        ArgumentOutOfRange_ListInsert,
+        ArgumentOutOfRange_NeedNonNegNum,
+        ArgumentOutOfRange_SmallCapacity,
+        Argument_InvalidOffLen,
+        ArgumentOutOfRange_BiggerThanCollection,
+        Serialization_MissingKeys,
+        Serialization_NullKey,
+        NotSupported_KeyCollectionSet,
+        NotSupported_ValueCollectionSet,
+        InvalidOperation_NullArray,
+        InvalidOperation_HSCapacityOverflow,
+        NotSupported_StringComparison,
+        ConcurrentCollection_SyncRoot_NotSupported,
+        ArgumentException_OtherNotArrayOfCorrectLength,
+        ArgumentOutOfRange_EndIndexStartIndex,
+        ArgumentOutOfRange_HugeArrayNotSupported,
+        Argument_AddingDuplicate,
+        Argument_InvalidArgumentForComparison,
+        Arg_LowerBoundsMustMatch,
+        Arg_MustBeType,
+        InvalidOperation_IComparerFailed,
+        NotSupported_FixedSizeCollection,
+        Rank_MultiDimNotSupported,
+        Arg_TypeNotSupported,
+    }
+}

+ 26 - 0
src/Avalonia.Base/Data/BindingNotification.cs

@@ -30,6 +30,12 @@ namespace Avalonia.Data
     /// Represents a binding notification that can be a valid binding value, or a binding or
     /// data validation error.
     /// </summary>
+    /// <remarks>
+    /// This class is very similar to <see cref="BindingValue{T}"/>, but where <see cref="BindingValue{T}"/>
+    /// is used by typed bindings, this class is used to hold binding and data validation errors in
+    /// untyped bindings. As Avalonia moves towards using typed bindings by default we may want to remove
+    /// this class.
+    /// </remarks>
     public class BindingNotification
     {
         /// <summary>
@@ -236,6 +242,26 @@ namespace Avalonia.Data
             _value = value;
         }
 
+        public BindingValue<object> ToBindingValue()
+        {
+            if (ErrorType == BindingErrorType.None)
+            {
+                return HasValue ? new BindingValue<object>(Value) : BindingValue<object>.Unset;
+            }
+            else if (ErrorType == BindingErrorType.Error)
+            {
+                return BindingValue<object>.BindingError(
+                    Error,
+                    HasValue ? new Optional<object>(Value) : Optional<object>.Empty);
+            }
+            else
+            {
+                return BindingValue<object>.DataValidationError(
+                    Error,
+                    HasValue ? new Optional<object>(Value) : Optional<object>.Empty);
+            }
+        }
+
         /// <inheritdoc/>
         public override string ToString()
         {

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

@@ -4,12 +4,13 @@
 using System;
 using System.Reactive.Disposables;
 using System.Reactive.Linq;
+using Avalonia.Reactive;
 
 namespace Avalonia.Data
 {
     public static class BindingOperations
     {
-        public static readonly object DoNothing = new object();
+        public static readonly object DoNothing = new DoNothingType();
 
         /// <summary>
         /// Applies an <see cref="InstancedBinding"/> a property on an <see cref="IAvaloniaObject"/>.
@@ -63,7 +64,10 @@ namespace Avalonia.Data
                         return source
                             .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue)
                             .Take(1)
-                            .Subscribe(x => targetCopy.SetValue(propertyCopy, x, bindingCopy.Priority));
+                            .Subscribe(x => targetCopy.SetValue(
+                                propertyCopy,
+                                BindingNotification.ExtractValue(x),
+                                bindingCopy.Priority));
                     }
                     else
                     {
@@ -88,4 +92,15 @@ namespace Avalonia.Data
             }
         }
     }
+
+    public sealed class DoNothingType
+    {
+        internal DoNothingType() { }
+
+        /// <summary>
+        /// Returns the string representation of <see cref="BindingOperations.DoNothing"/>.
+        /// </summary>
+        /// <returns>The string "(do nothing)".</returns>
+        public override string ToString() => "(do nothing)";
+    }
 }

+ 432 - 0
src/Avalonia.Base/Data/BindingValue.cs

@@ -0,0 +1,432 @@
+using System;
+using Avalonia.Utilities;
+
+#nullable enable
+
+namespace Avalonia.Data
+{
+    /// <summary>
+    /// Describes the type of a <see cref="BindingValue{T}"/>.
+    /// </summary>
+    [Flags]
+    public enum BindingValueType
+    {
+        /// <summary>
+        /// An unset value: the target property will revert to its unbound state until a new
+        /// binding value is produced.
+        /// </summary>
+        UnsetValue = 0,
+
+        /// <summary>
+        /// Do nothing: the binding value will be ignored.
+        /// </summary>
+        DoNothing = 1,
+
+        /// <summary>
+        /// A simple value.
+        /// </summary>
+        Value = 2 | HasValue,
+
+        /// <summary>
+        /// A binding error, such as a missing source property.
+        /// </summary>
+        BindingError = 3 | HasError,
+
+        /// <summary>
+        /// A data validation error.
+        /// </summary>
+        DataValidationError = 4 | HasError,
+
+        /// <summary>
+        /// A binding error with a fallback value.
+        /// </summary>
+        BindingErrorWithFallback = BindingError | HasValue,
+
+        /// <summary>
+        /// A data validation error with a fallback value.
+        /// </summary>
+        DataValidationErrorWithFallback = DataValidationError | HasValue,
+
+        TypeMask = 0x00ff,
+        HasValue = 0x0100,
+        HasError = 0x0200,
+    }
+
+    /// <summary>
+    /// A value passed into a binding.
+    /// </summary>
+    /// <typeparam name="T">The value type.</typeparam>
+    /// <remarks>
+    /// The avalonia binding system is typed, and as such additional state is stored in this
+    /// structure. A binding value can be in a number of states, described by the
+    /// <see cref="Type"/> property:
+    /// 
+    /// - <see cref="BindingValueType.Value"/>: a simple value
+    /// - <see cref="BindingValueType.UnsetValue"/>: the target property will revert to its unbound
+    ///   state until a new binding value is produced. Represented by
+    ///   <see cref="AvaloniaProperty.UnsetValue"/> in an untyped context
+    /// - <see cref="BindingValueType.DoNothing"/>: the binding value will be ignored. Represented
+    ///   by <see cref="BindingOperations.DoNothing"/> in an untyped context
+    /// - <see cref="BindingValueType.BindingError"/>: a binding error, such as a missing source
+    ///   property, with an optional fallback value
+    /// - <see cref="BindingValueType.DataValidationError"/>: a data validation error, with an
+    ///   optional fallback value
+    ///   
+    /// To create a new binding value you can:
+    /// 
+    /// - For a simple value, call the <see cref="BindingValue{T}"/> constructor or use an implicit
+    ///   conversion from <typeparamref name="T"/>
+    /// - For an unset value, use <see cref="Unset"/> or simply `default`
+    /// - For other types, call one of the static factory methods
+    /// </remarks>
+    public readonly struct BindingValue<T>
+    {
+        private readonly T _value;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="BindingValue{T}"/> struct with a type of
+        /// <see cref="BindingValueType.Value"/>
+        /// </summary>
+        /// <param name="value">The value.</param>
+        public BindingValue(T value)
+        {
+            ValidateValue(value);
+            _value = value;
+            Type = BindingValueType.Value;
+            Error = null;
+        }
+
+        private BindingValue(BindingValueType type, T value, Exception? error)
+        {
+            _value = value;
+            Type = type;
+            Error = error;
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether the binding value represents either a binding or data
+        /// validation error.
+        /// </summary>
+        public bool HasError => Type.HasFlagCustom(BindingValueType.HasError);
+
+        /// <summary>
+        /// Gets a value indicating whether the binding value has a value.
+        /// </summary>
+        public bool HasValue => Type.HasFlagCustom(BindingValueType.HasValue);
+
+        /// <summary>
+        /// Gets the type of the binding value.
+        /// </summary>
+        public BindingValueType Type { get; }
+
+        /// <summary>
+        /// Gets the binding value or fallback value.
+        /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// <see cref="HasValue"/> is false.
+        /// </exception>
+        public T Value => HasValue ? _value : throw new InvalidOperationException("BindingValue has no value.");
+
+        /// <summary>
+        /// Gets the binding or data validation error.
+        /// </summary>
+        public Exception? Error { get; }
+
+        /// <summary>
+        /// Converts the binding value to an <see cref="Optional{T}"/>.
+        /// </summary>
+        /// <returns></returns>
+        public Optional<T> ToOptional() => HasValue ? new Optional<T>(_value) : default;
+
+        /// <inheritdoc/>
+        public override string ToString() => HasError ? $"Error: {Error!.Message}" : _value?.ToString() ?? "(null)";
+
+        /// <summary>
+        /// Converts the value to untyped representation, using <see cref="AvaloniaProperty.UnsetValue"/>,
+        /// <see cref="BindingOperations.DoNothing"/> and <see cref="BindingNotification"/> where
+        /// appropriate.
+        /// </summary>
+        /// <returns>The untyped representation of the binding value.</returns>
+        public object? ToUntyped()
+        {
+            return Type switch
+            {
+                BindingValueType.UnsetValue => AvaloniaProperty.UnsetValue,
+                BindingValueType.DoNothing => BindingOperations.DoNothing,
+                BindingValueType.Value => _value,
+                BindingValueType.BindingError => 
+                    new BindingNotification(Error, BindingErrorType.Error),
+                BindingValueType.BindingErrorWithFallback =>
+                    new BindingNotification(Error, BindingErrorType.Error, Value),
+                BindingValueType.DataValidationError =>
+                    new BindingNotification(Error, BindingErrorType.DataValidationError),
+                BindingValueType.DataValidationErrorWithFallback =>
+                    new BindingNotification(Error, BindingErrorType.DataValidationError, Value),
+                _ => throw new NotSupportedException("Invalid BindingValueType."),
+            };
+        }
+
+        /// <summary>
+        /// Returns a new binding value with the specified value.
+        /// </summary>
+        /// <param name="value">The new value.</param>
+        /// <returns>The new binding value.</returns>
+        /// <exception cref="InvalidOperationException">
+        /// The binding type is <see cref="BindingValueType.UnsetValue"/> or
+        /// <see cref="BindingValueType.DoNothing"/>.
+        /// </exception>
+        public BindingValue<T> WithValue(T value)
+        {
+            if (Type == BindingValueType.DoNothing)
+            {
+                throw new InvalidOperationException("Cannot add value to DoNothing binding value.");
+            }
+
+            var type = Type == BindingValueType.UnsetValue ? BindingValueType.Value : Type;
+            return new BindingValue<T>(type | BindingValueType.HasValue, value, Error);
+        }
+
+        /// <summary>
+        /// Gets the value of the binding value if present, otherwise the default value.
+        /// </summary>
+        /// <returns>The value.</returns>
+        public T GetValueOrDefault() => HasValue ? _value : default;
+
+        /// <summary>
+        /// Gets the value of the binding value if present, otherwise a default value.
+        /// </summary>
+        /// <param name="defaultValue">The default value.</param>
+        /// <returns>The value.</returns>
+        public T GetValueOrDefault(T defaultValue) => HasValue ? _value : defaultValue;
+
+        /// <summary>
+        /// Gets the value if present, otherwise the default value.
+        /// </summary>
+        /// <returns>
+        /// The value if present and of the correct type, `default(TResult)` if the value is
+        /// not present or of an incorrect type.
+        /// </returns>
+        public TResult GetValueOrDefault<TResult>()
+        {
+            return HasValue ?
+                _value is TResult result ? result : default
+                : default;
+        }
+
+        /// <summary>
+        /// Gets the value of the binding value if present, otherwise a default value.
+        /// </summary>
+        /// <param name="defaultValue">The default value.</param>
+        /// <returns>
+        /// The value if present and of the correct type, `default(TResult)` if the value is
+        /// present but not of the correct type or null, or <paramref name="defaultValue"/> if the
+        /// value is not present.
+        /// </returns>
+        public TResult GetValueOrDefault<TResult>(TResult defaultValue)
+        {
+            return HasValue ?
+                _value is TResult result ? result : default
+                : defaultValue;
+        }
+
+        /// <summary>
+        /// Creates a <see cref="BindingValue{T}"/> from an object, handling the special values
+        /// <see cref="AvaloniaProperty.UnsetValue"/> and <see cref="BindingOperations.DoNothing"/>.
+        /// </summary>
+        /// <param name="value">The untyped value.</param>
+        /// <returns>The typed binding value.</returns>
+        public static BindingValue<T> FromUntyped(object? value)
+        {
+            return value switch
+            {
+                UnsetValueType _ => Unset,
+                DoNothingType _ => DoNothing,
+                BindingNotification n => n.ToBindingValue().Cast<T>(),
+                _ => (T)value
+            };
+        }
+
+        /// <summary>
+        /// Creates a binding value from an instance of the underlying value type.
+        /// </summary>
+        /// <param name="value">The value.</param>
+        public static implicit operator BindingValue<T>(T value) => new BindingValue<T>(value);
+
+        /// <summary>
+        /// Creates a binding value from an <see cref="Optional{T}"/>.
+        /// </summary>
+        /// <param name="optional">The optional value.</param>
+
+        public static implicit operator BindingValue<T>(Optional<T> optional)
+        {
+            return optional.HasValue ? optional.Value : Unset;
+        }
+
+        /// <summary>
+        /// Returns a binding value with a type of <see cref="BindingValueType.UnsetValue"/>.
+        /// </summary>
+        public static BindingValue<T> Unset => new BindingValue<T>(BindingValueType.UnsetValue, default, null);
+
+        /// <summary>
+        /// Returns a binding value with a type of <see cref="BindingValueType.DoNothing"/>.
+        /// </summary>
+        public static BindingValue<T> DoNothing => new BindingValue<T>(BindingValueType.DoNothing, default, null);
+
+        /// <summary>
+        /// Returns a binding value with a type of <see cref="BindingValueType.BindingError"/>.
+        /// </summary>
+        /// <param name="e">The binding error.</param>
+        public static BindingValue<T> BindingError(Exception e)
+        {
+            e = e ?? throw new ArgumentNullException("e");
+
+            return new BindingValue<T>(BindingValueType.BindingError, default, e);
+        }
+
+        /// <summary>
+        /// Returns a binding value with a type of <see cref="BindingValueType.BindingErrorWithFallback"/>.
+        /// </summary>
+        /// <param name="e">The binding error.</param>
+        /// <param name="fallbackValue">The fallback value.</param>
+        public static BindingValue<T> BindingError(Exception e, T fallbackValue)
+        {
+            e = e ?? throw new ArgumentNullException("e");
+
+            return new BindingValue<T>(BindingValueType.BindingErrorWithFallback, fallbackValue, e);
+        }
+
+        /// <summary>
+        /// Returns a binding value with a type of <see cref="BindingValueType.BindingError"/> or
+        /// <see cref="BindingValueType.BindingErrorWithFallback"/>.
+        /// </summary>
+        /// <param name="e">The binding error.</param>
+        /// <param name="fallbackValue">The fallback value.</param>
+        public static BindingValue<T> BindingError(Exception e, Optional<T> fallbackValue)
+        {
+            e = e ?? throw new ArgumentNullException("e");
+
+            return new BindingValue<T>(
+                fallbackValue.HasValue ?
+                    BindingValueType.BindingErrorWithFallback :
+                    BindingValueType.BindingError,
+                fallbackValue.HasValue ? fallbackValue.Value : default,
+                e);
+        }
+
+        /// <summary>
+        /// Returns a binding value with a type of <see cref="BindingValueType.DataValidationError"/>.
+        /// </summary>
+        /// <param name="e">The data validation error.</param>
+        public static BindingValue<T> DataValidationError(Exception e)
+        {
+            e = e ?? throw new ArgumentNullException("e");
+
+            return new BindingValue<T>(BindingValueType.DataValidationError, default, e);
+        }
+
+        /// <summary>
+        /// Returns a binding value with a type of <see cref="BindingValueType.DataValidationErrorWithFallback"/>.
+        /// </summary>
+        /// <param name="e">The data validation error.</param>
+        /// <param name="fallbackValue">The fallback value.</param>
+        public static BindingValue<T> DataValidationError(Exception e, T fallbackValue)
+        {
+            e = e ?? throw new ArgumentNullException("e");
+
+            return new BindingValue<T>(BindingValueType.DataValidationErrorWithFallback, fallbackValue, e);
+        }
+
+        /// <summary>
+        /// Returns a binding value with a type of <see cref="BindingValueType.DataValidationError"/> or
+        /// <see cref="BindingValueType.DataValidationErrorWithFallback"/>.
+        /// </summary>
+        /// <param name="e">The binding error.</param>
+        /// <param name="fallbackValue">The fallback value.</param>
+        public static BindingValue<T> DataValidationError(Exception e, Optional<T> fallbackValue)
+        {
+            e = e ?? throw new ArgumentNullException("e");
+
+            return new BindingValue<T>(
+                fallbackValue.HasValue ?
+                    BindingValueType.DataValidationError :
+                    BindingValueType.DataValidationErrorWithFallback,
+                fallbackValue.HasValue ? fallbackValue.Value : default,
+                e);
+        }
+
+        private static void ValidateValue(T value)
+        {
+            if (value is UnsetValueType)
+            {
+                throw new InvalidOperationException("AvaloniaValue.UnsetValue is not a valid value for BindingValue<>.");
+            }
+
+            if (value is DoNothingType)
+            {
+                throw new InvalidOperationException("BindingOperations.DoNothing is not a valid value for BindingValue<>.");
+            }
+
+            if (value is BindingValue<object>)
+            {
+                throw new InvalidOperationException("BindingValue<object> cannot be wrapped in a BindingValue<>.");
+            }
+        }
+    }
+
+    public static class BindingValueExtensions
+    {
+        /// <summary>
+        /// Casts the type of a <see cref="BindingValue{T}"/> using only the C# cast operator.
+        /// </summary>
+        /// <typeparam name="T">The target type.</typeparam>
+        /// <param name="value">The binding value.</param>
+        /// <returns>The cast value.</returns>
+        public static BindingValue<T> Cast<T>(this BindingValue<object> value)
+        {
+            return value.Type switch
+            {
+                BindingValueType.DoNothing => BindingValue<T>.DoNothing,
+                BindingValueType.UnsetValue => BindingValue<T>.Unset,
+                BindingValueType.Value => new BindingValue<T>((T)value.Value),
+                BindingValueType.BindingError => BindingValue<T>.BindingError(value.Error!),
+                BindingValueType.BindingErrorWithFallback => BindingValue<T>.BindingError(
+                        value.Error!,
+                        (T)value.Value),
+                BindingValueType.DataValidationError => BindingValue<T>.DataValidationError(value.Error!),
+                BindingValueType.DataValidationErrorWithFallback => BindingValue<T>.DataValidationError(
+                        value.Error!,
+                        (T)value.Value),
+                _ => throw new NotSupportedException("Invalid BindingValue type."),
+            };
+        }
+
+        /// <summary>
+        /// Casts the type of a <see cref="BindingValue{T}"/> using the implicit conversions
+        /// allowed by the C# language.
+        /// </summary>
+        /// <typeparam name="T">The target type.</typeparam>
+        /// <param name="value">The binding value.</param>
+        /// <returns>The cast value.</returns>
+        /// <remarks>
+        /// Note that this method uses reflection and as such may be slow.
+        /// </remarks>
+        public static BindingValue<T> Convert<T>(this BindingValue<object> value)
+        {
+            return value.Type switch
+            {
+                BindingValueType.DoNothing => BindingValue<T>.DoNothing,
+                BindingValueType.UnsetValue => BindingValue<T>.Unset,
+                BindingValueType.Value => new BindingValue<T>(TypeUtilities.ConvertImplicit<T>(value.Value)),
+                BindingValueType.BindingError => BindingValue<T>.BindingError(value.Error!),
+                BindingValueType.BindingErrorWithFallback => BindingValue<T>.BindingError(
+                        value.Error!,
+                        TypeUtilities.ConvertImplicit<T>(value.Value)),
+                BindingValueType.DataValidationError => BindingValue<T>.DataValidationError(value.Error!),
+                BindingValueType.DataValidationErrorWithFallback => BindingValue<T>.DataValidationError(
+                        value.Error!,
+                        TypeUtilities.ConvertImplicit<T>(value.Value)),
+                _ => throw new NotSupportedException("Invalid BindingValue type."),
+            };
+        }
+    }
+}

+ 152 - 0
src/Avalonia.Base/Data/Optional.cs

@@ -0,0 +1,152 @@
+using System;
+using System.Collections.Generic;
+
+#nullable enable
+
+namespace Avalonia.Data
+{
+    /// <summary>
+    /// An optional typed value.
+    /// </summary>
+    /// <typeparam name="T">The value type.</typeparam>
+    /// <remarks>
+    /// This struct is similar to <see cref="Nullable{T}"/> except it also accepts reference types:
+    /// note that null is a valid value for reference types. It is also similar to
+    /// <see cref="BindingValue{T}"/> but has only two states: "value present" and "value missing".
+    /// 
+    /// To create a new optional value you can:
+    /// 
+    /// - For a simple value, call the <see cref="Optional{T}"/> constructor or use an implicit
+    ///   conversion from <typeparamref name="T"/>
+    /// - For an missing value, use <see cref="Empty"/> or simply `default`
+    /// </remarks>
+    public readonly struct Optional<T> : IEquatable<Optional<T>>
+    {
+        private readonly T _value;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Optional{T}"/> struct with value.
+        /// </summary>
+        /// <param name="value">The value.</param>
+        public Optional(T value)
+        {
+            _value = value;
+            HasValue = true;
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether a value is present.
+        /// </summary>
+        public bool HasValue { get; }
+
+        /// <summary>
+        /// Gets the value.
+        /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// <see cref="HasValue"/> is false.
+        /// </exception>
+        public T Value => HasValue ? _value : throw new InvalidOperationException("Optional has no value.");
+
+        /// <inheritdoc/>
+        public override bool Equals(object obj) => obj is Optional<T> o && this == o;
+
+        /// <inheritdoc/>
+        public bool Equals(Optional<T> other) => this == other;
+
+        /// <inheritdoc/>
+        public override int GetHashCode() => HasValue ? _value?.GetHashCode() ?? 0 : 0;
+
+        /// <summary>
+        /// Casts the value (if any) to an <see cref="object"/>.
+        /// </summary>
+        /// <returns>The cast optional value.</returns>
+        public Optional<object> ToObject() => HasValue ? new Optional<object>(_value) : default;
+
+        /// <inheritdoc/>
+        public override string ToString() => HasValue ? _value?.ToString() ?? "(null)" : "(empty)";
+
+        /// <summary>
+        /// Gets the value if present, otherwise the default value.
+        /// </summary>
+        /// <returns>The value.</returns>
+        public T GetValueOrDefault() => HasValue ? _value : default;
+
+        /// <summary>
+        /// Gets the value if present, otherwise a default value.
+        /// </summary>
+        /// <param name="defaultValue">The default value.</param>
+        /// <returns>The value.</returns>
+        public T GetValueOrDefault(T defaultValue) => HasValue ? _value : defaultValue;
+
+        /// <summary>
+        /// Gets the value if present, otherwise the default value.
+        /// </summary>
+        /// <returns>
+        /// The value if present and of the correct type, `default(TResult)` if the value is
+        /// not present or of an incorrect type.
+        /// </returns>
+        public TResult GetValueOrDefault<TResult>()
+        {
+            return HasValue ?
+                _value is TResult result ? result : default
+                : default;
+        }
+
+        /// <summary>
+        /// Gets the value if present, otherwise a default value.
+        /// </summary>
+        /// <param name="defaultValue">The default value.</param>
+        /// <returns>
+        /// The value if present and of the correct type, `default(TResult)` if the value is
+        /// present but not of the correct type or null, or <paramref name="defaultValue"/> if the
+        /// value is not present.
+        /// </returns>
+        public TResult GetValueOrDefault<TResult>(TResult defaultValue)
+        {
+            return HasValue ?
+                _value is TResult result ? result : default
+                : defaultValue;
+        }
+
+        /// <summary>
+        /// Creates an <see cref="Optional{T}"/> from an instance of the underlying value type.
+        /// </summary>
+        /// <param name="value">The value.</param>
+        public static implicit operator Optional<T>(T value) => new Optional<T>(value);
+
+        /// <summary>
+        /// Compares two <see cref="Optional{T}"/>s for inequality.
+        /// </summary>
+        /// <param name="x">The first value.</param>
+        /// <param name="y">The second value.</param>
+        /// <returns>True if the values are unequal; otherwise false.</returns>
+        public static bool operator !=(Optional<T> x, Optional<T> y) => !(x == y);
+
+        /// <summary>
+        /// Compares two <see cref="Optional{T}"/>s for equality.
+        /// </summary>
+        /// <param name="x">The first value.</param>
+        /// <param name="y">The second value.</param>
+        /// <returns>True if the values are equal; otherwise false.</returns>
+        public static bool operator==(Optional<T> x, Optional<T> y)
+        {
+            if (!x.HasValue && !y.HasValue)
+            {
+                return true;
+            }
+            else if (x.HasValue && y.HasValue)
+            {
+                return EqualityComparer<T>.Default.Equals(x.Value, y.Value);
+            }
+            else
+            {
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// Returns an <see cref="Optional{T}"/> without a value.
+        /// </summary>
+        public static Optional<T> Empty => default;
+    }
+}

+ 2 - 29
src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs

@@ -1,6 +1,7 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using System;
 using Avalonia.Data;
 
 namespace Avalonia.Diagnostics
@@ -21,35 +22,7 @@ namespace Avalonia.Diagnostics
         /// </returns>
         public static AvaloniaPropertyValue GetDiagnostic(this AvaloniaObject o, AvaloniaProperty property)
         {
-            var set = o.GetSetValues();
-
-            if (set.TryGetValue(property, out var obj))
-            {
-                if (obj is PriorityValue value)
-                {
-                    return new AvaloniaPropertyValue(
-                        property,
-                        o.GetValue(property),
-                        (BindingPriority)value.ValuePriority,
-                        value.GetDiagnostic());
-                }
-                else
-                {
-                    return new AvaloniaPropertyValue(
-                        property,
-                        obj,
-                        BindingPriority.LocalValue,
-                        "Local value");
-                }
-            }
-            else
-            {
-                return new AvaloniaPropertyValue(
-                    property,
-                    o.GetValue(property),
-                    BindingPriority.Unset,
-                    "Unset");
-            }
+            return o.GetDiagnosticInternal(property);
         }
     }
 }

+ 78 - 10
src/Avalonia.Base/DirectProperty.cs

@@ -16,7 +16,7 @@ namespace Avalonia
     /// <see cref="AvaloniaProperty"/> system. They hold a getter and an optional setter which
     /// allows the avalonia property system to read and write the current value.
     /// </remarks>
-    public class DirectProperty<TOwner, TValue> : AvaloniaProperty<TValue>, IDirectPropertyAccessor
+    public class DirectProperty<TOwner, TValue> : DirectPropertyBase<TValue>, IDirectPropertyAccessor
         where TOwner : IAvaloniaObject
     {
         /// <summary>
@@ -26,12 +26,16 @@ namespace Avalonia
         /// <param name="getter">Gets the current value of the property.</param>
         /// <param name="setter">Sets the value of the property. May be null.</param>
         /// <param name="metadata">The property metadata.</param>
+        /// <param name="enableDataValidation">
+        /// Whether the property is interested in data validation.
+        /// </param>
         public DirectProperty(
             string name,
             Func<TOwner, TValue> getter,
             Action<TOwner, TValue> setter,
-            DirectPropertyMetadata<TValue> metadata)
-            : base(name, typeof(TOwner), metadata)
+            DirectPropertyMetadata<TValue> metadata,
+            bool enableDataValidation)
+            : base(name, typeof(TOwner), metadata, enableDataValidation)
         {
             Contract.Requires<ArgumentNullException>(getter != null);
 
@@ -46,12 +50,16 @@ namespace Avalonia
         /// <param name="getter">Gets the current value of the property.</param>
         /// <param name="setter">Sets the value of the property. May be null.</param>
         /// <param name="metadata">Optional overridden metadata.</param>
+        /// <param name="enableDataValidation">
+        /// Whether the property is interested in data validation.
+        /// </param>
         private DirectProperty(
-            AvaloniaProperty<TValue> source,
+            DirectPropertyBase<TValue> source,
             Func<TOwner, TValue> getter,
             Action<TOwner, TValue> setter,
-            DirectPropertyMetadata<TValue> metadata)
-            : base(source, typeof(TOwner), metadata)
+            DirectPropertyMetadata<TValue> metadata,
+            bool enableDataValidation)
+            : base(source, typeof(TOwner), metadata, enableDataValidation)
         {
             Contract.Requires<ArgumentNullException>(getter != null);
 
@@ -65,6 +73,9 @@ namespace Avalonia
         /// <inheritdoc/>
         public override bool IsReadOnly => Setter == null;
 
+        /// <inheritdoc/>
+        public override Type Owner => typeof(TOwner);
+
         /// <summary>
         /// Gets the getter function.
         /// </summary>
@@ -75,9 +86,6 @@ namespace Avalonia
         /// </summary>
         public Action<TOwner, TValue> Setter { get; }
 
-        /// <inheritdoc/>
-        Type IDirectPropertyAccessor.Owner => typeof(TOwner);
-
         /// <summary>
         /// Registers the direct property on another type.
         /// </summary>
@@ -99,6 +107,45 @@ namespace Avalonia
             BindingMode defaultBindingMode = BindingMode.Default,
             bool enableDataValidation = false)
                 where TNewOwner : AvaloniaObject
+        {
+            var metadata = new DirectPropertyMetadata<TValue>(
+                unsetValue: unsetValue,
+                defaultBindingMode: defaultBindingMode);
+
+            metadata.Merge(GetMetadata<TOwner>(), this);
+
+            var result = new DirectProperty<TNewOwner, TValue>(
+                (DirectPropertyBase<TValue>)this,
+                getter,
+                setter,
+                metadata,
+                enableDataValidation);
+
+            AvaloniaPropertyRegistry.Instance.Register(typeof(TNewOwner), result);
+            return result;
+        }
+
+        /// <summary>
+        /// Registers the direct property on another type.
+        /// </summary>
+        /// <typeparam name="TNewOwner">The type of the additional owner.</typeparam>
+        /// <param name="getter">Gets the current value of the property.</param>
+        /// <param name="setter">Sets the value of the property.</param>
+        /// <param name="unsetValue">
+        /// The value to use when the property is set to <see cref="AvaloniaProperty.UnsetValue"/>
+        /// </param>
+        /// <param name="defaultBindingMode">The default binding mode for the property.</param>
+        /// <param name="enableDataValidation">
+        /// Whether the property is interested in data validation.
+        /// </param>
+        /// <returns>The property.</returns>
+        public DirectProperty<TNewOwner, TValue> AddOwnerWithDataValidation<TNewOwner>(
+            Func<TNewOwner, TValue> getter,
+            Action<TNewOwner,TValue> setter,
+            TValue unsetValue = default(TValue),
+            BindingMode defaultBindingMode = BindingMode.Default,
+            bool enableDataValidation = false)
+                where TNewOwner : AvaloniaObject
         {
             var metadata = new DirectPropertyMetadata<TValue>(
                 unsetValue: unsetValue,
@@ -111,12 +158,33 @@ namespace Avalonia
                 this,
                 getter,
                 setter,
-                metadata);
+                metadata,
+                enableDataValidation);
 
             AvaloniaPropertyRegistry.Instance.Register(typeof(TNewOwner), result);
             return result;
         }
 
+        /// <inheritdoc/>
+        internal override TValue InvokeGetter(IAvaloniaObject instance)
+        {
+            return Getter((TOwner)instance);
+        }
+
+        /// <inheritdoc/>
+        internal override void InvokeSetter(IAvaloniaObject instance, BindingValue<TValue> value)
+        {
+            if (Setter == null)
+            {
+                throw new ArgumentException($"The property {Name} is readonly.");
+            }
+
+            if (value.HasValue)
+            {
+                Setter((TOwner)instance, value.Value);
+            }
+        }
+
         /// <inheritdoc/>
         object IDirectPropertyAccessor.GetValue(IAvaloniaObject instance)
         {

+ 153 - 0
src/Avalonia.Base/DirectPropertyBase.cs

@@ -0,0 +1,153 @@
+using System;
+using Avalonia.Data;
+using Avalonia.Reactive;
+
+#nullable enable
+
+namespace Avalonia
+{
+    /// <summary>
+    /// Base class for direct properties.
+    /// </summary>
+    /// <typeparam name="TValue">The type of the property's value.</typeparam>
+    /// <remarks>
+    /// Whereas <see cref="DirectProperty{TOwner, TValue}"/> is typed on the owner type, this base
+    /// class provides a non-owner-typed interface to a direct poperty.
+    /// </remarks>
+    public abstract class DirectPropertyBase<TValue> : AvaloniaProperty<TValue>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DirectPropertyBase{TValue}"/> class.
+        /// </summary>
+        /// <param name="name">The name of the property.</param>
+        /// <param name="ownerType">The type of the class that registers the property.</param>
+        /// <param name="metadata">The property metadata.</param>
+        /// <param name="enableDataValidation">
+        /// Whether the property is interested in data validation.
+        /// </param>
+        protected DirectPropertyBase(
+            string name,
+            Type ownerType,
+            PropertyMetadata metadata,
+            bool enableDataValidation)
+            : base(name, ownerType, metadata)
+        {
+            IsDataValidationEnabled = enableDataValidation;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AvaloniaProperty"/> class.
+        /// </summary>
+        /// <param name="source">The property to copy.</param>
+        /// <param name="ownerType">The new owner type.</param>
+        /// <param name="metadata">Optional overridden metadata.</param>
+        /// <param name="enableDataValidation">
+        /// Whether the property is interested in data validation.
+        /// </param>
+        protected DirectPropertyBase(
+            AvaloniaProperty source,
+            Type ownerType,
+            PropertyMetadata metadata,
+            bool enableDataValidation)
+            : base(source, ownerType, metadata)
+        {
+            IsDataValidationEnabled = enableDataValidation;
+        }
+
+        /// <summary>
+        /// Gets the type that registered the property.
+        /// </summary>
+        public abstract Type Owner { get; }
+
+        /// <summary>
+        /// Gets a value that indicates whether data validation is enabled for the property.
+        /// </summary>
+        public bool IsDataValidationEnabled { get; }
+
+        /// <summary>
+        /// Gets the value of the property on the instance.
+        /// </summary>
+        /// <param name="instance">The instance.</param>
+        /// <returns>The property value.</returns>
+        internal abstract TValue InvokeGetter(IAvaloniaObject instance);
+
+        /// <summary>
+        /// Sets the value of the property on the instance.
+        /// </summary>
+        /// <param name="instance">The instance.</param>
+        /// <param name="value">The value.</param>
+        internal abstract void InvokeSetter(IAvaloniaObject instance, BindingValue<TValue> value);
+
+        /// <summary>
+        /// Gets the unset value for the property on the specified type.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <returns>The unset value.</returns>
+        public TValue GetUnsetValue(Type type)
+        {
+            type = type ?? throw new ArgumentNullException(nameof(type));
+            return GetMetadata(type).UnsetValue;
+        }
+
+        /// <summary>
+        /// Gets the property metadata for the specified type.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <returns>
+        /// The property metadata.
+        /// </returns>
+        public new DirectPropertyMetadata<TValue> GetMetadata(Type type)
+        {
+            return (DirectPropertyMetadata<TValue>)base.GetMetadata(type);
+        }
+
+        /// <inheritdoc/>
+        internal override void RouteClearValue(IAvaloniaObject o)
+        {
+            o.ClearValue<TValue>(this);
+        }
+
+        /// <inheritdoc/>
+        internal override object? RouteGetValue(IAvaloniaObject o)
+        {
+            return o.GetValue<TValue>(this);
+        }
+
+        /// <inheritdoc/>
+        internal override void RouteSetValue(
+            IAvaloniaObject o,
+            object value,
+            BindingPriority priority)
+        {
+            var v = TryConvert(value);
+
+            if (v.HasValue)
+            {
+                o.SetValue<TValue>(this, (TValue)v.Value);
+            }
+            else if (v.Type == BindingValueType.UnsetValue)
+            {
+                o.ClearValue(this);
+            }
+            else if (v.HasError)
+            {
+                throw v.Error!;
+            }
+        }
+
+        /// <inheritdoc/>
+        internal override IDisposable RouteBind(
+            IAvaloniaObject o,
+            IObservable<BindingValue<object>> source,
+            BindingPriority priority)
+        {
+            var adapter = TypedBindingAdapter<TValue>.Create(o, this, source);
+            return o.Bind<TValue>(this, adapter);
+        }
+
+        internal override void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent)
+        {
+            throw new NotSupportedException("Direct properties do not support inheritance.");
+        }
+    }
+}

+ 63 - 18
src/Avalonia.Base/IAvaloniaObject.cs

@@ -17,16 +17,24 @@ namespace Avalonia
         event EventHandler<AvaloniaPropertyChangedEventArgs> PropertyChanged;
 
         /// <summary>
-        /// Raised when an inheritable <see cref="AvaloniaProperty"/> value changes on this object.
+        /// Clears an <see cref="AvaloniaProperty"/>'s local value.
         /// </summary>
-        event EventHandler<AvaloniaPropertyChangedEventArgs> InheritablePropertyChanged;
+        /// <param name="property">The property.</param>
+        void ClearValue<T>(StyledPropertyBase<T> property);
+
+        /// <summary>
+        /// Clears an <see cref="AvaloniaProperty"/>'s local value.
+        /// </summary>
+        /// <param name="property">The property.</param>
+        void ClearValue<T>(DirectPropertyBase<T> property);
 
         /// <summary>
         /// Gets a <see cref="AvaloniaProperty"/> value.
         /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
         /// <param name="property">The property.</param>
         /// <returns>The value.</returns>
-        object GetValue(AvaloniaProperty property);
+        T GetValue<T>(StyledPropertyBase<T> property);
 
         /// <summary>
         /// Gets a <see cref="AvaloniaProperty"/> value.
@@ -34,7 +42,7 @@ namespace Avalonia
         /// <typeparam name="T">The type of the property.</typeparam>
         /// <param name="property">The property.</param>
         /// <returns>The value.</returns>
-        T GetValue<T>(AvaloniaProperty<T> property);
+        T GetValue<T>(DirectPropertyBase<T> property);
 
         /// <summary>
         /// Checks whether a <see cref="AvaloniaProperty"/> is animating.
@@ -53,12 +61,13 @@ namespace Avalonia
         /// <summary>
         /// Sets a <see cref="AvaloniaProperty"/> value.
         /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
         /// <param name="property">The property.</param>
         /// <param name="value">The value.</param>
         /// <param name="priority">The priority of the value.</param>
-        void SetValue(
-            AvaloniaProperty property, 
-            object value, 
+        void SetValue<T>(
+            StyledPropertyBase<T> property,
+            T value,
             BindingPriority priority = BindingPriority.LocalValue);
 
         /// <summary>
@@ -67,24 +76,21 @@ namespace Avalonia
         /// <typeparam name="T">The type of the property.</typeparam>
         /// <param name="property">The property.</param>
         /// <param name="value">The value.</param>
-        /// <param name="priority">The priority of the value.</param>
-        void SetValue<T>(
-            AvaloniaProperty<T> property,
-            T value,
-            BindingPriority priority = BindingPriority.LocalValue);
+        void SetValue<T>(DirectPropertyBase<T> property, T value);
 
         /// <summary>
         /// Binds a <see cref="AvaloniaProperty"/> to an observable.
         /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
         /// <param name="property">The property.</param>
         /// <param name="source">The observable.</param>
         /// <param name="priority">The priority of the binding.</param>
         /// <returns>
         /// A disposable which can be used to terminate the binding.
         /// </returns>
-        IDisposable Bind(
-            AvaloniaProperty property,
-            IObservable<object> source,
+        IDisposable Bind<T>(
+            StyledPropertyBase<T> property,
+            IObservable<BindingValue<T>> source,
             BindingPriority priority = BindingPriority.LocalValue);
 
         /// <summary>
@@ -93,13 +99,52 @@ namespace Avalonia
         /// <typeparam name="T">The type of the property.</typeparam>
         /// <param name="property">The property.</param>
         /// <param name="source">The observable.</param>
-        /// <param name="priority">The priority of the binding.</param>
         /// <returns>
         /// A disposable which can be used to terminate the binding.
         /// </returns>
         IDisposable Bind<T>(
+            DirectPropertyBase<T> property,
+            IObservable<BindingValue<T>> source);
+
+        /// <summary>
+        /// Coerces the specified <see cref="AvaloniaProperty"/>.
+        /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
+        /// <param name="property">The property.</param>
+        void CoerceValue<T>(StyledPropertyBase<T> property);
+
+        /// <summary>
+        /// Registers an object as an inheritance child.
+        /// </summary>
+        /// <param name="child">The inheritance child.</param>
+        /// <remarks>
+        /// Inheritance children will receive a call to
+        /// <see cref="InheritedPropertyChanged{T}(AvaloniaProperty{T}, Optional{T}, Optional{T})"/>
+        /// when an inheritable property value changes on the parent.
+        /// </remarks>
+        void AddInheritanceChild(IAvaloniaObject child);
+
+        /// <summary>
+        /// Unregisters an object as an inheritance child.
+        /// </summary>
+        /// <param name="child">The inheritance child.</param>
+        /// <remarks>
+        /// Removes an inheritance child that was added by a call to
+        /// <see cref="AddInheritanceChild(IAvaloniaObject)"/>.
+        /// </remarks>
+        void RemoveInheritanceChild(IAvaloniaObject child);
+
+        /// <summary>
+        /// Called when an inheritable property changes on an object registered as an inheritance
+        /// parent.
+        /// </summary>
+        /// <typeparam name="T">The type of the value.</typeparam>
+        /// <param name="property">The property that has changed.</param>
+        /// <param name="oldValue"></param>
+        /// <param name="newValue"></param>
+        void InheritedPropertyChanged<T>(
             AvaloniaProperty<T> property,
-            IObservable<T> source,
-            BindingPriority priority = BindingPriority.LocalValue);
+            Optional<T> oldValue,
+            Optional<T> newValue);
     }
 }

+ 0 - 51
src/Avalonia.Base/IPriorityValueOwner.cs

@@ -1,51 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using Avalonia.Data;
-using Avalonia.Utilities;
-
-namespace Avalonia
-{
-    /// <summary>
-    /// An owner of a <see cref="PriorityValue"/>.
-    /// </summary>
-    internal interface IPriorityValueOwner
-    {
-        /// <summary>
-        /// Called when a <see cref="PriorityValue"/>'s value changes.
-        /// </summary>
-        /// <param name="property">The the property that has changed.</param>
-        /// <param name="priority">The priority of the value.</param>
-        /// <param name="oldValue">The old value.</param>
-        /// <param name="newValue">The new value.</param>
-        void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue);
-
-        /// <summary>
-        /// Called when a <see cref="BindingNotification"/> is received by a 
-        /// <see cref="PriorityValue"/>.
-        /// </summary>
-        /// <param name="property">The the property that has changed.</param>
-        /// <param name="notification">The notification.</param>
-        void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification);
-
-        /// <summary>
-        /// Returns deferred setter for given non-direct property.
-        /// </summary>
-        /// <param name="property">Property.</param>
-        /// <returns>Deferred setter for given property.</returns>
-        DeferredSetter<object> GetNonDirectDeferredSetter(AvaloniaProperty property);
-
-        /// <summary>
-        /// Logs a binding error.
-        /// </summary>
-        /// <param name="property">The property the error occurred on.</param>
-        /// <param name="e">The binding error.</param>
-        void LogError(AvaloniaProperty property, Exception e);
-
-        /// <summary>
-        /// Ensures that the current thread is the UI thread.
-        /// </summary>
-        void VerifyAccess();
-    }
-}

+ 0 - 9
src/Avalonia.Base/IStyledPropertyAccessor.cs

@@ -18,14 +18,5 @@ namespace Avalonia
         /// The default value.
         /// </returns>
         object GetDefaultValue(Type type);
-
-        /// <summary>
-        /// Gets a validation function for the property on the specified type.
-        /// </summary>
-        /// <param name="type">The type.</param>
-        /// <returns>
-        /// The validation function, or null if no validation function exists.
-        /// </returns>
-        Func<IAvaloniaObject, object, object> GetValidationFunc(Type type);
     }
 }

+ 1 - 6
src/Avalonia.Base/IStyledPropertyMetadata.cs

@@ -14,10 +14,5 @@ namespace Avalonia
         /// Gets the default value for the property.
         /// </summary>
         object DefaultValue { get; }
-
-        /// <summary>
-        /// Gets the property's validation function.
-        /// </summary>
-        Func<IAvaloniaObject, object, object> Validate { get; }
     }
-}
+}

+ 0 - 160
src/Avalonia.Base/PriorityBindingEntry.cs

@@ -1,160 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Runtime.ExceptionServices;
-using Avalonia.Data;
-using Avalonia.Threading;
-
-namespace Avalonia
-{
-    /// <summary>
-    /// A registered binding in a <see cref="PriorityValue"/>.
-    /// </summary>
-    internal class PriorityBindingEntry : IDisposable, IObserver<object>
-    {
-        private readonly PriorityLevel _owner;
-        private IDisposable _subscription;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="PriorityBindingEntry"/> class.
-        /// </summary>
-        /// <param name="owner">The owner.</param>
-        /// <param name="index">
-        /// The binding index. Later bindings should have higher indexes.
-        /// </param>
-        public PriorityBindingEntry(PriorityLevel owner, int index)
-        {
-            _owner = owner;
-            Index = index;
-        }
-
-        /// <summary>
-        /// Gets the observable associated with the entry.
-        /// </summary>
-        public IObservable<object> Observable { get; private set; }
-
-        /// <summary>
-        /// Gets a description of the binding.
-        /// </summary>
-        public string Description
-        {
-            get;
-            private set;
-        }
-
-        /// <summary>
-        /// Gets the binding entry index. Later bindings will have higher indexes.
-        /// </summary>
-        public int Index
-        {
-            get;
-        }
-
-        /// <summary>
-        /// Gets a value indicating whether the binding has completed.
-        /// </summary>
-        public bool HasCompleted { get; private set; }
-
-        /// <summary>
-        /// The current value of the binding.
-        /// </summary>
-        public object Value
-        {
-            get;
-            private set;
-        }
-
-        /// <summary>
-        /// Starts listening to the binding.
-        /// </summary>
-        /// <param name="binding">The binding.</param>
-        public void Start(IObservable<object> binding)
-        {
-            Contract.Requires<ArgumentNullException>(binding != null);
-
-            if (_subscription != null)
-            {
-                throw new Exception("PriorityValue.Entry.Start() called more than once.");
-            }
-
-            Observable = binding;
-            Value = AvaloniaProperty.UnsetValue;
-
-            if (binding is IDescription)
-            {
-                Description = ((IDescription)binding).Description;
-            }
-
-            _subscription = binding.Subscribe(this);
-        }
-
-        /// <summary>
-        /// Ends the binding subscription.
-        /// </summary>
-        public void Dispose()
-        {
-            _subscription?.Dispose();
-        }
-
-        void IObserver<object>.OnNext(object value)
-        {
-            void Signal(PriorityBindingEntry instance, object newValue)
-            {
-                var notification = newValue as BindingNotification;
-
-                if (notification != null)
-                {
-                    if (notification.HasValue || notification.ErrorType == BindingErrorType.Error)
-                    {
-                        instance.Value = notification.Value;
-                        instance._owner.Changed(instance);
-                    }
-
-                    if (notification.ErrorType != BindingErrorType.None)
-                    {
-                        instance._owner.Error(instance, notification);
-                    }
-                }
-                else
-                {
-                    instance.Value = newValue;
-                    instance._owner.Changed(instance);
-                }
-            }
-
-            if (Dispatcher.UIThread.CheckAccess())
-            {
-                Signal(this, value);
-            }
-            else
-            {
-                // To avoid allocating closure in the outer scope we need to capture variables
-                // locally. This allows us to skip most of the allocations when on UI thread.
-                var instance = this;
-                var newValue = value;
-
-                Dispatcher.UIThread.Post(() => Signal(instance, newValue));
-            }
-        }
-
-        void IObserver<object>.OnCompleted()
-        {
-            HasCompleted = true;
-
-            if (Dispatcher.UIThread.CheckAccess())
-            {
-                _owner.Completed(this);
-            }
-            else
-            {
-                Dispatcher.UIThread.Post(() => _owner.Completed(this));
-            }
-        }
-
-        void IObserver<object>.OnError(Exception error)
-        {
-            ExceptionDispatchInfo.Capture(error).Throw();
-        }
-    }
-}

+ 0 - 227
src/Avalonia.Base/PriorityLevel.cs

@@ -1,227 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Threading;
-using Avalonia.Data;
-
-namespace Avalonia
-{
-    /// <summary>
-    /// Stores bindings for a priority level in a <see cref="PriorityValue"/>.
-    /// </summary>
-    /// <remarks>
-    /// <para>
-    /// Each priority level in a <see cref="PriorityValue"/> has a current <see cref="Value"/>,
-    /// a list of <see cref="Bindings"/> and a <see cref="DirectValue"/>. When there are no
-    /// bindings present, or all bindings return <see cref="AvaloniaProperty.UnsetValue"/> then
-    /// <code>Value</code> will equal <code>DirectValue</code>.
-    /// </para>
-    /// <para>
-    /// When there are bindings present, then the latest added binding that doesn't return
-    /// <code>UnsetValue</code> will take precedence. The active binding is returned by the
-    /// <see cref="ActiveBindingIndex"/> property (which refers to the active binding's
-    /// <see cref="PriorityBindingEntry.Index"/> property rather than the index in
-    /// <code>Bindings</code>).
-    /// </para>
-    /// <para>
-    /// If <code>DirectValue</code> is set while a binding is active, then it will replace the
-    /// current value until the active binding fires again.
-    /// </para>
-    /// </remarks>
-    internal class PriorityLevel
-    {
-        private object _directValue;
-        private int _nextIndex;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="PriorityLevel"/> class.
-        /// </summary>
-        /// <param name="owner">The owner.</param>
-        /// <param name="priority">The priority.</param>
-        public PriorityLevel(
-            PriorityValue owner,
-            int priority)
-        {
-            Contract.Requires<ArgumentNullException>(owner != null);
-
-            Owner = owner;
-            Priority = priority;
-            Value = _directValue = AvaloniaProperty.UnsetValue;
-            ActiveBindingIndex = -1;
-            Bindings = new LinkedList<PriorityBindingEntry>();
-        }
-
-        /// <summary>
-        /// Gets the owner of the level.
-        /// </summary>
-        public PriorityValue Owner { get; }
-
-        /// <summary>
-        /// Gets the priority of this level.
-        /// </summary>
-        public int Priority { get; }
-
-        /// <summary>
-        /// Gets or sets the direct value for this priority level.
-        /// </summary>
-        public object DirectValue
-        {
-            get
-            {
-                return _directValue;
-            }
-
-            set
-            {
-                Value = _directValue = value;
-                Owner.LevelValueChanged(this);
-            }
-        }
-
-        /// <summary>
-        /// Gets the current binding for the priority level.
-        /// </summary>
-        public object Value { get; private set; }
-
-        /// <summary>
-        /// Gets the <see cref="PriorityBindingEntry.Index"/> value of the active binding, or -1
-        /// if no binding is active.
-        /// </summary>
-        public int ActiveBindingIndex { get; private set; }
-
-        /// <summary>
-        /// Gets the bindings for the priority level.
-        /// </summary>
-        public LinkedList<PriorityBindingEntry> Bindings { get; }
-
-        /// <summary>
-        /// Adds a binding.
-        /// </summary>
-        /// <param name="binding">The binding to add.</param>
-        /// <returns>A disposable used to remove the binding.</returns>
-        public IDisposable Add(IObservable<object> binding)
-        {
-            Contract.Requires<ArgumentNullException>(binding != null);
-
-            var entry = new PriorityBindingEntry(this, _nextIndex++);
-            var node = Bindings.AddFirst(entry);
-
-            entry.Start(binding);
-
-            return new RemoveBindingDisposable(node, Bindings, this);
-        }
-
-        /// <summary>
-        /// Invoked when an entry in <see cref="Bindings"/> changes value.
-        /// </summary>
-        /// <param name="entry">The entry that changed.</param>
-        public void Changed(PriorityBindingEntry entry)
-        {
-            if (entry.Index >= ActiveBindingIndex)
-            {
-                if (entry.Value != AvaloniaProperty.UnsetValue)
-                {
-                    Value = entry.Value;
-                    ActiveBindingIndex = entry.Index;
-                    Owner.LevelValueChanged(this);
-                }
-                else
-                {
-                    ActivateFirstBinding();
-                }
-            }
-        }
-
-        /// <summary>
-        /// Invoked when an entry in <see cref="Bindings"/> completes.
-        /// </summary>
-        /// <param name="entry">The entry that completed.</param>
-        public void Completed(PriorityBindingEntry entry)
-        {
-            Bindings.Remove(entry);
-
-            if (entry.Index >= ActiveBindingIndex)
-            {
-                ActivateFirstBinding();
-            }
-        }
-
-        /// <summary>
-        /// Invoked when an entry in <see cref="Bindings"/> encounters a recoverable error.
-        /// </summary>
-        /// <param name="entry">The entry that completed.</param>
-        /// <param name="error">The error.</param>
-        public void Error(PriorityBindingEntry entry, BindingNotification error)
-        {
-            Owner.LevelError(this, error);
-        }
-
-        /// <summary>
-        /// Activates the first binding that has a value.
-        /// </summary>
-        private void ActivateFirstBinding()
-        {
-            foreach (var binding in Bindings)
-            {
-                if (binding.Value != AvaloniaProperty.UnsetValue)
-                {
-                    Value = binding.Value;
-                    ActiveBindingIndex = binding.Index;
-                    Owner.LevelValueChanged(this);
-                    return;
-                }
-            }
-
-            Value = DirectValue;
-            ActiveBindingIndex = -1;
-            Owner.LevelValueChanged(this);
-        }
-
-        private sealed class RemoveBindingDisposable : IDisposable
-        {
-            private readonly LinkedList<PriorityBindingEntry> _bindings;
-            private readonly PriorityLevel _priorityLevel;
-            private LinkedListNode<PriorityBindingEntry> _binding;
-
-            public RemoveBindingDisposable(
-                LinkedListNode<PriorityBindingEntry> binding,
-                LinkedList<PriorityBindingEntry> bindings,
-                PriorityLevel priorityLevel)
-            {
-                _binding = binding;
-                _bindings = bindings;
-                _priorityLevel = priorityLevel;
-            }
-
-            public void Dispose()
-            {
-                LinkedListNode<PriorityBindingEntry> binding = Interlocked.Exchange(ref _binding, null);
-
-                if (binding == null)
-                {
-                    // Some system is trying to remove binding twice.
-                    Debug.Assert(false);
-
-                    return;
-                }
-
-                PriorityBindingEntry entry = binding.Value;
-
-                if (!entry.HasCompleted)
-                {
-                    _bindings.Remove(binding);
-
-                    entry.Dispose();
-
-                    if (entry.Index >= _priorityLevel.ActiveBindingIndex)
-                    {
-                        _priorityLevel.ActivateFirstBinding();
-                    }
-                }
-            }
-        }
-    }
-}

+ 0 - 315
src/Avalonia.Base/PriorityValue.cs

@@ -1,315 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using Avalonia.Data;
-using Avalonia.Logging;
-using Avalonia.Utilities;
-
-namespace Avalonia
-{
-    /// <summary>
-    /// Maintains a list of prioritized bindings together with a current value.
-    /// </summary>
-    /// <remarks>
-    /// Bindings, in the form of <see cref="IObservable{Object}"/>s are added to the object using
-    /// the <see cref="Add"/> method. With the observable is passed a priority, where lower values
-    /// represent higher priorities. The current <see cref="Value"/> is selected from the highest
-    /// priority binding that doesn't return <see cref="AvaloniaProperty.UnsetValue"/>. Where there
-    /// are multiple bindings registered with the same priority, the most recently added binding
-    /// has a higher priority. Each time the value changes, the 
-    /// <see cref="IPriorityValueOwner.Changed"/> method on the 
-    /// owner object is fired with the old and new values.
-    /// </remarks>
-    internal sealed class PriorityValue : ISetAndNotifyHandler<(object,int)>
-    {
-        private readonly Type _valueType;
-        private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
-        private readonly Func<object, object> _validate;
-        private (object value, int priority) _value;
-        private DeferredSetter<object> _setter;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="PriorityValue"/> class.
-        /// </summary>
-        /// <param name="owner">The owner of the object.</param>
-        /// <param name="property">The property that the value represents.</param>
-        /// <param name="valueType">The value type.</param>
-        /// <param name="validate">An optional validation function.</param>
-        public PriorityValue(
-            IPriorityValueOwner owner,
-            AvaloniaProperty property, 
-            Type valueType,
-            Func<object, object> validate = null)
-        {
-            Owner = owner;
-            Property = property;
-            _valueType = valueType;
-            _value = (AvaloniaProperty.UnsetValue, int.MaxValue);
-            _validate = validate;
-        }
-
-        /// <summary>
-        /// Gets a value indicating whether the property is animating.
-        /// </summary>
-        public bool IsAnimating
-        {
-            get
-            {
-                return ValuePriority <= (int)BindingPriority.Animation && 
-                    GetLevel(ValuePriority).ActiveBindingIndex != -1;
-            }
-        }
-
-        /// <summary>
-        /// Gets the owner of the value.
-        /// </summary>
-        public IPriorityValueOwner Owner { get; }
-
-        /// <summary>
-        /// Gets the property that the value represents.
-        /// </summary>
-        public AvaloniaProperty Property { get; }
-
-        /// <summary>
-        /// Gets the current value.
-        /// </summary>
-        public object Value => _value.value;
-
-        /// <summary>
-        /// Gets the priority of the binding that is currently active.
-        /// </summary>
-        public int ValuePriority => _value.priority;
-
-        /// <summary>
-        /// Adds a new binding.
-        /// </summary>
-        /// <param name="binding">The binding.</param>
-        /// <param name="priority">The binding priority.</param>
-        /// <returns>
-        /// A disposable that will remove the binding.
-        /// </returns>
-        public IDisposable Add(IObservable<object> binding, int priority)
-        {
-            return GetLevel(priority).Add(binding);
-        }
-
-        /// <summary>
-        /// Sets the value for a specified priority.
-        /// </summary>
-        /// <param name="value">The value.</param>
-        /// <param name="priority">The priority</param>
-        public void SetValue(object value, int priority)
-        {
-            GetLevel(priority).DirectValue = value;
-        }
-
-        /// <summary>
-        /// Gets the currently active bindings on this object.
-        /// </summary>
-        /// <returns>An enumerable collection of bindings.</returns>
-        public IEnumerable<PriorityBindingEntry> GetBindings()
-        {
-            foreach (var level in _levels)
-            {
-                foreach (var binding in level.Value.Bindings)
-                {
-                    yield return binding;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Returns diagnostic string that can help the user debug the bindings in effect on
-        /// this object.
-        /// </summary>
-        /// <returns>A diagnostic string.</returns>
-        public string GetDiagnostic()
-        {
-            var b = new StringBuilder();
-            var first = true;
-
-            foreach (var level in _levels)
-            {
-                if (!first)
-                {
-                    b.AppendLine();
-                }
-
-                b.Append(ValuePriority == level.Key ? "*" : string.Empty);
-                b.Append("Priority ");
-                b.Append(level.Key);
-                b.Append(": ");
-                b.AppendLine(level.Value.Value?.ToString() ?? "(null)");
-                b.AppendLine("--------");
-                b.Append("Direct: ");
-                b.AppendLine(level.Value.DirectValue?.ToString() ?? "(null)");
-
-                foreach (var binding in level.Value.Bindings)
-                {
-                    b.Append(level.Value.ActiveBindingIndex == binding.Index ? "*" : string.Empty);
-                    b.Append(binding.Description ?? binding.Observable.GetType().Name);
-                    b.Append(": ");
-                    b.AppendLine(binding.Value?.ToString() ?? "(null)");
-                }
-
-                first = false;
-            }
-
-            return b.ToString();
-        }
-
-        /// <summary>
-        /// Called when the value for a priority level changes.
-        /// </summary>
-        /// <param name="level">The priority level of the changed entry.</param>
-        public void LevelValueChanged(PriorityLevel level)
-        {
-            if (level.Priority <= ValuePriority)
-            {
-                if (level.Value != AvaloniaProperty.UnsetValue)
-                {
-                    UpdateValue(level.Value, level.Priority);
-                }
-                else
-                {
-                    foreach (var i in _levels.Values.OrderBy(x => x.Priority))
-                    {
-                        if (i.Value != AvaloniaProperty.UnsetValue)
-                        {
-                            UpdateValue(i.Value, i.Priority);
-                            return;
-                        }
-                    }
-
-                    UpdateValue(AvaloniaProperty.UnsetValue, int.MaxValue);
-                }
-            }
-        }
-
-        /// <summary>
-        /// Called when a priority level encounters an error.
-        /// </summary>
-        /// <param name="level">The priority level of the changed entry.</param>
-        /// <param name="error">The binding error.</param>
-        public void LevelError(PriorityLevel level, BindingNotification error)
-        {
-            Owner.LogError(Property, error.Error);
-        }
-
-        /// <summary>
-        /// Causes a revalidation of the value.
-        /// </summary>
-        public void Revalidate()
-        {
-            if (_validate != null)
-            {
-                PriorityLevel level;
-
-                if (_levels.TryGetValue(ValuePriority, out level))
-                {
-                    UpdateValue(level.Value, level.Priority);
-                }
-            }
-        }
-
-        /// <summary>
-        /// Gets the <see cref="PriorityLevel"/> with the specified priority, creating it if it
-        /// doesn't already exist.
-        /// </summary>
-        /// <param name="priority">The priority.</param>
-        /// <returns>The priority level.</returns>
-        private PriorityLevel GetLevel(int priority)
-        {
-            PriorityLevel result;
-
-            if (!_levels.TryGetValue(priority, out result))
-            {
-                result = new PriorityLevel(this, priority);
-                _levels.Add(priority, result);
-            }
-
-            return result;
-        }
-
-        /// <summary>
-        /// Updates the current <see cref="Value"/> and notifies all subscribers.
-        /// </summary>
-        /// <param name="value">The value to set.</param>
-        /// <param name="priority">The priority level that the value came from.</param>
-        private void UpdateValue(object value, int priority)
-        {
-            var newValue = (value, priority);
-
-            if (newValue == _value)
-            {
-                return;
-            }
-
-            if (_setter == null)
-            {
-                _setter = Owner.GetNonDirectDeferredSetter(Property);
-            }
-
-            _setter.SetAndNotifyCallback(Property, this, ref _value, newValue);
-        }
-
-        void ISetAndNotifyHandler<(object, int)>.HandleSetAndNotify(AvaloniaProperty property, ref (object, int) backing, (object, int) value)
-        {
-            SetAndNotify(ref backing, value);
-        }
-
-        private void SetAndNotify(ref (object value, int priority) backing, (object value, int priority) update)
-        {
-            var val = update.value;
-            var notification = val as BindingNotification;
-            object castValue;
-
-            if (notification != null)
-            {
-                val = (notification.HasValue) ? notification.Value : null;
-            }
-
-            if (TypeUtilities.TryConvertImplicit(_valueType, val, out castValue))
-            {
-                var old = backing.value;
-
-                if (_validate != null && castValue != AvaloniaProperty.UnsetValue)
-                {
-                    castValue = _validate(castValue);
-                }
-
-                backing = (castValue, update.priority);
-
-                if (notification?.HasValue == true)
-                {
-                    notification.SetValue(castValue);
-                }
-
-                if (notification == null || notification.HasValue)
-                {
-                    Owner?.Changed(Property, ValuePriority, old, Value);
-                }
-
-                if (notification != null)
-                {
-                    Owner?.BindingNotificationReceived(Property, notification);
-                }
-            }
-            else
-            {
-                Logger.TryGet(LogEventLevel.Error)?.Log(
-                    LogArea.Binding,
-                    Owner,
-                    "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
-                    Property.Name,
-                    _valueType,
-                    val,
-                    val?.GetType());
-            }
-        }
-    }
-}

+ 107 - 0
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@@ -0,0 +1,107 @@
+using System;
+using Avalonia.Data;
+using Avalonia.Threading;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+    /// <summary>
+    /// Represents an untyped interface to <see cref="BindingEntry{T}"/>.
+    /// </summary>
+    internal interface IBindingEntry : IPriorityValueEntry, IDisposable
+    {
+    }
+
+    /// <summary>
+    /// Stores a binding in a <see cref="ValueStore"/> or <see cref="PriorityValue{T}"/>.
+    /// </summary>
+    /// <typeparam name="T">The property type.</typeparam>
+    internal class BindingEntry<T> : IBindingEntry, IPriorityValueEntry<T>, IObserver<BindingValue<T>>
+    {
+        private readonly IAvaloniaObject _owner;
+        private IValueSink _sink;
+        private IDisposable? _subscription;
+
+        public BindingEntry(
+            IAvaloniaObject owner,
+            StyledPropertyBase<T> property,
+            IObservable<BindingValue<T>> source,
+            BindingPriority priority,
+            IValueSink sink)
+        {
+            _owner = owner;
+            Property = property;
+            Source = source;
+            Priority = priority;
+            _sink = sink;
+        }
+
+        public StyledPropertyBase<T> Property { get; }
+        public BindingPriority Priority { get; }
+        public IObservable<BindingValue<T>> Source { get; }
+        public Optional<T> Value { get; private set; }
+        Optional<object> IValue.Value => Value.ToObject();
+        BindingPriority IValue.ValuePriority => Priority;
+
+        public void Dispose()
+        {
+            _subscription?.Dispose();
+            _subscription = null;
+            _sink.Completed(Property, this, Value);
+        }
+
+        public void OnCompleted() => _sink.Completed(Property, this, Value);
+
+        public void OnError(Exception error)
+        {
+            throw new NotImplementedException();
+        }
+
+        public void OnNext(BindingValue<T> value)
+        {
+            if (Dispatcher.UIThread.CheckAccess())
+            {
+                UpdateValue(value); 
+            }
+            else
+            {
+                // To avoid allocating closure in the outer scope we need to capture variables
+                // locally. This allows us to skip most of the allocations when on UI thread.
+                var instance = this;
+                var newValue = value;
+
+                Dispatcher.UIThread.Post(() => instance.UpdateValue(newValue));
+            }
+        }
+
+        public void Start()
+        {
+            _subscription = Source.Subscribe(this);
+        }
+
+        public void Reparent(IValueSink sink) => _sink = sink;
+        
+        private void UpdateValue(BindingValue<T> value)
+        {
+            if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false)
+            {
+                value = Property.GetDefaultValue(_owner.GetType());
+            }
+
+            if (value.Type == BindingValueType.DoNothing)
+            {
+                return;
+            }
+
+            var old = Value;
+
+            if (value.Type != BindingValueType.DataValidationError)
+            {
+                Value = value.ToOptional();
+            }
+
+            _sink.ValueChanged(Property, Priority, old, value);
+        }
+    }
+}

+ 33 - 0
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@@ -0,0 +1,33 @@
+using System;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+    /// <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>
+    {
+        public ConstantValueEntry(
+            StyledPropertyBase<T> property,
+            T value,
+            BindingPriority priority)
+        {
+            Property = property;
+            Value = value;
+            Priority = priority;
+        }
+
+        public StyledPropertyBase<T> Property { get; }
+        public BindingPriority Priority { get; }
+        public Optional<T> Value { get; }
+        Optional<object> IValue.Value => Value.ToObject();
+        BindingPriority IValue.ValuePriority => Priority;
+
+        public void Reparent(IValueSink sink) { }
+    }
+}

+ 25 - 0
src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs

@@ -0,0 +1,25 @@
+using System;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+    /// <summary>
+    /// Represents an untyped interface to <see cref="IPriorityValueEntry{T}"/>.
+    /// </summary>
+    internal interface IPriorityValueEntry : IValue
+    {
+        BindingPriority Priority { get; }
+
+        void Reparent(IValueSink sink);
+    }
+
+    /// <summary>
+    /// Represents an object that can act as an entry in a <see cref="PriorityValue{T}"/>.
+    /// </summary>
+    /// <typeparam name="T">The property type.</typeparam>
+    internal interface IPriorityValueEntry<T> : IPriorityValueEntry, IValue<T>
+    {
+    }
+}

+ 24 - 0
src/Avalonia.Base/PropertyStore/IValue.cs

@@ -0,0 +1,24 @@
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+    /// <summary>
+    /// Represents an untyped interface to <see cref="IValue{T}"/>.
+    /// </summary>
+    internal interface IValue
+    {
+        Optional<object> Value { get; }
+        BindingPriority ValuePriority { get; }
+    }
+
+    /// <summary>
+    /// Represents an object that can act as an entry in a <see cref="ValueStore"/>.
+    /// </summary>
+    /// <typeparam name="T">The property type.</typeparam>
+    internal interface IValue<T> : IValue
+    {
+        new Optional<T> Value { get; }
+    }
+}

+ 23 - 0
src/Avalonia.Base/PropertyStore/IValueSink.cs

@@ -0,0 +1,23 @@
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+    /// <summary>
+    /// Represents an entity that can receive change notifications in a <see cref="ValueStore"/>.
+    /// </summary>
+    internal interface IValueSink
+    {
+        void ValueChanged<T>(
+            StyledPropertyBase<T> property,
+            BindingPriority priority,
+            Optional<T> oldValue,
+            BindingValue<T> newValue);
+
+        void Completed<T>(
+            StyledPropertyBase<T> property,
+            IPriorityValueEntry entry,
+            Optional<T> oldValue);
+    }
+}

+ 22 - 0
src/Avalonia.Base/PropertyStore/LocalValueEntry.cs

@@ -0,0 +1,22 @@
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+    /// <summary>
+    /// Stores a value with local value priority in a <see cref="ValueStore"/> or
+    /// <see cref="PriorityValue{T}"/>.
+    /// </summary>
+    /// <typeparam name="T">The property type.</typeparam>
+    internal class LocalValueEntry<T> : IValue<T>
+    {
+        private T _value;
+
+        public LocalValueEntry(T value) => _value = value;
+        public Optional<T> Value => _value;
+        public BindingPriority ValuePriority => BindingPriority.LocalValue;
+        Optional<object> IValue.Value => Value.ToObject();
+        public void SetValue(T value) => _value = value;
+    }
+}

+ 195 - 0
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@@ -0,0 +1,195 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.PropertyStore
+{
+    /// <summary>
+    /// Stores a set of prioritized values and bindings in a <see cref="ValueStore"/>.
+    /// </summary>
+    /// <typeparam name="T">The property type.</typeparam>
+    /// <remarks>
+    /// When more than a single value or binding is applied to a property in an
+    /// <see cref="AvaloniaObject"/>, the entry in the <see cref="ValueStore"/> is converted into
+    /// a <see cref="PriorityValue{T}"/>. This class holds any number of
+    /// <see cref="IPriorityValueEntry{T}"/> entries (sorted first by priority and then in the order
+    /// they were added) plus a local value.
+    /// </remarks>
+    internal class PriorityValue<T> : IValue<T>, IValueSink
+    {
+        private readonly IAvaloniaObject _owner;
+        private readonly IValueSink _sink;
+        private readonly List<IPriorityValueEntry<T>> _entries = new List<IPriorityValueEntry<T>>();
+        private readonly Func<IAvaloniaObject, T, T>? _coerceValue;
+        private Optional<T> _localValue;
+
+        public PriorityValue(
+            IAvaloniaObject owner,
+            StyledPropertyBase<T> property,
+            IValueSink sink)
+        {
+            _owner = owner;
+            Property = property;
+            _sink = sink;
+
+            if (property.HasCoercion)
+            {
+                var metadata = property.GetMetadata(owner.GetType());
+                _coerceValue = metadata.CoerceValue;
+            }
+        }
+
+        public PriorityValue(
+            IAvaloniaObject owner,
+            StyledPropertyBase<T> property,
+            IValueSink sink,
+            IPriorityValueEntry<T> existing)
+            : this(owner, property, sink)
+        {
+            existing.Reparent(this);
+            _entries.Add(existing);
+            
+            if (existing.Value.HasValue)
+            {
+                Value = existing.Value;
+                ValuePriority = existing.Priority;
+            }
+        }
+
+        public PriorityValue(
+            IAvaloniaObject owner,
+            StyledPropertyBase<T> property,
+            IValueSink sink,
+            LocalValueEntry<T> existing)
+            : this(owner, property, sink)
+        {
+            _localValue = existing.Value;
+            Value = _localValue;
+            ValuePriority = BindingPriority.LocalValue;
+        }
+
+        public StyledPropertyBase<T> Property { get; }
+        public Optional<T> Value { get; private set; }
+        public BindingPriority ValuePriority { get; private set; }
+        public IReadOnlyList<IPriorityValueEntry<T>> Entries => _entries;
+        Optional<object> IValue.Value => Value.ToObject();
+
+        public void ClearLocalValue() => UpdateEffectiveValue();
+
+        public void SetValue(T value, BindingPriority priority)
+        {
+            if (priority == BindingPriority.LocalValue)
+            {
+                _localValue = value;
+            }
+            else
+            {
+                var insert = FindInsertPoint(priority);
+                _entries.Insert(insert, new ConstantValueEntry<T>(Property, value, priority));
+            }
+
+            UpdateEffectiveValue();
+        }
+
+        public BindingEntry<T> AddBinding(IObservable<BindingValue<T>> source, BindingPriority priority)
+        {
+            var binding = new BindingEntry<T>(_owner, Property, source, priority, this);
+            var insert = FindInsertPoint(binding.Priority);
+            _entries.Insert(insert, binding);
+            return binding;
+        }
+
+        public void CoerceValue() => UpdateEffectiveValue();
+
+        void IValueSink.ValueChanged<TValue>(
+            StyledPropertyBase<TValue> property,
+            BindingPriority priority,
+            Optional<TValue> oldValue,
+            BindingValue<TValue> newValue)
+        {
+            if (priority == BindingPriority.LocalValue)
+            {
+                _localValue = default;
+            }
+
+            UpdateEffectiveValue();
+        }
+
+        void IValueSink.Completed<TValue>(
+            StyledPropertyBase<TValue> property,
+            IPriorityValueEntry entry,
+            Optional<TValue> oldValue)
+        {
+            _entries.Remove((IPriorityValueEntry<T>)entry);
+            UpdateEffectiveValue();
+        }
+
+        private int FindInsertPoint(BindingPriority priority)
+        {
+            var result = _entries.Count;
+
+            for (var i = 0; i < _entries.Count; ++i)
+            {
+                if (_entries[i].Priority < priority)
+                {
+                    result = i;
+                    break;
+                }
+            }
+
+            return result;
+        }
+
+        private void UpdateEffectiveValue()
+        {
+            var reachedLocalValues = false;
+            var value = default(Optional<T>);
+
+            if (_entries.Count > 0)
+            {
+                for (var i = _entries.Count - 1; i >= 0; --i)
+                {
+                    var entry = _entries[i];
+
+                    if (!reachedLocalValues && entry.Priority >= BindingPriority.LocalValue)
+                    {
+                        reachedLocalValues = true;
+
+                        if (_localValue.HasValue)
+                        {
+                            value = _localValue;
+                            ValuePriority = BindingPriority.LocalValue;
+                            break;
+                        }
+                    }
+
+                    if (entry.Value.HasValue)
+                    {
+                        value = entry.Value;
+                        ValuePriority = entry.Priority;
+                        break;
+                    }
+                }
+            }
+            else if (_localValue.HasValue)
+            {
+                value = _localValue;
+                ValuePriority = BindingPriority.LocalValue;
+            }
+
+            if (value.HasValue && _coerceValue != null)
+            {
+                value = _coerceValue(_owner, value.Value);
+            }
+
+            if (value != Value)
+            {
+                var old = Value;
+                Value = value;
+                _sink.ValueChanged(Property, ValuePriority, old, value);
+            }
+        }
+    }
+}

+ 76 - 0
src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs

@@ -0,0 +1,76 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.Reactive
+{
+    internal class AvaloniaPropertyBindingObservable<T> : LightweightObservableBase<BindingValue<T>>, IDescription
+    {
+        private readonly WeakReference<IAvaloniaObject> _target;
+        private readonly AvaloniaProperty _property;
+        private T _value;
+
+#nullable disable
+        public AvaloniaPropertyBindingObservable(
+            IAvaloniaObject target,
+            AvaloniaProperty property)
+        {
+            _target = new WeakReference<IAvaloniaObject>(target);
+            _property = property;
+        }
+#nullable enable
+
+        public string Description => $"{_target.GetType().Name}.{_property.Name}";
+
+        protected override void Initialize()
+        {
+            if (_target.TryGetTarget(out var target))
+            {
+                _value = (T)target.GetValue(_property);
+                target.PropertyChanged += PropertyChanged;
+            }
+        }
+
+        protected override void Deinitialize()
+        {
+            if (_target.TryGetTarget(out var target))
+            {
+                target.PropertyChanged -= PropertyChanged;
+            }
+        }
+
+        protected override void Subscribed(IObserver<BindingValue<T>> observer, bool first)
+        {
+            observer.OnNext(new BindingValue<T>(_value));
+        }
+
+        private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Property == _property)
+            {
+                if (e is AvaloniaPropertyChangedEventArgs<T> typedArgs)
+                {
+                    var newValue = e.Sender.GetValue(typedArgs.Property);
+
+                    if (!typedArgs.OldValue.HasValue || !EqualityComparer<T>.Default.Equals(newValue, _value))
+                    {
+                        _value = newValue;
+                        PublishNext(_value);
+                    }
+                }
+                else
+                {
+                    var newValue = e.Sender.GetValue(e.Property);
+
+                    if (!Equals(newValue, _value))
+                    {
+                        _value = (T)newValue;
+                        PublishNext(_value);
+                    }
+                }
+            }
+        }
+    }
+}

+ 16 - 2
src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs

@@ -44,8 +44,22 @@ namespace Avalonia.Reactive
         {
             if (e.Property == _property)
             {
-                _value = (T)e.NewValue;
-                PublishNext(_value);
+                T newValue;
+
+                if (e is AvaloniaPropertyChangedEventArgs<T> typed)
+                {
+                    newValue = typed.Sender.GetValue(typed.Property);
+                }
+                else
+                {
+                    newValue = (T)e.Sender.GetValue(e.Property);
+                }
+
+                if (!Equals(newValue, _value))
+                {
+                    _value = (T)newValue;
+                    PublishNext(_value);
+                }
             }
         }
     }

+ 61 - 0
src/Avalonia.Base/Reactive/BindingValueAdapter.cs

@@ -0,0 +1,61 @@
+using System;
+using System.Reactive.Subjects;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.Reactive
+{
+    internal class BindingValueAdapter<T> : SingleSubscriberObservableBase<BindingValue<T>>,
+        IObserver<T>
+    {
+        private readonly IObservable<T> _source;
+        private IDisposable? _subscription;
+
+        public BindingValueAdapter(IObservable<T> source) => _source = source;
+        public void OnCompleted() => PublishCompleted();
+        public void OnError(Exception error) => PublishError(error);
+        public void OnNext(T value) => PublishNext(BindingValue<T>.FromUntyped(value));
+        protected override void Subscribed() => _subscription = _source.Subscribe(this);
+        protected override void Unsubscribed() => _subscription?.Dispose();
+    }
+
+    internal class BindingValueSubjectAdapter<T> : SingleSubscriberObservableBase<BindingValue<T>>,
+        ISubject<BindingValue<T>>
+    {
+        private readonly ISubject<T> _source;
+        private readonly Inner _inner;
+        private IDisposable? _subscription;
+
+        public BindingValueSubjectAdapter(ISubject<T> source)
+        {
+            _source = source;
+            _inner = new Inner(this);
+        }
+
+        public void OnCompleted() => _source.OnCompleted();
+        public void OnError(Exception error) => _source.OnError(error);
+        
+        public void OnNext(BindingValue<T> value)
+        {
+            if (value.HasValue)
+            {
+                _source.OnNext(value.Value);
+            }
+        }
+
+        protected override void Subscribed() => _subscription = _source.Subscribe(_inner);
+        protected override void Unsubscribed() => _subscription?.Dispose();
+
+        private class Inner : IObserver<T>
+        {
+            private readonly BindingValueSubjectAdapter<T> _owner;
+
+            public Inner(BindingValueSubjectAdapter<T> owner) => _owner = owner;
+
+            public void OnCompleted() => _owner.PublishCompleted();
+            public void OnError(Exception error) => _owner.PublishError(error);
+            public void OnNext(T value) => _owner.PublishNext(BindingValue<T>.FromUntyped(value));
+        }
+    }
+}

+ 35 - 0
src/Avalonia.Base/Reactive/BindingValueExtensions.cs

@@ -0,0 +1,35 @@
+using System;
+using System.Reactive.Subjects;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.Reactive
+{
+    public static class BindingValueExtensions
+    {
+        public static IObservable<BindingValue<T>> ToBindingValue<T>(this IObservable<T> source)
+        {
+            source = source ?? throw new ArgumentNullException(nameof(source));
+            return new BindingValueAdapter<T>(source);
+        }
+
+        public static ISubject<BindingValue<T>> ToBindingValue<T>(this ISubject<T> source)
+        {
+            source = source ?? throw new ArgumentNullException(nameof(source));
+            return new BindingValueSubjectAdapter<T>(source);
+        }
+
+        public static IObservable<object?> ToUntyped<T>(this IObservable<BindingValue<T>> source)
+        {
+            source = source ?? throw new ArgumentNullException(nameof(source));
+            return new UntypedBindingAdapter<T>(source);
+        }
+
+        public static ISubject<object?> ToUntyped<T>(this ISubject<BindingValue<T>> source)
+        {
+            source = source ?? throw new ArgumentNullException(nameof(source));
+            return new UntypedBindingSubjectAdapter<T>(source);
+        }
+    }
+}

+ 63 - 0
src/Avalonia.Base/Reactive/TypedBindingAdapter.cs

@@ -0,0 +1,63 @@
+using System;
+using Avalonia.Data;
+using Avalonia.Logging;
+
+#nullable enable
+
+namespace Avalonia.Reactive
+{
+    internal class TypedBindingAdapter<T> : SingleSubscriberObservableBase<BindingValue<T>>,
+        IObserver<BindingValue<object>>
+    {
+        private readonly IAvaloniaObject _target;
+        private readonly AvaloniaProperty<T> _property;
+        private readonly IObservable<BindingValue<object>> _source;
+        private IDisposable? _subscription;
+
+        public TypedBindingAdapter(
+            IAvaloniaObject target,
+            AvaloniaProperty<T> property,
+            IObservable<BindingValue<object>> source)
+        {
+            _target = target;
+            _property = property;
+            _source = source;
+        }
+
+        public void OnNext(BindingValue<object> value)
+        {
+            try
+            {
+                PublishNext(value.Convert<T>());
+            }
+            catch (InvalidCastException e)
+            {
+                Logger.TryGet(LogEventLevel.Error)?.Log(
+                    LogArea.Binding,
+                    _target,
+                    "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
+                    _property.Name,
+                    _property.PropertyType,
+                    value.Value,
+                    value.Value?.GetType());
+                PublishNext(BindingValue<T>.BindingError(e));
+            }
+        }
+
+        public void OnCompleted() => PublishCompleted();
+        public void OnError(Exception error) => PublishError(error);
+
+        public static IObservable<BindingValue<T>> Create(
+            IAvaloniaObject target,
+            AvaloniaProperty<T> property,
+            IObservable<BindingValue<object>> source)
+        {
+            return source is IObservable<BindingValue<T>> result ?
+                result :
+                new TypedBindingAdapter<T>(target, property, source);
+        }
+
+        protected override void Subscribed() => _subscription = _source.Subscribe(this);
+        protected override void Unsubscribed() => _subscription?.Dispose();
+    }
+}

+ 57 - 0
src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs

@@ -0,0 +1,57 @@
+using System;
+using System.Reactive.Subjects;
+using Avalonia.Data;
+
+#nullable enable
+
+namespace Avalonia.Reactive
+{
+    internal class UntypedBindingAdapter<T> : SingleSubscriberObservableBase<object?>,
+        IObserver<BindingValue<T>>
+    {
+        private readonly IObservable<BindingValue<T>> _source;
+        private IDisposable? _subscription;
+
+        public UntypedBindingAdapter(IObservable<BindingValue<T>> source) => _source = source;
+        public void OnCompleted() => PublishCompleted();
+        public void OnError(Exception error) => PublishError(error);
+        public void OnNext(BindingValue<T> value) => value.ToUntyped();
+        protected override void Subscribed() => _subscription = _source.Subscribe(this);
+        protected override void Unsubscribed() => _subscription?.Dispose();
+    }
+
+    internal class UntypedBindingSubjectAdapter<T> : SingleSubscriberObservableBase<object?>,
+        ISubject<object?>
+    {
+        private readonly ISubject<BindingValue<T>> _source;
+        private readonly Inner _inner;
+        private IDisposable? _subscription;
+
+        public UntypedBindingSubjectAdapter(ISubject<BindingValue<T>> source)
+        {
+            _source = source;
+            _inner = new Inner(this);
+        }
+
+        public void OnCompleted() => _source.OnCompleted();
+        public void OnError(Exception error) => _source.OnError(error);
+        public void OnNext(object? value)
+        {
+            _source.OnNext(BindingValue<T>.FromUntyped(value));
+        }
+
+        protected override void Subscribed() => _subscription = _source.Subscribe(_inner);
+        protected override void Unsubscribed() => _subscription?.Dispose();
+
+        private class Inner : IObserver<BindingValue<T>>
+        {
+            private readonly UntypedBindingSubjectAdapter<T> _owner;
+
+            public Inner(UntypedBindingSubjectAdapter<T> owner) => _owner = owner;
+
+            public void OnCompleted() => _owner.PublishCompleted();
+            public void OnError(Exception error) => _owner.PublishError(error);
+            public void OnNext(BindingValue<T> value) => _owner.PublishNext(value.ToUntyped());
+        }
+    }
+}

+ 3 - 1
src/Avalonia.Base/StyledProperty.cs

@@ -17,14 +17,16 @@ namespace Avalonia
         /// <param name="ownerType">The type of the class that registers the property.</param>
         /// <param name="metadata">The property metadata.</param>
         /// <param name="inherits">Whether the property inherits its value.</param>
+        /// <param name="validate">A value validation callback.</param>
         /// <param name="notifying">A <see cref="AvaloniaProperty.Notifying"/> callback.</param>
         public StyledProperty(
             string name,
             Type ownerType,
             StyledPropertyMetadata<TValue> metadata,
             bool inherits = false,
+            Func<TValue, bool> validate = null,
             Action<IAvaloniaObject, bool> notifying = null)
-            : base(name, ownerType, metadata, inherits, notifying)
+            : base(name, ownerType, metadata, inherits, validate, notifying)
         {
         }
 

+ 96 - 29
src/Avalonia.Base/StyledPropertyBase.cs

@@ -3,13 +3,15 @@
 
 using System;
 using System.Diagnostics;
+using Avalonia.Data;
+using Avalonia.Reactive;
 
 namespace Avalonia
 {
     /// <summary>
     /// Base class for styled properties.
     /// </summary>
-    public class StyledPropertyBase<TValue> : AvaloniaProperty<TValue>, IStyledPropertyAccessor
+    public abstract class StyledPropertyBase<TValue> : AvaloniaProperty<TValue>, IStyledPropertyAccessor
     {
         private bool _inherits;
 
@@ -20,12 +22,14 @@ namespace Avalonia
         /// <param name="ownerType">The type of the class that registers the property.</param>
         /// <param name="metadata">The property metadata.</param>
         /// <param name="inherits">Whether the property inherits its value.</param>
+        /// <param name="validate">A value validation callback.</param>
         /// <param name="notifying">A <see cref="AvaloniaProperty.Notifying"/> callback.</param>
         protected StyledPropertyBase(
             string name,
             Type ownerType,            
             StyledPropertyMetadata<TValue> metadata,
             bool inherits = false,
+            Func<TValue, bool> validate = null,
             Action<IAvaloniaObject, bool> notifying = null)
                 : base(name, ownerType, metadata, notifying)
         {
@@ -38,6 +42,14 @@ namespace Avalonia
             }
 
             _inherits = inherits;
+            ValidateValue = validate;
+            HasCoercion |= metadata.CoerceValue != null;
+
+            if (validate?.Invoke(metadata.DefaultValue) == false)
+            {
+                throw new ArgumentException(
+                    $"'{metadata.DefaultValue}' is not a valid default value for '{name}'.");
+            }
         }
 
         /// <summary>
@@ -59,6 +71,29 @@ namespace Avalonia
         /// </value>
         public override bool Inherits => _inherits;
 
+        /// <summary>
+        /// Gets the value validation callback for the property.
+        /// </summary>
+        public Func<TValue, bool> ValidateValue { get; }
+
+        /// <summary>
+        /// Gets a value indicating whether this property has any value coercion callbacks defined
+        /// in its metadata.
+        /// </summary>
+        internal bool HasCoercion { get; private set; }
+
+        public TValue CoerceValue(IAvaloniaObject instance, TValue baseValue)
+        {
+            var metadata = GetMetadata(instance.GetType());
+
+            if (metadata.CoerceValue != null)
+            {
+                return metadata.CoerceValue.Invoke(instance, baseValue);
+            }
+
+            return baseValue;
+        }
+
         /// <summary>
         /// Gets the default value for the property on the specified type.
         /// </summary>
@@ -68,7 +103,7 @@ namespace Avalonia
         {
             Contract.Requires<ArgumentNullException>(type != null);
 
-            return GetMetadata(type).DefaultValue.Typed;
+            return GetMetadata(type).DefaultValue;
         }
 
         /// <summary>
@@ -120,31 +155,18 @@ namespace Avalonia
         /// <param name="metadata">The metadata.</param>
         public void OverrideMetadata(Type type, StyledPropertyMetadata<TValue> metadata)
         {
-            base.OverrideMetadata(type, metadata);
-        }
-
-        /// <summary>
-        /// Overrides the validation function for the specified type.
-        /// </summary>
-        /// <typeparam name="THost">The type.</typeparam>
-        /// <param name="validate">The validation function.</param>
-        public void OverrideValidation<THost>(Func<THost, TValue, TValue> validate)
-            where THost : IAvaloniaObject
-        {
-            Func<IAvaloniaObject, TValue, TValue> f;
-
-            if (validate != null)
-            {
-                f = Cast(validate);
-            }
-            else
+            if (ValidateValue != null)
             {
-                // Passing null to the validation function means that the property metadata merge
-                // will take the base validation function, so instead use an empty validation.
-                f = (o, v) => v;
+                if (!ValidateValue(metadata.DefaultValue))
+                {
+                    throw new ArgumentException(
+                        $"'{metadata.DefaultValue}' is not a valid default value for '{Name}'.");
+                }
             }
 
-            base.OverrideMetadata(typeof(THost), new StyledPropertyMetadata<TValue>(validate: f));
+            HasCoercion |= metadata.CoerceValue != null;
+
+            base.OverrideMetadata(type, metadata);
         }
 
         /// <summary>
@@ -157,20 +179,65 @@ namespace Avalonia
         }
 
         /// <inheritdoc/>
-        Func<IAvaloniaObject, object, object> IStyledPropertyAccessor.GetValidationFunc(Type type)
+        object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type);
+
+        /// <inheritdoc/>
+        internal override void RouteClearValue(IAvaloniaObject o)
         {
-            Contract.Requires<ArgumentNullException>(type != null);
-            return ((IStyledPropertyMetadata)base.GetMetadata(type)).Validate;
+            o.ClearValue<TValue>(this);
         }
 
         /// <inheritdoc/>
-        object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type);
+        internal override object RouteGetValue(IAvaloniaObject o)
+        {
+            return o.GetValue<TValue>(this);
+        }
+
+        /// <inheritdoc/>
+        internal override void RouteSetValue(
+            IAvaloniaObject o,
+            object value,
+            BindingPriority priority)
+        {
+            var v = TryConvert(value);
+
+            if (v.HasValue)
+            {
+                o.SetValue<TValue>(this, (TValue)v.Value, priority);
+            }
+            else if (v.Type == BindingValueType.UnsetValue)
+            {
+                o.ClearValue(this);
+            }
+            else if (v.HasError)
+            {
+                throw v.Error;
+            }
+        }
+
+        /// <inheritdoc/>
+        internal override IDisposable RouteBind(
+            IAvaloniaObject o,
+            IObservable<BindingValue<object>> source,
+            BindingPriority priority)
+        {
+            var adapter = TypedBindingAdapter<TValue>.Create(o, this, source);
+            return o.Bind<TValue>(this, adapter, priority);
+        }
+
+        /// <inheritdoc/>
+        internal override void RouteInheritanceParentChanged(
+            AvaloniaObject o,
+            IAvaloniaObject oldParent)
+        {
+            o.InheritanceParentChanged(this, oldParent);
+        }
 
         private object GetDefaultBoxedValue(Type type)
         {
             Contract.Requires<ArgumentNullException>(type != null);
 
-            return GetMetadata(type).DefaultValue.Boxed;
+            return GetMetadata(type).DefaultValue;
         }
 
         [DebuggerHidden]

+ 16 - 29
src/Avalonia.Base/StyledPropertyMetadata`1.cs

@@ -12,35 +12,35 @@ namespace Avalonia
     /// </summary>
     public class StyledPropertyMetadata<TValue> : PropertyMetadata, IStyledPropertyMetadata
     {
+        private Optional<TValue> _defaultValue;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="StyledPropertyMetadata{TValue}"/> class.
         /// </summary>
         /// <param name="defaultValue">The default value of the property.</param>
-        /// <param name="validate">A validation function.</param>
         /// <param name="defaultBindingMode">The default binding mode.</param>
+        /// <param name="coerce">A value coercion callback.</param>
         public StyledPropertyMetadata(
-            TValue defaultValue = default,
-            Func<IAvaloniaObject, TValue, TValue> validate = null,
-            BindingMode defaultBindingMode = BindingMode.Default)
+            Optional<TValue> defaultValue = default,
+            BindingMode defaultBindingMode = BindingMode.Default,
+            Func<IAvaloniaObject, TValue, TValue> coerce = null)
                 : base(defaultBindingMode)
         {
-            DefaultValue = new BoxedValue<TValue>(defaultValue);
-            Validate = validate;
+            _defaultValue = defaultValue;
+            CoerceValue = coerce;
         }
 
         /// <summary>
         /// Gets the default value for the property.
         /// </summary>
-        internal BoxedValue<TValue> DefaultValue { get; private set; }
+        public TValue DefaultValue => _defaultValue.GetValueOrDefault();
 
         /// <summary>
-        /// Gets the validation callback.
+        /// Gets the value coercion callback, if any.
         /// </summary>
-        public Func<IAvaloniaObject, TValue, TValue> Validate { get; private set; }
-
-        object IStyledPropertyMetadata.DefaultValue => DefaultValue.Boxed;
+        public Func<IAvaloniaObject, TValue, TValue>? CoerceValue { get; private set; }
 
-        Func<IAvaloniaObject, object, object> IStyledPropertyMetadata.Validate => Cast(Validate);
+        object IStyledPropertyMetadata.DefaultValue => DefaultValue;
 
         /// <inheritdoc/>
         public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty property)
@@ -49,29 +49,16 @@ namespace Avalonia
 
             if (baseMetadata is StyledPropertyMetadata<TValue> src)
             {
-                if (DefaultValue.Boxed == null)
+                if (!_defaultValue.HasValue)
                 {
-                    DefaultValue = src.DefaultValue;
+                    _defaultValue = src.DefaultValue;
                 }
 
-                if (Validate == null)
+                if (CoerceValue == null)
                 {
-                    Validate = src.Validate;
+                    CoerceValue = src.CoerceValue;
                 }
             }
         }
-
-        [DebuggerHidden]
-        private static Func<IAvaloniaObject, object, object> Cast(Func<IAvaloniaObject, TValue, TValue> f)
-        {
-            if (f == null)
-            {
-                return null;
-            }
-            else
-            {
-                return (o, v) => f(o, (TValue)v);
-            }
-        }
     }
 }

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

@@ -129,6 +129,27 @@ namespace Avalonia.Utilities
             _entries[TryFindEntry(property.Id).Item1].Value = value;
         }
 
+        public void Remove(AvaloniaProperty property)
+        {
+            var (index, found) = TryFindEntry(property.Id);
+
+            if (found)
+            {
+                Entry[] entries = new Entry[_entries.Length - 1];
+                int ix = 0;
+
+                for (int i = 0; i < _entries.Length; ++i)
+                {
+                    if (i != index)
+                    {
+                        entries[ix++] = _entries[i];
+                    }
+                }
+
+                _entries = entries;
+            }
+        }
+
         public Dictionary<AvaloniaProperty, TValue> ToDictionary()
         {
             var dict = new Dictionary<AvaloniaProperty, TValue>(_entries.Length - 1);

+ 0 - 128
src/Avalonia.Base/Utilities/DeferredSetter.cs

@@ -1,128 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-
-namespace Avalonia.Utilities
-{
-    /// <summary>
-    /// A utility class to enable deferring assignment until after property-changed notifications are sent.
-    /// Used to fix #855.
-    /// </summary>
-    /// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
-    internal sealed class DeferredSetter<TSetRecord>
-    {
-        private readonly SingleOrQueue<TSetRecord> _pendingValues;
-        private bool _isNotifying;
-
-        public DeferredSetter()
-        {
-            _pendingValues = new SingleOrQueue<TSetRecord>();
-        }
-
-        private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty<TSetRecord> property, ref TSetRecord backing, TSetRecord value)
-        {
-            var old = backing;
-
-            backing = value;
-
-            source.RaisePropertyChanged(property, old, value);
-        }
-
-        public bool SetAndNotify(
-            AvaloniaObject source,
-            AvaloniaProperty<TSetRecord> property,
-            ref TSetRecord backing,
-            TSetRecord value)
-        {
-            if (!_isNotifying)
-            {
-                using (new NotifyDisposable(this))
-                {
-                    SetAndRaisePropertyChanged(source, property, ref backing, value);
-                }
-
-                if (!_pendingValues.Empty)
-                {
-                    using (new NotifyDisposable(this))
-                    {
-                        while (!_pendingValues.Empty)
-                        {
-                            SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue());
-                        }
-                    }
-                }
-
-                return true;
-            }
-
-            _pendingValues.Enqueue(value);
-
-            return false;
-        }
-
-        public bool SetAndNotifyCallback<TValue>(AvaloniaProperty property, ISetAndNotifyHandler<TValue> setAndNotifyHandler, ref TValue backing, TValue value)
-            where TValue : TSetRecord
-        {
-            if (!_isNotifying)
-            {
-                using (new NotifyDisposable(this))
-                {
-                    setAndNotifyHandler.HandleSetAndNotify(property, ref backing, value);
-                }
-
-                if (!_pendingValues.Empty)
-                {
-                    using (new NotifyDisposable(this))
-                    {
-                        while (!_pendingValues.Empty)
-                        {
-                            setAndNotifyHandler.HandleSetAndNotify(property, ref backing, (TValue)_pendingValues.Dequeue());
-                        }
-                    }
-                }
-
-                return true;
-            }
-
-            _pendingValues.Enqueue(value);
-
-            return false;
-        }
-
-        /// <summary>
-        /// Disposable that marks the property as currently notifying.
-        /// When disposed, marks the property as done notifying.
-        /// </summary>
-        private readonly struct NotifyDisposable : IDisposable
-        {
-            private readonly DeferredSetter<TSetRecord> _setter;
-
-            internal NotifyDisposable(DeferredSetter<TSetRecord> setter)
-            {
-                _setter = setter;
-                _setter._isNotifying = true;
-            }
-
-            public void Dispose()
-            {
-                _setter._isNotifying = false;
-            }
-        }
-    }
-
-    /// <summary>
-    /// Handler for set and notify requests.
-    /// </summary>
-    /// <typeparam name="TValue">Value type.</typeparam>
-    internal interface ISetAndNotifyHandler<TValue>
-    {
-        /// <summary>
-        /// Handles deferred setter requests to set a value.
-        /// </summary>
-        /// <param name="property">Property being set.</param>
-        /// <param name="backing">Backing field reference.</param>
-        /// <param name="value">New value.</param>
-        void HandleSetAndNotify(AvaloniaProperty property, ref TValue backing, TValue value);
-    }
-}

+ 36 - 16
src/Avalonia.Base/Utilities/TypeUtilities.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.ComponentModel;
 using System.Globalization;
 using System.Linq;
 using System.Reflection;
@@ -92,8 +93,7 @@ namespace Avalonia.Utilities
         /// <returns>True if the type accepts null values; otherwise false.</returns>
         public static bool AcceptsNull(Type type)
         {
-            var t = type.GetTypeInfo();
-            return !t.IsValueType || (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(Nullable<>)));
+            return !type.IsValueType || IsNullableType(type);
         }
 
         /// <summary>
@@ -119,10 +119,8 @@ namespace Avalonia.Utilities
             }
 
             var from = value.GetType();
-            var fromTypeInfo = from.GetTypeInfo();
-            var toTypeInfo = to.GetTypeInfo();
 
-            if (toTypeInfo.IsAssignableFrom(fromTypeInfo))
+            if (to.IsAssignableFrom(from))
             {
                 result = value;
                 return true;
@@ -134,7 +132,7 @@ namespace Avalonia.Utilities
                 return true;
             }
 
-            if (toTypeInfo.IsEnum && from == typeof(string))
+            if (to.IsEnum && from == typeof(string))
             {
                 if (Enum.IsDefined(to, (string)value))
                 {
@@ -143,7 +141,7 @@ namespace Avalonia.Utilities
                 }
             }
 
-            if (!fromTypeInfo.IsEnum && toTypeInfo.IsEnum)
+            if (!from.IsEnum && to.IsEnum)
             {
                 result = null;
 
@@ -154,7 +152,7 @@ namespace Avalonia.Utilities
                 }
             }
 
-            if (fromTypeInfo.IsEnum && IsNumeric(to))
+            if (from.IsEnum && IsNumeric(to))
             {
                 try
                 {
@@ -188,6 +186,14 @@ namespace Avalonia.Utilities
                 }
             }
 
+            var typeConverter = TypeDescriptor.GetConverter(to);
+
+            if (typeConverter.CanConvertFrom(from) == true)
+            {
+                result = typeConverter.ConvertFrom(null, culture, value);
+                return true;
+            }
+
             var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit | OperatorType.Explicit);
 
             if (cast != null)
@@ -223,10 +229,8 @@ namespace Avalonia.Utilities
             }
 
             var from = value.GetType();
-            var fromTypeInfo = from.GetTypeInfo();
-            var toTypeInfo = to.GetTypeInfo();
 
-            if (toTypeInfo.IsAssignableFrom(fromTypeInfo))
+            if (to.IsAssignableFrom(from))
             {
                 result = value;
                 return true;
@@ -289,6 +293,17 @@ namespace Avalonia.Utilities
             return TryConvertImplicit(type, value, out object result) ? result : Default(type);
         }
 
+        public static T ConvertImplicit<T>(object value)
+        {
+            if (TryConvertImplicit(typeof(T), value, out var result))
+            {
+                return (T)result;
+            }
+
+            throw new InvalidCastException(
+                $"Unable to convert object '{value ?? "(null)"}' of type '{value?.GetType()}' to type '{typeof(T)}'.");
+        }
+
         /// <summary>
         /// Gets the default value for the specified type.
         /// </summary>
@@ -296,9 +311,7 @@ namespace Avalonia.Utilities
         /// <returns>The default value.</returns>
         public static object Default(Type type)
         {
-            var typeInfo = type.GetTypeInfo();
-
-            if (typeInfo.IsValueType)
+            if (type.IsValueType)
             {
                 return Activator.CreateInstance(type);
             }
@@ -324,9 +337,11 @@ namespace Avalonia.Utilities
                 return false;
             }
 
-            if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
+            Type underlyingType = Nullable.GetUnderlyingType(type);
+
+            if (underlyingType != null)
             {
-                return IsNumeric(Nullable.GetUnderlyingType(type));
+                return IsNumeric(underlyingType);
             }
             else
             {
@@ -341,6 +356,11 @@ namespace Avalonia.Utilities
             Explicit = 2
         }
 
+        private static bool IsNullableType(Type type)
+        {
+            return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
+        }
+
         private static MethodInfo FindTypeConversionOperatorMethod(Type fromType, Type toType, OperatorType operatorType)
         {
             const string implicitName = "op_Implicit";

+ 94 - 0
src/Avalonia.Base/Utilities/ValueSingleOrList.cs

@@ -0,0 +1,94 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Utilities
+{
+    /// <summary>
+    /// A list like struct optimized for holding zero or one items.
+    /// </summary>
+    /// <typeparam name="T">The type of items held in the list.</typeparam>
+    /// <remarks>
+    /// Once more than value has been added to this storage it will switch to using <see cref="List"/> internally.
+    /// </remarks>
+    public ref struct ValueSingleOrList<T>
+    {
+        private bool _isSingleSet;
+
+        /// <summary>
+        /// Single contained value. Only valid if <see cref="IsSingle"/> is set.
+        /// </summary>
+        public T Single { get; private set; }
+
+        /// <summary>
+        /// List of values.
+        /// </summary>
+        public List<T> List { get; private set; }
+
+        /// <summary>
+        /// If this struct is backed by a list.
+        /// </summary>
+        public bool HasList => List != null;
+
+        /// <summary>
+        /// If this struct contains only single value and storage was not promoted to a list.
+        /// </summary>
+        public bool IsSingle => List == null && _isSingleSet;
+
+        /// <summary>
+        /// Adds a value.
+        /// </summary>
+        /// <param name="value">Value to add.</param>
+        public void Add(T value)
+        {
+            if (List != null)
+            {
+                List.Add(value);
+            }
+            else
+            {
+                if (!_isSingleSet)
+                {
+                    Single = value;
+
+                    _isSingleSet = true;
+                }
+                else
+                {
+                    List = new List<T>();
+
+                    List.Add(Single);
+                    List.Add(value);
+
+                    Single = default;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Removes a value.
+        /// </summary>
+        /// <param name="value">Value to remove.</param>
+        public bool Remove(T value)
+        {
+            if (List != null)
+            {
+                return List.Remove(value);
+            }
+
+            if (!_isSingleSet)
+            {
+                return false;
+            }
+
+            if (EqualityComparer<T>.Default.Equals(Single, value))
+            {
+                Single = default;
+
+                _isSingleSet = false;
+
+                return true;
+            }
+
+            return false;
+        }
+    }
+}

+ 7 - 2
src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs

@@ -183,11 +183,16 @@ namespace Avalonia.Utilities
                 for (int c = 0; c < _count; c++)
                 {
                     var r = _data[c];
+
+                    TSubscriber target = null;
+
+                    r.Subscriber?.TryGetTarget(out target);
+
                     //Mark current index as first empty
-                    if (r.Subscriber == null && empty == -1)
+                    if (target == null && empty == -1)
                         empty = c;
                     //If current element isn't null and we have an empty one
-                    if (r.Subscriber != null && empty != -1)
+                    if (target != null && empty != -1)
                     {
                         _data[c] = default;
                         _data[empty] = r;

+ 203 - 132
src/Avalonia.Base/ValueStore.cs

@@ -1,205 +1,276 @@
 using System;
-using System.Collections.Generic;
 using Avalonia.Data;
+using Avalonia.PropertyStore;
 using Avalonia.Utilities;
 
+#nullable enable
+
 namespace Avalonia
 {
-    internal class ValueStore : IPriorityValueOwner
+    /// <summary>
+    /// Stores styled property values for an <see cref="AvaloniaObject"/>.
+    /// </summary>
+    /// <remarks>
+    /// At its core this class consists of an <see cref="AvaloniaProperty"/> to 
+    /// <see cref="IValue"/> mapping which holds the current values for each set property. This
+    /// <see cref="IValue"/> can be in one of 4 states:
+    /// 
+    /// - For a single local value it will be an instance of <see cref="LocalValueEntry{T}"/>.
+    /// - For a single value of a priority other than LocalValue it will be an instance of
+    ///   <see cref="ConstantValueEntry{T}"/>`
+    /// - For a single binding it will be an instance of <see cref="BindingEntry{T}"/>
+    /// - For all other cases it will be an instance of <see cref="PriorityValue{T}"/>
+    /// </remarks>
+    internal class ValueStore : IValueSink
     {
-        private readonly AvaloniaPropertyValueStore<object> _propertyValues;
-        private readonly AvaloniaPropertyValueStore<object> _deferredSetters;
         private readonly AvaloniaObject _owner;
+        private readonly IValueSink _sink;
+        private readonly AvaloniaPropertyValueStore<IValue> _values;
 
         public ValueStore(AvaloniaObject owner)
         {
-            _owner = owner;
-            _propertyValues = new AvaloniaPropertyValueStore<object>();
-            _deferredSetters = new AvaloniaPropertyValueStore<object>();
+            _sink = _owner = owner;
+            _values = new AvaloniaPropertyValueStore<IValue>();
         }
 
-        public IDisposable AddBinding(
-            AvaloniaProperty property,
-            IObservable<object> source,
-            BindingPriority priority)
+        public bool IsAnimating(AvaloniaProperty property)
         {
-            PriorityValue priorityValue;
-
-            if (_propertyValues.TryGetValue(property, out var v))
-            {
-                priorityValue = v as PriorityValue;
-
-                if (priorityValue == null)
-                {
-                    priorityValue = CreatePriorityValue(property);
-                    priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
-                    _propertyValues.SetValue(property, priorityValue);
-                }
-            }
-            else
+            if (_values.TryGetValue(property, out var slot))
             {
-                priorityValue = CreatePriorityValue(property);
-                _propertyValues.AddValue(property, priorityValue);
+                return slot.ValuePriority < BindingPriority.LocalValue;
             }
 
-            return priorityValue.Add(source, (int)priority);
+            return false;
         }
 
-        public void AddValue(AvaloniaProperty property, object value, int priority)
+        public bool IsSet(AvaloniaProperty property)
         {
-            PriorityValue priorityValue;
-
-            if (_propertyValues.TryGetValue(property, out var v))
+            if (_values.TryGetValue(property, out var slot))
             {
-                priorityValue = v as PriorityValue;
-
-                if (priorityValue == null)
-                {
-                    if (priority == (int)BindingPriority.LocalValue)
-                    {
-                        Validate(property, ref value);
-                        _propertyValues.SetValue(property, value);
-                        Changed(property, priority, v, value);
-                        return;
-                    }
-                    else
-                    {
-                        priorityValue = CreatePriorityValue(property);
-                        priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
-                        _propertyValues.SetValue(property, priorityValue);
-                    }
-                }
+                return slot.Value.HasValue;
             }
-            else
+
+            return false;
+        }
+
+        public bool TryGetValue<T>(StyledPropertyBase<T> property, out T value)
+        {
+            if (_values.TryGetValue(property, out var slot))
             {
-                if (value == AvaloniaProperty.UnsetValue)
-                {
-                    return;
-                }
+                var v = (IValue<T>)slot;
 
-                if (priority == (int)BindingPriority.LocalValue)
-                {
-                    Validate(property, ref value);
-                    _propertyValues.AddValue(property, value);
-                    Changed(property, priority, AvaloniaProperty.UnsetValue, value);
-                    return;
-                }
-                else
+                if (v.Value.HasValue)
                 {
-                    priorityValue = CreatePriorityValue(property);
-                    _propertyValues.AddValue(property, priorityValue);
+                    value = v.Value.Value;
+                    return true;
                 }
             }
 
-            priorityValue.SetValue(value, priority);
+            value = default!;
+            return false;
         }
 
-        public void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
+        public void SetValue<T>(StyledPropertyBase<T> property, T value, BindingPriority priority)
         {
-            _owner.BindingNotificationReceived(property, notification);
+            if (property.ValidateValue?.Invoke(value) == false)
+            {
+                throw new ArgumentException($"{value} is not a valid value for '{property.Name}.");
+            }
+
+            if (_values.TryGetValue(property, out var slot))
+            {
+                SetExisting(slot, property, value, priority);
+            }
+            else if (property.HasCoercion)
+            {
+                // If the property has any coercion callbacks then always create a PriorityValue.
+                var entry = new PriorityValue<T>(_owner, property, this);
+                _values.AddValue(property, entry);
+                entry.SetValue(value, priority);
+            }
+            else if (priority == BindingPriority.LocalValue)
+            {
+                _values.AddValue(property, new LocalValueEntry<T>(value));
+                _sink.ValueChanged(property, priority, default, value);
+            }
+            else
+            {
+                var entry = new ConstantValueEntry<T>(property, value, priority);
+                _values.AddValue(property, entry);
+                _sink.ValueChanged(property, priority, default, value);
+            }
         }
 
-        public void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue)
+        public IDisposable AddBinding<T>(
+            StyledPropertyBase<T> property,
+            IObservable<BindingValue<T>> source,
+            BindingPriority priority)
         {
-            _owner.PriorityValueChanged(property, priority, oldValue, newValue);
+            if (_values.TryGetValue(property, out var slot))
+            {
+                return BindExisting(slot, property, source, priority);
+            }
+            else if (property.HasCoercion)
+            {
+                // If the property has any coercion callbacks then always create a PriorityValue.
+                var entry = new PriorityValue<T>(_owner, property, this);
+                var binding = entry.AddBinding(source, priority);
+                _values.AddValue(property, entry);
+                binding.Start();
+                return binding;
+            }
+            else
+            {
+                var entry = new BindingEntry<T>(_owner, property, source, priority, this);
+                _values.AddValue(property, entry);
+                entry.Start();
+                return entry;
+            }
         }
 
-        public IDictionary<AvaloniaProperty, object> GetSetValues()
+        public void ClearLocalValue<T>(StyledPropertyBase<T> property)
         {
-            return _propertyValues.ToDictionary();
+            if (_values.TryGetValue(property, out var slot))
+            {
+                if (slot is PriorityValue<T> p)
+                {
+                    p.ClearLocalValue();
+                }
+                else
+                {
+                    var remove = slot is ConstantValueEntry<T> c ?
+                        c.Priority == BindingPriority.LocalValue : 
+                        !(slot is IPriorityValueEntry<T>);
+
+                    if (remove)
+                    {
+                        var old = TryGetValue(property, out var value) ? value : default;
+                        _values.Remove(property);
+                        _sink.ValueChanged(
+                            property,
+                            BindingPriority.Unset,
+                            old,
+                            BindingValue<T>.Unset);
+                    }
+                }
+            }
         }
 
-        public void LogError(AvaloniaProperty property, Exception e)
+        public void CoerceValue<T>(StyledPropertyBase<T> property)
         {
-            _owner.LogBindingError(property, e);
+            if (_values.TryGetValue(property, out var slot))
+            {
+                if (slot is PriorityValue<T> p)
+                {
+                    p.CoerceValue();
+                }
+            }
         }
 
-        public object GetValue(AvaloniaProperty property)
+        public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property)
         {
-            var result = AvaloniaProperty.UnsetValue;
-
-            if (_propertyValues.TryGetValue(property, out var value))
+            if (_values.TryGetValue(property, out var slot))
             {
-                result = (value is PriorityValue priorityValue) ? priorityValue.Value : value;
+                return new Diagnostics.AvaloniaPropertyValue(
+                    property,
+                    slot.Value.HasValue ? slot.Value.Value : AvaloniaProperty.UnsetValue,
+                    slot.ValuePriority,
+                    null);
             }
 
-            return result;
+            return null;
         }
 
-        public bool IsAnimating(AvaloniaProperty property)
+        void IValueSink.ValueChanged<T>(
+            StyledPropertyBase<T> property,
+            BindingPriority priority,
+            Optional<T> oldValue,
+            BindingValue<T> newValue)
         {
-            return _propertyValues.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
+            _sink.ValueChanged(property, priority, oldValue, newValue);
         }
 
-        public bool IsSet(AvaloniaProperty property)
+        void IValueSink.Completed<T>(
+            StyledPropertyBase<T> property,
+            IPriorityValueEntry entry,
+            Optional<T> oldValue)
         {
-            if (_propertyValues.TryGetValue(property, out var value))
+            if (_values.TryGetValue(property, out var slot))
             {
-                return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue;
+                if (slot == entry)
+                {
+                    _values.Remove(property);
+                    _sink.Completed(property, entry, oldValue);
+                }
             }
-
-            return false;
         }
 
-        public void Revalidate(AvaloniaProperty property)
+        private void SetExisting<T>(
+            object slot,
+            StyledPropertyBase<T> property,
+            T value,
+            BindingPriority priority)
         {
-            if (_propertyValues.TryGetValue(property, out var value))
+            if (slot is IPriorityValueEntry<T> e)
             {
-                (value as PriorityValue)?.Revalidate();
+                var priorityValue = new PriorityValue<T>(_owner, property, this, e);
+                _values.SetValue(property, priorityValue);
+                priorityValue.SetValue(value, priority);
             }
-        }
-
-        public void VerifyAccess() => _owner.VerifyAccess();
-
-        private PriorityValue CreatePriorityValue(AvaloniaProperty property)
-        {
-            var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType());
-            Func<object, object> validate2 = null;
-
-            if (validate != null)
+            else if (slot is PriorityValue<T> p)
             {
-                validate2 = v => validate(_owner, v);
+                p.SetValue(value, priority);
+            }
+            else if (slot is LocalValueEntry<T> l)
+            {
+                if (priority == BindingPriority.LocalValue)
+                {
+                    var old = l.Value;
+                    l.SetValue(value);
+                    _sink.ValueChanged(property, priority, old, value);
+                }
+                else
+                {
+                    var priorityValue = new PriorityValue<T>(_owner, property, this, l);
+                    priorityValue.SetValue(value, priority);
+                    _values.SetValue(property, priorityValue);
+                }
+            }
+            else
+            {
+                throw new NotSupportedException("Unrecognised value store slot type.");
             }
-
-            return new PriorityValue(
-                this,
-                property,
-                property.PropertyType,
-                validate2);
         }
 
-        private void Validate(AvaloniaProperty property, ref object value)
+        private IDisposable BindExisting<T>(
+            object slot,
+            StyledPropertyBase<T> property,
+            IObservable<BindingValue<T>> source,
+            BindingPriority priority)
         {
-            var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType());
+            PriorityValue<T> priorityValue;
 
-            if (validate != null && value != AvaloniaProperty.UnsetValue)
+            if (slot is IPriorityValueEntry<T> e)
             {
-                value = validate(_owner, value);
+                priorityValue = new PriorityValue<T>(_owner, property, this, e);
             }
-        }
-
-        private DeferredSetter<T> GetDeferredSetter<T>(AvaloniaProperty property)
-        {
-            if (_deferredSetters.TryGetValue(property, out var deferredSetter))
+            else if (slot is PriorityValue<T> p)
             {
-                return (DeferredSetter<T>)deferredSetter;
+                priorityValue = p;
+            }
+            else if (slot is LocalValueEntry<T> l)
+            {
+                priorityValue = new PriorityValue<T>(_owner, property, this, l);
+            }
+            else
+            {
+                throw new NotSupportedException("Unrecognised value store slot type.");
             }
 
-            var newDeferredSetter = new DeferredSetter<T>();
-
-            _deferredSetters.AddValue(property, newDeferredSetter);
-
-            return newDeferredSetter;
-        }
-
-        public DeferredSetter<object> GetNonDirectDeferredSetter(AvaloniaProperty property)
-        {
-            return GetDeferredSetter<object>(property);
-        }
-
-        public DeferredSetter<T> GetDirectDeferredSetter<T>(AvaloniaProperty<T> property)
-        {
-            return GetDeferredSetter<T>(property);
+            var binding = priorityValue.AddBinding(source, priority);
+            _values.SetValue(property, priorityValue);
+            binding.Start();
+            return binding;
         }
     }
 }

+ 1 - 1
src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs

@@ -238,7 +238,7 @@ namespace Avalonia.Collections
                 }
                 else
                 {
-                    return seq.ThenByDescending(o => GetValue(o), InternalComparer);
+                    return seq.ThenBy(o => GetValue(o), InternalComparer);
                 }
             }
 

+ 48 - 106
src/Avalonia.Controls.DataGrid/DataGrid.cs

@@ -65,7 +65,7 @@ namespace Avalonia.Controls
         private const double DATAGRID_minimumColumnHeaderHeight = 4;
         internal const double DATAGRID_maximumStarColumnWidth = 10000;
         internal const double DATAGRID_minimumStarColumnWidth = 0.001;
-        private const double DATAGRID_mouseWheelDelta = 48.0;
+        private const double DATAGRID_mouseWheelDelta = 72.0;
         private const double DATAGRID_maxHeadersThickness = 32768;
 
         private const double DATAGRID_defaultRowHeight = 22;
@@ -149,6 +149,9 @@ namespace Avalonia.Controls
 
         private IEnumerable _items;
 
+        public event EventHandler<ScrollEventArgs> HorizontalScroll;
+        public event EventHandler<ScrollEventArgs> VerticalScroll;
+
         /// <summary>
         /// Identifies the CanUserReorderColumns dependency property.
         /// </summary>
@@ -202,20 +205,12 @@ namespace Avalonia.Controls
             AvaloniaProperty.Register<DataGrid, double>(
                 nameof(ColumnHeaderHeight),
                 defaultValue: double.NaN,
-                validate: ValidateColumnHeaderHeight);
+                validate: IsValidColumnHeaderHeight);
 
-        private static double ValidateColumnHeaderHeight(DataGrid grid, double value)
+        private static bool IsValidColumnHeaderHeight(double value)
         {
-            if (value < DATAGRID_minimumColumnHeaderHeight)
-            {
-                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(ColumnHeaderHeight), DATAGRID_minimumColumnHeaderHeight);
-            }
-            if (value > DATAGRID_maxHeadersThickness)
-            {
-                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(ColumnHeaderHeight), DATAGRID_maxHeadersThickness);
-            }
-
-            return value;
+            return double.IsNaN(value) ||
+                (value >= DATAGRID_minimumColumnHeaderHeight && value <= DATAGRID_maxHeadersThickness);
         }
 
         /// <summary>
@@ -273,15 +268,7 @@ namespace Avalonia.Controls
             set { SetValue(FrozenColumnCountProperty, value); }
         }
 
-        private static int ValidateFrozenColumnCount(DataGrid grid, int value)
-        {
-            if (value < 0)
-            {
-                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(FrozenColumnCount), 0);
-            }
-
-            return value;
-        }
+        private static bool ValidateFrozenColumnCount(int value) => value >= 0;
 
         public static readonly StyledProperty<DataGridGridLinesVisibility> GridLinesVisibilityProperty =
             AvaloniaProperty.Register<DataGrid, DataGridGridLinesVisibility>(nameof(GridLinesVisibility));
@@ -389,36 +376,22 @@ namespace Avalonia.Controls
         public bool IsValid
         {
             get { return _isValid; }
-            internal set { SetAndRaise(IsValidProperty, ref _isValid, value); }
+            internal set 
+            { 
+                SetAndRaise(IsValidProperty, ref _isValid, value);
+                PseudoClasses.Set(":invalid", !value);
+            }
         }
 
         public static readonly StyledProperty<double> MaxColumnWidthProperty =
             AvaloniaProperty.Register<DataGrid, double>(
                 nameof(MaxColumnWidth),
                 defaultValue: DATAGRID_defaultMaxColumnWidth,
-                validate: ValidateMaxColumnWidth);
+                validate: IsValidColumnWidth);
 
-        private static double ValidateMaxColumnWidth(DataGrid grid, double value)
+        private static bool IsValidColumnWidth(double value)
         {
-            if (double.IsNaN(value))
-            {
-                throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(MaxColumnWidth));
-            }
-            if (value < 0)
-            {
-                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MaxColumnWidth), 0);
-            }
-            if (grid.MinColumnWidth > value)
-            {
-                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MaxColumnWidth), nameof(MinColumnWidth));
-            }
-
-            if (value < 0)
-            {
-                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(FrozenColumnCount), 0);
-            }
-
-            return value;
+            return !double.IsNaN(value) && value > 0;
         }
 
         /// <summary>
@@ -434,28 +407,11 @@ namespace Avalonia.Controls
             AvaloniaProperty.Register<DataGrid, double>(
                 nameof(MinColumnWidth),
                 defaultValue: DATAGRID_defaultMinColumnWidth,
-                validate: ValidateMinColumnWidth);
+                validate: IsValidMinColumnWidth);
 
-        private static double ValidateMinColumnWidth(DataGrid grid, double value)
+        private static bool IsValidMinColumnWidth(double value)
         {
-            if (double.IsNaN(value))
-            {
-                throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(MinColumnWidth));
-            }
-            if (value < 0)
-            {
-                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MinColumnWidth), 0);
-            }
-            if (double.IsPositiveInfinity(value))
-            {
-                throw DataGridError.DataGrid.ValueCannotBeSetToInfinity(nameof(MinColumnWidth));
-            }
-            if (grid.MaxColumnWidth < value)
-            {
-                throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(MinColumnWidth), nameof(MaxColumnWidth));
-            }
-
-            return value;
+            return !double.IsNaN(value) && !double.IsPositiveInfinity(value) && value >= 0;
         }
 
         /// <summary>
@@ -483,19 +439,12 @@ namespace Avalonia.Controls
             AvaloniaProperty.Register<DataGrid, double>(
                 nameof(RowHeight),
                 defaultValue: double.NaN,
-                validate: ValidateRowHeight);
-        private static double ValidateRowHeight(DataGrid grid, double value)
+                validate: IsValidRowHeight);
+        private static bool IsValidRowHeight(double value)
         {
-            if (value < DataGridRow.DATAGRIDROW_minimumHeight)
-            {
-                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(RowHeight), 0);
-            }
-            if (value > DataGridRow.DATAGRIDROW_maximumHeight)
-            {
-                throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(RowHeight), DataGridRow.DATAGRIDROW_maximumHeight);
-            }
-
-            return value;
+            return double.IsNaN(value) ||
+                (value >= DataGridRow.DATAGRIDROW_minimumHeight &&
+                 value <= DataGridRow.DATAGRIDROW_maximumHeight);
         }
 
         /// <summary>
@@ -511,19 +460,12 @@ namespace Avalonia.Controls
             AvaloniaProperty.Register<DataGrid, double>(
                 nameof(RowHeaderWidth),
                 defaultValue: double.NaN,
-                validate: ValidateRowHeaderWidth);
-        private static double ValidateRowHeaderWidth(DataGrid grid, double value)
+                validate: IsValidRowHeaderWidth);
+        private static bool IsValidRowHeaderWidth(double value)
         {
-            if (value < DATAGRID_minimumRowHeaderWidth)
-            {
-                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(RowHeaderWidth), DATAGRID_minimumRowHeaderWidth);
-            }
-            if (value > DATAGRID_maxHeadersThickness)
-            {
-                throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(RowHeaderWidth), DATAGRID_maxHeadersThickness);
-            }
-
-            return value;
+            return double.IsNaN(value) ||
+                (value >= DATAGRID_minimumRowHeaderWidth &&
+                 value <= DATAGRID_maxHeadersThickness);
         }
 
         /// <summary>
@@ -721,8 +663,6 @@ namespace Avalonia.Controls
                 HorizontalScrollBarVisibilityProperty,
                 VerticalScrollBarVisibilityProperty);
 
-            PseudoClass<DataGrid, bool>(IsValidProperty, x => !x, ":invalid");
-
             ItemsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnItemsPropertyChanged(e));
             CanUserResizeColumnsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnCanUserResizeColumnsChanged(e));
             ColumnWidthProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnColumnWidthChanged(e));
@@ -2537,25 +2477,25 @@ namespace Avalonia.Controls
 
         internal bool ProcessDownKey(KeyEventArgs e)
         {
-            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift);
             return ProcessDownKeyInternal(shift, ctrl);
         }
 
         internal bool ProcessEndKey(KeyEventArgs e)
         {
-            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift);
             return ProcessEndKey(shift, ctrl);
         }
 
         internal bool ProcessEnterKey(KeyEventArgs e)
         {
-            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift);
             return ProcessEnterKey(shift, ctrl);
         }
 
         internal bool ProcessHomeKey(KeyEventArgs e)
         {
-            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift);
             return ProcessHomeKey(shift, ctrl);
         }
 
@@ -2595,25 +2535,25 @@ namespace Avalonia.Controls
 
         internal bool ProcessLeftKey(KeyEventArgs e)
         {
-            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift);
             return ProcessLeftKey(shift, ctrl);
         }
 
         internal bool ProcessNextKey(KeyEventArgs e)
         {
-            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift);
             return ProcessNextKey(shift, ctrl);
         }
 
         internal bool ProcessPriorKey(KeyEventArgs e)
         {
-            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift);
             return ProcessPriorKey(shift, ctrl);
         }
 
         internal bool ProcessRightKey(KeyEventArgs e)
         {
-            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift);
             return ProcessRightKey(shift, ctrl);
         }
 
@@ -2731,7 +2671,7 @@ namespace Avalonia.Controls
 
         internal bool ProcessUpKey(KeyEventArgs e)
         {
-            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift);
             return ProcessUpKey(shift, ctrl);
         }
 
@@ -2999,7 +2939,7 @@ namespace Avalonia.Controls
         //TODO: Ensure left button is checked for
         internal bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit)
         {
-            KeyboardHelper.GetMetaKeyState(pointerPressedEventArgs.InputModifiers, out bool ctrl, out bool shift);
+            KeyboardHelper.GetMetaKeyState(pointerPressedEventArgs.KeyModifiers, out bool ctrl, out bool shift);
             return UpdateStateOnMouseLeftButtonDown(pointerPressedEventArgs, columnIndex, slot, allowEdit, shift, ctrl);
         }
 
@@ -4288,6 +4228,7 @@ namespace Avalonia.Controls
         private void HorizontalScrollBar_Scroll(object sender, ScrollEventArgs e)
         {
             ProcessHorizontalScroll(e.ScrollEventType);
+            HorizontalScroll?.Invoke(sender, e);
         }
 
         private bool IsColumnOutOfBounds(int columnIndex)
@@ -4441,7 +4382,7 @@ namespace Avalonia.Controls
 
         private bool ProcessAKey(KeyEventArgs e)
         {
-            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift, out bool alt);
+            KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift, out bool alt);
 
             if (ctrl && !shift && !alt && SelectionMode == DataGridSelectionMode.Extended)
             {
@@ -4507,10 +4448,10 @@ namespace Avalonia.Controls
                     return ProcessAKey(e);
 
                 case Key.C:
-                    return ProcessCopyKey(e.Modifiers);
+                    return ProcessCopyKey(e.KeyModifiers);
 
                 case Key.Insert:
-                    return ProcessCopyKey(e.Modifiers);
+                    return ProcessCopyKey(e.KeyModifiers);
             }
             if (focusDataGrid)
             {
@@ -4709,7 +4650,7 @@ namespace Avalonia.Controls
 
         private bool ProcessF2Key(KeyEventArgs e)
         {
-            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift);
 
             if (!shift && !ctrl &&
                 _editingColumnIndex == -1 && CurrentColumnIndex != -1 && GetRowSelection(CurrentSlot) &&
@@ -5066,7 +5007,7 @@ namespace Avalonia.Controls
 
         private bool ProcessTabKey(KeyEventArgs e)
         {
-            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift);
             return ProcessTabKey(e, shift, ctrl);
         }
 
@@ -5620,6 +5561,7 @@ namespace Avalonia.Controls
         private void VerticalScrollBar_Scroll(object sender, ScrollEventArgs e)
         {
             ProcessVerticalScroll(e.ScrollEventType);
+            VerticalScroll?.Invoke(sender, e);
         }
 
         //TODO: Ensure left button is checked for
@@ -5852,7 +5794,7 @@ namespace Avalonia.Controls
         /// to the Clipboard as text.
         /// </summary>
         /// <returns>Whether or not the DataGrid handled the key press.</returns>
-        private bool ProcessCopyKey(InputModifiers modifiers)
+        private bool ProcessCopyKey(KeyModifiers modifiers)
         {
             KeyboardHelper.GetMetaKeyState(modifiers, out bool ctrl, out bool shift, out bool alt);
 

+ 55 - 51
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@@ -67,7 +67,7 @@ namespace Avalonia.Controls
 
         static DataGridColumnHeader()
         {
-            AreSeparatorsVisibleProperty.Changed.AddClassHandler<DataGridColumnHeader>((x,e) => x.OnAreSeparatorsVisibleChanged(e));
+            AreSeparatorsVisibleProperty.Changed.AddClassHandler<DataGridColumnHeader>((x, e) => x.OnAreSeparatorsVisibleChanged(e));
         }
 
         /// <summary>
@@ -103,7 +103,7 @@ namespace Avalonia.Controls
         {
             get;
             set;
-        } 
+        }
         internal DataGrid OwningGrid => OwningColumn?.OwningGrid;
 
         internal int ColumnIndex
@@ -116,19 +116,19 @@ namespace Avalonia.Controls
                 }
                 return OwningColumn.Index;
             }
-        } 
+        }
 
         internal ListSortDirection? CurrentSortingState
         {
             get;
             private set;
-        } 
+        }
 
         private bool IsMouseOver
         {
             get;
             set;
-        } 
+        }
 
         private bool IsPressed
         {
@@ -158,14 +158,14 @@ namespace Avalonia.Controls
                 && OwningGrid.DataConnection.AllowSort)
             {
                 var sort = OwningColumn.GetSortDescription();
-                if(sort != null)
+                if (sort != null)
                 {
                     CurrentSortingState = sort.Descending ? ListSortDirection.Descending : ListSortDirection.Ascending;
                 }
             }
-            PseudoClasses.Set(":sortascending", 
+            PseudoClasses.Set(":sortascending",
                 CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Ascending);
-            PseudoClasses.Set(":sortdescending", 
+            PseudoClasses.Set(":sortdescending",
                 CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Descending);
         }
 
@@ -190,28 +190,28 @@ namespace Avalonia.Controls
             }
         }
 
-        internal void OnMouseLeftButtonUp_Click(InputModifiers inputModifiers, ref bool handled)
+        internal void OnMouseLeftButtonUp_Click(KeyModifiers keyModifiers, ref bool handled)
         {
             // completed a click without dragging, so we're sorting
-            InvokeProcessSort(inputModifiers);
+            InvokeProcessSort(keyModifiers);
             handled = true;
-        } 
+        }
 
-        internal void InvokeProcessSort(InputModifiers inputModifiers)
+        internal void InvokeProcessSort(KeyModifiers keyModifiers)
         {
             Debug.Assert(OwningGrid != null);
-            if (OwningGrid.WaitForLostFocus(() => InvokeProcessSort(inputModifiers)))
+            if (OwningGrid.WaitForLostFocus(() => InvokeProcessSort(keyModifiers)))
             {
                 return;
             }
             if (OwningGrid.CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true))
             {
-                Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(inputModifiers));
+                Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(keyModifiers));
             }
-        } 
+        }
 
         //TODO GroupSorting
-        internal void ProcessSort(InputModifiers inputModifiers)
+        internal void ProcessSort(KeyModifiers keyModifiers)
         {
             // if we can sort:
             //  - DataConnection.AllowSort is true, and
@@ -233,7 +233,7 @@ namespace Avalonia.Controls
 
                 DataGridSortDescription newSort;
 
-                KeyboardHelper.GetMetaKeyState(inputModifiers, out bool ctrl, out bool shift);
+                KeyboardHelper.GetMetaKeyState(keyModifiers, out bool ctrl, out bool shift);
 
                 DataGridSortDescription sort = OwningColumn.GetSortDescription();
                 IDataGridCollectionView collectionView = owningGrid.DataConnection.CollectionView;
@@ -246,45 +246,49 @@ namespace Avalonia.Controls
                         owningGrid.DataConnection.SortDescriptions.Clear();
                     }
 
-                    if (sort != null)
+                    // if ctrl is held down, we only clear the sort directions
+                    if (!ctrl)
                     {
-                        newSort = sort.SwitchSortDirection();
-
-                        // changing direction should not affect sort order, so we replace this column's
-                        // sort description instead of just adding it to the end of the collection
-                        int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort);
-                        if (oldIndex >= 0)
+                        if (sort != null)
                         {
-                            owningGrid.DataConnection.SortDescriptions.Remove(sort);
-                            owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort);
+                            newSort = sort.SwitchSortDirection();
+
+                            // changing direction should not affect sort order, so we replace this column's
+                            // sort description instead of just adding it to the end of the collection
+                            int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort);
+                            if (oldIndex >= 0)
+                            {
+                                owningGrid.DataConnection.SortDescriptions.Remove(sort);
+                                owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort);
+                            }
+                            else
+                            {
+                                owningGrid.DataConnection.SortDescriptions.Add(newSort);
+                            }
                         }
                         else
                         {
+                            string propertyName = OwningColumn.GetSortPropertyName();
+                            // no-opt if we couldn't find a property to sort on
+                            if (string.IsNullOrEmpty(propertyName))
+                            {
+                                return;
+                            }
+
+                            newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture);
                             owningGrid.DataConnection.SortDescriptions.Add(newSort);
                         }
                     }
-                    else
-                    {
-                        string propertyName = OwningColumn.GetSortPropertyName();
-                        // no-opt if we couldn't find a property to sort on
-                        if (string.IsNullOrEmpty(propertyName))
-                        {
-                            return;
-                        }
-
-                        newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture);
-                        owningGrid.DataConnection.SortDescriptions.Add(newSort);
-                    }
                 }
             }
-        } 
+        }
 
         private bool CanReorderColumn(DataGridColumn column)
         {
-            return OwningGrid.CanUserReorderColumns 
+            return OwningGrid.CanUserReorderColumns
                 && !(column is DataGridFillerColumn)
                 && (column.CanUserReorderInternal.HasValue && column.CanUserReorderInternal.Value || !column.CanUserReorderInternal.HasValue);
-        } 
+        }
 
         /// <summary>
         /// Determines whether a column can be resized by dragging the border of its header.  If star sizing
@@ -302,7 +306,7 @@ namespace Avalonia.Controls
                 return false;
             }
             return column.ActualCanUserResize;
-        }  
+        }
 
         private static bool TrySetResizeColumn(DataGridColumn column)
         {
@@ -316,7 +320,7 @@ namespace Avalonia.Controls
                 return true;
             }
             return false;
-        } 
+        }
 
         //TODO DragDrop
 
@@ -326,7 +330,7 @@ namespace Avalonia.Controls
 
             if (OwningGrid != null && OwningGrid.ColumnHeaders != null)
             {
-                args.Device.Capture(this);
+                args.Pointer.Capture(this);
 
                 _dragMode = DragMode.MouseDown;
                 _frozenColumnsWidth = OwningGrid.ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth();
@@ -371,7 +375,7 @@ namespace Avalonia.Controls
             {
                 if (_dragMode == DragMode.MouseDown)
                 {
-                   OnMouseLeftButtonUp_Click(args.InputModifiers, ref handled);
+                    OnMouseLeftButtonUp_Click(args.KeyModifiers, ref handled);
                 }
                 else if (_dragMode == DragMode.Reorder)
                 {
@@ -391,7 +395,7 @@ namespace Avalonia.Controls
                 SetDragCursor(mousePosition);
 
                 // Variables that track drag mode states get reset in DataGridColumnHeader_LostMouseCapture
-                args.Device.Capture(null);
+                args.Pointer.Capture(null);
                 OnLostMouseCapture();
                 _dragMode = DragMode.None;
                 handled = true;
@@ -449,7 +453,7 @@ namespace Avalonia.Controls
 
             OnMouseLeave();
             ApplyState();
-        } 
+        }
 
         private void DataGridColumnHeader_PointerPressed(object sender, PointerPressedEventArgs e)
         {
@@ -577,7 +581,7 @@ namespace Avalonia.Controls
             {
                 return OwningGrid.Columns.Count - 1;
             }
-        } 
+        }
 
         /// <summary>
         /// Returns true if the mouse is 
@@ -723,7 +727,7 @@ namespace Avalonia.Controls
                     Point targetPosition = new Point(0, 0);
                     if (targetColumn == null || targetColumn == OwningGrid.ColumnsInternal.FillerColumn || targetColumn.IsFrozen != OwningColumn.IsFrozen)
                     {
-                        targetColumn = 
+                        targetColumn =
                             OwningGrid.ColumnsInternal.GetLastColumn(
                                 isVisible: true,
                                 isFrozen: OwningColumn.IsFrozen,
@@ -741,7 +745,7 @@ namespace Avalonia.Controls
 
                 handled = true;
             }
-        } 
+        }
 
         private void OnMouseMove_Resize(ref bool handled, Point mousePositionHeaders)
         {
@@ -764,7 +768,7 @@ namespace Avalonia.Controls
 
                 handled = true;
             }
-        } 
+        }
 
         private void SetDragCursor(Point mousePosition)
         {

+ 3 - 17
src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs

@@ -68,25 +68,11 @@ namespace Avalonia.Controls
             AvaloniaProperty.Register<DataGridRowGroupHeader, double>(
                 nameof(SublevelIndent),
                 defaultValue: DataGrid.DATAGRID_defaultRowGroupSublevelIndent,
-                validate: ValidateSublevelIndent);
+                validate: IsValidSublevelIndent);
 
-        private static double ValidateSublevelIndent(DataGridRowGroupHeader header, double value)
+        private static bool IsValidSublevelIndent(double value)
         {
-            // We don't need to revert to the old value if our input is bad because we never read this property value
-            if (double.IsNaN(value))
-            {
-                throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(SublevelIndent));
-            }
-            else if (double.IsInfinity(value))
-            {
-                throw DataGridError.DataGrid.ValueCannotBeSetToInfinity(nameof(SublevelIndent));
-            }
-            else if (value < 0)
-            {
-                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(SublevelIndent), 0);
-            }
-
-            return value;
+            return !double.IsNaN(value) && !double.IsInfinity(value) && value >= 0;
         }
 
         /// <summary>

+ 1 - 1
src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs

@@ -14,7 +14,7 @@ namespace Avalonia.Controls.Primitives
     /// </summary>
     public class DataGridFrozenGrid : Grid
     {
-        public static readonly AvaloniaProperty<bool> IsFrozenProperty =
+        public static readonly StyledProperty<bool> IsFrozenProperty =
             AvaloniaProperty.RegisterAttached<DataGridFrozenGrid, Control, bool>("IsFrozen");
 
         /// <summary>

+ 1 - 1
src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs

@@ -5,7 +5,7 @@ using System.Reflection;
 using System.Runtime.CompilerServices;
 using Avalonia.Metadata;
 
-[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests")]
+[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid.UnitTests")]
 [assembly: InternalsVisibleTo("Avalonia.DesignerSupport")]
 
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")]

+ 18 - 15
src/Avalonia.Controls.DataGrid/Themes/Default.xaml

@@ -45,7 +45,7 @@
 
             <Path Name="SortIcon"
                   Grid.Column="1"
-                  Fill="#FF444444"
+                  Fill="{TemplateBinding Foreground}"
                   HorizontalAlignment="Left"
                   VerticalAlignment="Center"
                   Stretch="Uniform"
@@ -113,7 +113,7 @@
 
   <Style Selector="DataGridRow /template/ Rectangle#BackgroundRectangle">
     <Setter Property="IsVisible" Value="False"/>
-    <Setter Property="Fill" Value="#FFBADDE9" />
+    <Setter Property="Fill" Value="{DynamicResource HighlightBrush}" />
   </Style>
 
   <Style Selector="DataGridRow:pointerover /template/ Rectangle#BackgroundRectangle">
@@ -126,6 +126,10 @@
     <Setter Property="Opacity" Value="1"/>
   </Style>
 
+  <Style Selector="DataGridRow:selected">
+    <Setter Property="Foreground" Value="{DynamicResource HighlightForegroundBrush}" />
+  </Style>
+
   <Style Selector="DataGridRowHeader">
     <Setter Property="Template">
       <ControlTemplate>
@@ -139,7 +143,7 @@
   </Style>
 
   <Style Selector="DataGridRowGroupHeader">
-    <Setter Property="Background" Value="#FFE4E8EA" />
+    <Setter Property="Background" Value="{DynamicResource ThemeControlMidHighBrush}" />
     <Setter Property="Height" Value="20"/>
     <Setter Property="Template">
       <ControlTemplate>
@@ -148,7 +152,6 @@
                                  ColumnDefinitions="Auto,Auto,Auto,Auto"
                                  RowDefinitions="Auto,*,Auto">
 
-          <Rectangle Grid.Column="1" Grid.ColumnSpan="5" Fill="#FFFFFFFF" Height="1"/>
           <Rectangle Grid.Column="1" Grid.Row="1" Name="IndentSpacer" />
           <ToggleButton Grid.Column="2" Grid.Row="1" Name="ExpanderButton" Margin="2,0,0,0"/>
 
@@ -169,7 +172,7 @@
     <Setter Property="Template">
       <ControlTemplate>
         <Border Grid.Column="0" Width="20" Height="20" Background="Transparent" HorizontalAlignment="Center" VerticalAlignment="Center">
-          <Path Fill="Black"
+          <Path Fill="{TemplateBinding Foreground}"
                 HorizontalAlignment="Center"
                 VerticalAlignment="Center"
                 Data="M 0 2 L 4 6 L 0 10 Z" />
@@ -199,30 +202,30 @@
     <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}" />
     <Setter Property="DropLocationIndicatorTemplate">
       <Template>
-        <Rectangle Fill="#FF3F4346" Width="2"/>
+        <Rectangle Fill="{DynamicResource ThemeBorderHighColor}" Width="2"/>
       </Template>
     </Setter>
     <Setter Property="Template">
       <ControlTemplate>
-        <Border BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}">
-          <Grid
-          RowDefinitions="Auto,*,Auto,Auto"
-          ColumnDefinitions="Auto,*,Auto">
+        <Border Background="{TemplateBinding Background}"
+                BorderThickness="{TemplateBinding BorderThickness}"
+                BorderBrush="{TemplateBinding BorderBrush}">
+          <Grid RowDefinitions="Auto,*,Auto,Auto" ColumnDefinitions="Auto,*,Auto">
 
             <DataGridColumnHeader Name="PART_TopLeftCornerHeader" Width="22" />
             <DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter" Grid.Column="1"/>
             <DataGridColumnHeader Name="PART_TopRightCornerHeader" Grid.Column="2"/>
-            <Rectangle Name="PART_ColumnHeadersAndRowsSeparator" Grid.ColumnSpan="3" VerticalAlignment="Bottom" StrokeThickness="1" Height="1" Fill="#FFC9CACA"/>
+            <Rectangle Name="PART_ColumnHeadersAndRowsSeparator" Grid.ColumnSpan="3" VerticalAlignment="Bottom" StrokeThickness="1" Height="1" Fill="{DynamicResource ThemeControlMidHighBrush}"/>
 
             <DataGridRowsPresenter Name="PART_RowsPresenter" Grid.ColumnSpan="2" Grid.Row="1" />
-            <Rectangle Name="BottomRightCorner" Fill="#FFE9EEF4" Grid.Column="2" Grid.Row="2" />
-            <Rectangle Name="BottomLeftCorner" Fill="#FFE9EEF4" Grid.Row="2" Grid.ColumnSpan="2" />
-            <ScrollBar Name="PART_VerticalScrollbar" Orientation="Vertical" Grid.Column="2" Grid.Row="1" Width="{DynamicResource ScrollBarThickness}" Margin="0,-1,-1,-1"/>
+            <Rectangle Name="BottomRightCorner" Fill="{DynamicResource ThemeControlMidHighBrush}" Grid.Column="2" Grid.Row="2" />
+            <Rectangle Name="BottomLeftCorner" Fill="{DynamicResource ThemeControlMidHighBrush}" Grid.Row="2" Grid.ColumnSpan="2" />
+            <ScrollBar Name="PART_VerticalScrollbar" Orientation="Vertical" Grid.Column="2" Grid.Row="1" Width="{DynamicResource ScrollBarThickness}"/>
 
             <Grid Grid.Column="1" Grid.Row="2"
                   ColumnDefinitions="Auto,*">
               <Rectangle Name="PART_FrozenColumnScrollBarSpacer" />
-              <ScrollBar Name="PART_HorizontalScrollbar" Grid.Column="1" Orientation="Horizontal" Height="{DynamicResource ScrollBarThickness}" Margin="-1,0,-1,-1"/>
+              <ScrollBar Name="PART_HorizontalScrollbar" Grid.Column="1" Orientation="Horizontal" Height="{DynamicResource ScrollBarThickness}"/>
             </Grid>
           </Grid>
         </Border>

+ 8 - 7
src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs

@@ -9,16 +9,17 @@ namespace Avalonia.Controls.Utils
 {
     internal static class KeyboardHelper
     {
-        public static void GetMetaKeyState(InputModifiers modifiers, out bool ctrl, out bool shift)
+        public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrl, out bool shift)
         {
-            ctrl = (modifiers & InputModifiers.Control) == InputModifiers.Control;
-            shift = (modifiers & InputModifiers.Shift) == InputModifiers.Shift;
+            ctrl = (modifiers & KeyModifiers.Control) == KeyModifiers.Control;
+            shift = (modifiers & KeyModifiers.Shift) == KeyModifiers.Shift;
         }
-        public static void GetMetaKeyState(InputModifiers modifiers, out bool ctrl, out bool shift, out bool alt)
+
+        public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrl, out bool shift, out bool alt)
         {
-            ctrl = (modifiers & InputModifiers.Control) == InputModifiers.Control;
-            shift = (modifiers & InputModifiers.Shift) == InputModifiers.Shift;
-            alt = (modifiers & InputModifiers.Alt) == InputModifiers.Alt;
+            ctrl = (modifiers & KeyModifiers.Control) == KeyModifiers.Control;
+            shift = (modifiers & KeyModifiers.Shift) == KeyModifiers.Shift;
+            alt = (modifiers & KeyModifiers.Alt) == KeyModifiers.Alt;
         }
     }
 }

+ 36 - 3
src/Avalonia.Controls/Application.cs

@@ -32,7 +32,7 @@ namespace Avalonia
     /// method.
     /// - Tracks the lifetime of the application.
     /// </remarks>
-    public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode
+    public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceNode
     {
         /// <summary>
         /// The application-global data templates.
@@ -44,6 +44,7 @@ namespace Avalonia
         private readonly Styler _styler = new Styler();
         private Styles _styles;
         private IResourceDictionary _resources;
+        private bool _notifyingResourcesChanged;
 
         /// <summary>
         /// Defines the <see cref="DataContext"/> property.
@@ -160,7 +161,19 @@ namespace Avalonia
         /// <remarks>
         /// Global styles apply to all windows in the application.
         /// </remarks>
-        public Styles Styles => _styles ?? (_styles = new Styles());
+        public Styles Styles
+        {
+            get
+            {
+                if (_styles == null)
+                {
+                    _styles = new Styles(this);
+                    _styles.ResourcesChanged += ThisResourcesChanged;
+                }
+
+                return _styles;
+            }
+        }
 
         /// <inheritdoc/>
         bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
@@ -233,9 +246,29 @@ namespace Avalonia
             
         }
 
+        private void NotifyResourcesChanged(ResourcesChangedEventArgs e)
+        {
+            if (_notifyingResourcesChanged)
+            {
+                return;
+            }
+
+            try
+            {
+                _notifyingResourcesChanged = true;
+                (_resources as ISetResourceParent)?.ParentResourcesChanged(e);
+                (_styles as ISetResourceParent)?.ParentResourcesChanged(e);
+                ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
+            }
+            finally
+            {
+                _notifyingResourcesChanged = false;
+            }
+        }
+
         private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e)
         {
-            ResourcesChanged?.Invoke(this, e);
+            NotifyResourcesChanged(e);
         }
 
         private string _name;

+ 8 - 29
src/Avalonia.Controls/AutoCompleteBox.cs

@@ -378,7 +378,7 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<int> MinimumPrefixLengthProperty =
             AvaloniaProperty.Register<AutoCompleteBox, int>(
                 nameof(MinimumPrefixLength), 1,
-                validate: ValidateMinimumPrefixLength);
+                validate: IsValidMinimumPrefixLength);
 
         /// <summary>
         /// Identifies the
@@ -392,7 +392,7 @@ namespace Avalonia.Controls
             AvaloniaProperty.Register<AutoCompleteBox, TimeSpan>(
                 nameof(MinimumPopulateDelay),
                 TimeSpan.Zero,
-                validate: ValidateMinimumPopulateDelay);
+                validate: IsValidMinimumPopulateDelay);
 
         /// <summary>
         /// Identifies the
@@ -406,7 +406,7 @@ namespace Avalonia.Controls
             AvaloniaProperty.Register<AutoCompleteBox, double>(
                 nameof(MaxDropDownHeight),
                 double.PositiveInfinity,
-                validate: ValidateMaxDropDownHeight);
+                validate: IsValidMaxDropDownHeight);
 
         /// <summary>
         /// Identifies the
@@ -495,7 +495,7 @@ namespace Avalonia.Controls
             AvaloniaProperty.Register<AutoCompleteBox, AutoCompleteFilterMode>(
                 nameof(FilterMode),
                 defaultValue: AutoCompleteFilterMode.StartsWith,
-                validate: ValidateFilterMode);
+                validate: IsValidFilterMode);
 
         /// <summary>
         /// Identifies the
@@ -546,26 +546,11 @@ namespace Avalonia.Controls
                 o => o.AsyncPopulator,
                 (o, v) => o.AsyncPopulator = v);
 
-        private static int ValidateMinimumPrefixLength(AutoCompleteBox control, int value)
-        {
-            Contract.Requires<ArgumentOutOfRangeException>(value >= -1);
-
-            return value;
-        }
-
-        private static TimeSpan ValidateMinimumPopulateDelay(AutoCompleteBox control, TimeSpan value)
-        {
-            Contract.Requires<ArgumentOutOfRangeException>(value.TotalMilliseconds >= 0.0);
-
-            return value;
-        }
+        private static bool IsValidMinimumPrefixLength(int value) => value >= -1;
 
-        private static double ValidateMaxDropDownHeight(AutoCompleteBox control, double value)
-        {
-            Contract.Requires<ArgumentOutOfRangeException>(value >= 0.0);
+        private static bool IsValidMinimumPopulateDelay(TimeSpan value) => value.TotalMilliseconds >= 0.0;
 
-            return value;
-        }
+        private static bool IsValidMaxDropDownHeight(double value) => value >= 0.0;
 
         private static bool IsValidFilterMode(AutoCompleteFilterMode mode)
         {
@@ -590,12 +575,6 @@ namespace Avalonia.Controls
                     return false;
             }
         }
-        private static AutoCompleteFilterMode ValidateFilterMode(AutoCompleteBox control, AutoCompleteFilterMode value)
-        {
-            Contract.Requires<ArgumentException>(IsValidFilterMode(value));
-
-            return value;
-        }
 
         /// <summary>
         /// Handle the change of the IsEnabled property.
@@ -704,7 +683,7 @@ namespace Avalonia.Controls
                 added.Add(e.NewValue);
             }
 
-            OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, added, removed));
+            OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, removed, added));
         }
 
         /// <summary>

+ 28 - 5
src/Avalonia.Controls/Button.cs

@@ -91,7 +91,11 @@ namespace Avalonia.Controls
             CommandProperty.Changed.Subscribe(CommandChanged);
             IsDefaultProperty.Changed.Subscribe(IsDefaultChanged);
             IsCancelProperty.Changed.Subscribe(IsCancelChanged);
-            PseudoClass<Button>(IsPressedProperty, ":pressed");
+        }
+
+        public Button()
+        {
+            UpdatePseudoClasses(IsPressed);
         }
 
         /// <summary>
@@ -306,18 +310,32 @@ namespace Avalonia.Controls
                 }
             }
         }
-
+        
         protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
         {
             IsPressed = false;
         }
 
-        protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
+        protected override void OnPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
         {
-            base.UpdateDataValidation(property, status);
+            base.OnPropertyChanged(property, oldValue, newValue, priority);
+
+            if (property == IsPressedProperty)
+            {
+                UpdatePseudoClasses(newValue.GetValueOrDefault<bool>());
+            }
+        }
+
+        protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
+        {
+            base.UpdateDataValidation(property, value);
             if (property == CommandProperty)
             {
-                if (status?.ErrorType == BindingErrorType.Error)
+                if (value.Type == BindingValueType.BindingError)
                 {
                     if (_commandCanExecute)
                     {
@@ -474,5 +492,10 @@ namespace Avalonia.Controls
                 OnClick();
             }
         }
+
+        private void UpdatePseudoClasses(bool isPressed)
+        {
+            PseudoClasses.Set(":pressed", isPressed);
+        }
     }
 }

+ 26 - 2
src/Avalonia.Controls/ButtonSpinner.cs

@@ -1,5 +1,6 @@
 using System;
 using Avalonia.Controls.Primitives;
+using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 
@@ -34,6 +35,11 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<Location> ButtonSpinnerLocationProperty =
             AvaloniaProperty.Register<ButtonSpinner, Location>(nameof(ButtonSpinnerLocation), Location.Right);
 
+        public ButtonSpinner()
+        {
+            UpdatePseudoClasses(ButtonSpinnerLocation);
+        }
+
         private Button _decreaseButton;
         /// <summary>
         /// Gets or sets the DecreaseButton template part.
@@ -85,8 +91,6 @@ namespace Avalonia.Controls
         static ButtonSpinner()
         {
             AllowSpinProperty.Changed.Subscribe(AllowSpinChanged);
-            PseudoClass<ButtonSpinner, Location>(ButtonSpinnerLocationProperty, location => location == Location.Left, ":left");
-            PseudoClass<ButtonSpinner, Location>(ButtonSpinnerLocationProperty, location => location == Location.Right, ":right");
         }
 
         /// <summary>
@@ -201,6 +205,20 @@ namespace Avalonia.Controls
             }
         }
 
+        protected override void OnPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
+        {
+            base.OnPropertyChanged(property, oldValue, newValue, priority);
+
+            if (property == ButtonSpinnerLocationProperty)
+            {
+                UpdatePseudoClasses(newValue.GetValueOrDefault<Location>());
+            }
+        }
+
         protected override void OnValidSpinDirectionChanged(ValidSpinDirections oldValue, ValidSpinDirections newValue)
         {
             SetButtonUsage();
@@ -259,5 +277,11 @@ namespace Avalonia.Controls
                 OnSpin(new SpinEventArgs(SpinEvent, direction));
             }
         }
+
+        private void UpdatePseudoClasses(Location location)
+        {
+            PseudoClasses.Set(":left", location == Location.Left);
+            PseudoClasses.Set(":right", location == Location.Right);
+        }
     }
 }

+ 4 - 14
src/Avalonia.Controls/Calendar/Calendar.cs

@@ -352,7 +352,8 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<CalendarMode> DisplayModeProperty =
             AvaloniaProperty.Register<Calendar, CalendarMode>(
                 nameof(DisplayMode),
-                validate: ValidateDisplayMode);
+                validate: IsValidDisplayMode);
+
         /// <summary>
         /// Gets or sets a value indicating whether the calendar is displayed in
         /// months, years, or decades.
@@ -417,17 +418,6 @@ namespace Avalonia.Controls
             }
             OnDisplayModeChanged(new CalendarModeChangedEventArgs((CalendarMode)e.OldValue, mode));
         }
-        private static CalendarMode ValidateDisplayMode(Calendar o, CalendarMode mode)
-        {
-            if(IsValidDisplayMode(mode))
-            {
-                return mode;
-            }
-            else
-            {
-                throw new ArgumentOutOfRangeException(nameof(mode), "Invalid DisplayMode");
-            }
-        }
         private static bool IsValidDisplayMode(CalendarMode mode)
         {
             return mode == CalendarMode.Month
@@ -1585,7 +1575,7 @@ namespace Avalonia.Controls
             base.OnPointerWheelChanged(e);
             if (!e.Handled)
             {
-                CalendarExtensions.GetMetaKeyState(e.InputModifiers, out bool ctrl, out bool shift);
+                CalendarExtensions.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift);
 
                 if (!ctrl)
                 {
@@ -1641,7 +1631,7 @@ namespace Avalonia.Controls
             // Some keys (e.g. Left/Right) need to be translated in RightToLeft mode
             Key invariantKey = e.Key;  //InteractionHelper.GetLogicalKey(FlowDirection, e.Key);
 
-            CalendarExtensions.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            CalendarExtensions.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift);
 
             switch (invariantKey)
             {

+ 3 - 3
src/Avalonia.Controls/Calendar/CalendarExtensions.cs

@@ -9,10 +9,10 @@ namespace Avalonia.Controls.Primitives
 {
     internal static class CalendarExtensions
     {
-        public static void GetMetaKeyState(InputModifiers modifiers, out bool ctrl, out bool shift)
+        public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrl, out bool shift)
         {
-            ctrl = (modifiers & InputModifiers.Control) == InputModifiers.Control;
-            shift = (modifiers & InputModifiers.Shift) == InputModifiers.Shift;
+            ctrl = (modifiers & KeyModifiers.Control) == KeyModifiers.Control;
+            shift = (modifiers & KeyModifiers.Shift) == KeyModifiers.Shift;
         }
     }
 }

+ 7 - 7
src/Avalonia.Controls/Calendar/CalendarItem.cs

@@ -943,8 +943,8 @@ namespace Avalonia.Controls.Primitives
             {
                 CalendarDayButton b = (CalendarDayButton)sender;
                 // The button is in Pressed state. Change the state to normal.
-                if (e.Device.Captured == b)
-                    e.Device.Capture(null);
+                if (e.Pointer.Captured == b)
+                    e.Pointer.Capture(null);
                 _lastCalendarDayButton = b;
             }
         }
@@ -958,7 +958,7 @@ namespace Avalonia.Controls.Primitives
                 }
 
                 bool ctrl, shift;
-                CalendarExtensions.GetMetaKeyState(e.InputModifiers, out ctrl, out shift);
+                CalendarExtensions.GetMetaKeyState(e.KeyModifiers, out ctrl, out shift);
                 CalendarDayButton b = sender as CalendarDayButton;
 
                 if (b != null)
@@ -1213,8 +1213,8 @@ namespace Avalonia.Controls.Primitives
             {
                 CalendarButton b = (CalendarButton)sender;
                 // The button is in Pressed state. Change the state to normal.
-                if (e.Device.Captured == b)
-                    e.Device.Capture(null);
+                if (e.Pointer.Captured == b)
+                    e.Pointer.Capture(null);
                 //b.ReleaseMouseCapture();
 
                 _lastCalendarButton = b;
@@ -1224,7 +1224,7 @@ namespace Avalonia.Controls.Primitives
         {
             if (_lastCalendarDayButton != null)
             {
-                e.Device.Capture(_lastCalendarDayButton);
+                e.Pointer.Capture(_lastCalendarDayButton);
             }
         }
 
@@ -1232,7 +1232,7 @@ namespace Avalonia.Controls.Primitives
         {
             if (_lastCalendarButton != null)
             {
-                e.Device.Capture(_lastCalendarButton);
+                e.Pointer.Capture(_lastCalendarButton);
             }
         }
         

+ 14 - 26
src/Avalonia.Controls/Calendar/DatePicker.cs

@@ -190,13 +190,13 @@ namespace Avalonia.Controls
             AvaloniaProperty.Register<DatePicker, DatePickerFormat>(
                 nameof(SelectedDateFormat),
                 defaultValue: DatePickerFormat.Short,
-                validate: ValidateSelectedDateFormat);
+                validate: IsValidSelectedDateFormat);
 
         public static readonly StyledProperty<string> CustomDateFormatStringProperty =
             AvaloniaProperty.Register<DatePicker, string>(
                 nameof(CustomDateFormatString),
                 defaultValue: "d",
-                validate: ValidateDateFormatString);
+                validate: IsValidDateFormatString);
 
         public static readonly DirectProperty<DatePicker, string> TextProperty =
             AvaloniaProperty.RegisterDirect<DatePicker, string>(
@@ -512,11 +512,17 @@ namespace Avalonia.Controls
             base.OnTemplateApplied(e);
         }
 
-        protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
+        protected override void OnPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
         {
+            base.OnPropertyChanged(property, oldValue, newValue, priority);
+
             if (property == SelectedDateProperty)
             {
-                DataValidationErrors.SetError(this, status.Error);
+                DataValidationErrors.SetError(this, newValue.Error);
             }
         }
 
@@ -782,7 +788,7 @@ namespace Avalonia.Controls
                     removedItems.Add(removedDate.Value);
                 }
 
-                handler(this, new SelectionChangedEventArgs(SelectingItemsControl.SelectionChangedEvent, addedItems, removedItems));
+                handler(this, new SelectionChangedEventArgs(SelectingItemsControl.SelectionChangedEvent, removedItems, addedItems));
             }
         }
         private void OnCalendarClosed(EventArgs e)
@@ -1002,7 +1008,7 @@ namespace Avalonia.Controls
                     }
                 case Key.Down:
                     { 
-                        if ((e.Modifiers & InputModifiers.Control) == InputModifiers.Control)
+                        if ((e.KeyModifiers & KeyModifiers.Control) == KeyModifiers.Control)
                         {
                             HandlePopUp();
                             return true;
@@ -1140,27 +1146,9 @@ namespace Avalonia.Controls
                 || value == DatePickerFormat.Short
                 || value == DatePickerFormat.Custom;
         }
-        private static DatePickerFormat ValidateSelectedDateFormat(DatePicker dp, DatePickerFormat format)
+        private static bool IsValidDateFormatString(string formatString)
         {
-            if(IsValidSelectedDateFormat(format))
-            {
-                return format;
-            }
-            else
-            {
-                throw new ArgumentOutOfRangeException(nameof(format), "DatePickerFormat value is not valid.");
-            }
-        }
-        private static string ValidateDateFormatString(DatePicker dp, string formatString)
-        {
-            if(string.IsNullOrWhiteSpace(formatString))
-            {
-                throw new ArgumentException("DateFormatString value is not valid.", nameof(formatString));
-            }
-            else
-            {
-                return formatString;
-            }
+            return !string.IsNullOrWhiteSpace(formatString);
         }
         private static DateTime DiscardDayTime(DateTime d)
         {

+ 2 - 2
src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs

@@ -49,7 +49,7 @@ namespace Avalonia.Controls.Primitives
 
         private void InvokeCollectionChanged(System.Collections.IList removedItems, System.Collections.IList addedItems)
         {
-            _owner.OnSelectedDatesCollectionChanged(new SelectionChangedEventArgs(null, addedItems, removedItems));
+            _owner.OnSelectedDatesCollectionChanged(new SelectionChangedEventArgs(null, removedItems, addedItems));
         }
 
         /// <summary>
@@ -119,7 +119,7 @@ namespace Avalonia.Controls.Primitives
                 }
             }
 
-            _owner.OnSelectedDatesCollectionChanged(new SelectionChangedEventArgs(null, _addedItems, _owner.RemovedItems));
+            _owner.OnSelectedDatesCollectionChanged(new SelectionChangedEventArgs(null, _owner.RemovedItems, _addedItems));
             _owner.RemovedItems.Clear();
             _owner.UpdateMonths();
             _isRangeAdded = false;

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