Browse Source

Merge branch 'master' into prs/geometrytostring

pull/3367/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
dbf482426c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 27
      Avalonia.sln
  2. 2
      build/SharedVersion.props
  3. 28
      native/Avalonia.Native/inc/avalonia-native.h
  4. 13
      native/Avalonia.Native/inc/comimpl.h
  5. 12
      native/Avalonia.Native/inc/rendertarget.h
  6. 20
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  7. 166
      native/Avalonia.Native/src/OSX/cgl.mm
  8. 7
      native/Avalonia.Native/src/OSX/common.h
  9. 261
      native/Avalonia.Native/src/OSX/gl.mm
  10. 12
      native/Avalonia.Native/src/OSX/main.mm
  11. 284
      native/Avalonia.Native/src/OSX/rendertarget.mm
  12. 217
      native/Avalonia.Native/src/OSX/window.mm
  13. 1
      nukebuild/Build.cs
  14. 8
      readme.md
  15. 6
      samples/ControlCatalog/MainView.xaml
  16. 85
      samples/ControlCatalog/Pages/ImagePage.xaml
  17. 39
      samples/ControlCatalog/Pages/ImagePage.xaml.cs
  18. 8
      samples/ControlCatalog/Pages/ProgressBarPage.xaml
  19. 2
      samples/RenderDemo/Pages/RenderTargetBitmapPage.cs
  20. 64
      scripts/avalonia-rename.ps1
  21. 26
      src/Avalonia.Animation/Animatable.cs
  22. 2
      src/Avalonia.Animation/IterationCount.cs
  23. 8
      src/Avalonia.Base/AttachedProperty.cs
  24. 724
      src/Avalonia.Base/AvaloniaObject.cs
  25. 335
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  26. 148
      src/Avalonia.Base/AvaloniaProperty.cs
  27. 40
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
  28. 67
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs
  29. 163
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  30. 28
      src/Avalonia.Base/AvaloniaProperty`1.cs
  31. 28
      src/Avalonia.Base/BoxedValue.cs
  32. 20
      src/Avalonia.Base/Collections/AvaloniaList.cs
  33. 40
      src/Avalonia.Base/Collections/Pooled/ClearMode.cs
  34. 31
      src/Avalonia.Base/Collections/Pooled/ICollectionDebugView.cs
  35. 21
      src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs
  36. 1531
      src/Avalonia.Base/Collections/Pooled/PooledList.cs
  37. 699
      src/Avalonia.Base/Collections/Pooled/PooledStack.cs
  38. 28
      src/Avalonia.Base/Collections/Pooled/StackDebugView.cs
  39. 691
      src/Avalonia.Base/Collections/Pooled/ThrowHelper.cs
  40. 26
      src/Avalonia.Base/Data/BindingNotification.cs
  41. 19
      src/Avalonia.Base/Data/BindingOperations.cs
  42. 432
      src/Avalonia.Base/Data/BindingValue.cs
  43. 152
      src/Avalonia.Base/Data/Optional.cs
  44. 31
      src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs
  45. 88
      src/Avalonia.Base/DirectProperty.cs
  46. 153
      src/Avalonia.Base/DirectPropertyBase.cs
  47. 81
      src/Avalonia.Base/IAvaloniaObject.cs
  48. 51
      src/Avalonia.Base/IPriorityValueOwner.cs
  49. 9
      src/Avalonia.Base/IStyledPropertyAccessor.cs
  50. 7
      src/Avalonia.Base/IStyledPropertyMetadata.cs
  51. 160
      src/Avalonia.Base/PriorityBindingEntry.cs
  52. 227
      src/Avalonia.Base/PriorityLevel.cs
  53. 315
      src/Avalonia.Base/PriorityValue.cs
  54. 107
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  55. 33
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  56. 25
      src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs
  57. 24
      src/Avalonia.Base/PropertyStore/IValue.cs
  58. 23
      src/Avalonia.Base/PropertyStore/IValueSink.cs
  59. 22
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  60. 195
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  61. 76
      src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs
  62. 18
      src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs
  63. 61
      src/Avalonia.Base/Reactive/BindingValueAdapter.cs
  64. 35
      src/Avalonia.Base/Reactive/BindingValueExtensions.cs
  65. 63
      src/Avalonia.Base/Reactive/TypedBindingAdapter.cs
  66. 57
      src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs
  67. 4
      src/Avalonia.Base/StyledProperty.cs
  68. 125
      src/Avalonia.Base/StyledPropertyBase.cs
  69. 45
      src/Avalonia.Base/StyledPropertyMetadata`1.cs
  70. 21
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  71. 128
      src/Avalonia.Base/Utilities/DeferredSetter.cs
  72. 43
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  73. 94
      src/Avalonia.Base/Utilities/ValueSingleOrList.cs
  74. 9
      src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs
  75. 335
      src/Avalonia.Base/ValueStore.cs
  76. 2
      src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs
  77. 152
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  78. 20
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  79. 20
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  80. 2
      src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs
  81. 2
      src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs
  82. 33
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  83. 15
      src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs
  84. 2
      src/Avalonia.Controls/Application.cs
  85. 37
      src/Avalonia.Controls/AutoCompleteBox.cs
  86. 33
      src/Avalonia.Controls/Button.cs
  87. 28
      src/Avalonia.Controls/ButtonSpinner.cs
  88. 18
      src/Avalonia.Controls/Calendar/Calendar.cs
  89. 6
      src/Avalonia.Controls/Calendar/CalendarExtensions.cs
  90. 14
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  91. 40
      src/Avalonia.Controls/Calendar/DatePicker.cs
  92. 4
      src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs
  93. 2
      src/Avalonia.Controls/ComboBox.cs
  94. 20
      src/Avalonia.Controls/ControlExtensions.cs
  95. 12
      src/Avalonia.Controls/DefinitionBase.cs
  96. 4
      src/Avalonia.Controls/DrawingPresenter.cs
  97. 41
      src/Avalonia.Controls/Expander.cs
  98. 8
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  99. 24
      src/Avalonia.Controls/Grid.cs
  100. 12
      src/Avalonia.Controls/GridSplitter.cs

27
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}

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

28
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
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
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

20
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
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;
};

7
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();

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

12
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();

284
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);
}

217
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
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");

8
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:
```

6
samples/ControlCatalog/MainView.xaml

@ -32,7 +32,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">

85
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>

39
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;
}
}
}

8
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">

2
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);

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 .

26
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;
}
}

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

8
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)
{
}

724
src/Avalonia.Base/AvaloniaObject.cs

File diff suppressed because it is too large

335
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"/>.

148
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>

40
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
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);
}
}

163
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; }

28
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;
}
}
}

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
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
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
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
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
src/Avalonia.Base/Collections/Pooled/PooledList.cs

File diff suppressed because it is too large

699
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
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
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
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()
{

19
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
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
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;
}
}

31
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);
}
}
}

88
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
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.");
}
}
}

81
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);
}
}

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

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

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

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

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

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

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

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

125
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]

45
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
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);

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

43
src/Avalonia.Base/Utilities/TypeUtilities.cs

@ -92,8 +92,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 +118,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 +131,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 +140,7 @@ namespace Avalonia.Utilities
}
}
if (!fromTypeInfo.IsEnum && toTypeInfo.IsEnum)
if (!from.IsEnum && to.IsEnum)
{
result = null;
@ -154,7 +151,7 @@ namespace Avalonia.Utilities
}
}
if (fromTypeInfo.IsEnum && IsNumeric(to))
if (from.IsEnum && IsNumeric(to))
{
try
{
@ -223,10 +220,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 +284,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 +302,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 +328,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 +347,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
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;
}
}
}

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

335
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 ? (object)slot.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;
}
}
}

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

152
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -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);

20
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@ -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;
@ -326,7 +326,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 +371,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 +391,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;

20
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>

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

2
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")]

33
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>

15
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;
}
}
}

2
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.

37
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>

33
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);
}
}
}

28
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);
}
}
}

18
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)
{

6
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;
}
}
}

14
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);
}
}

40
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)
{

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

2
src/Avalonia.Controls/ComboBox.cs

@ -130,7 +130,7 @@ namespace Avalonia.Controls
return;
if (e.Key == Key.F4 ||
((e.Key == Key.Down || e.Key == Key.Up) && ((e.Modifiers & InputModifiers.Alt) != 0)))
((e.Key == Key.Down || e.Key == Key.Up) && ((e.KeyModifiers & KeyModifiers.Alt) != 0)))
{
IsDropDownOpen = !IsDropDownOpen;
e.Handled = true;

20
src/Avalonia.Controls/ControlExtensions.cs

@ -69,26 +69,6 @@ namespace Avalonia.Controls
return nameScope.Find<T>(name);
}
/// <summary>
/// Adds or removes a pseudoclass depending on a boolean value.
/// </summary>
/// <param name="classes">The pseudoclasses collection.</param>
/// <param name="name">The name of the pseudoclass to set.</param>
/// <param name="value">True to add the pseudoclass or false to remove.</param>
public static void Set(this IPseudoClasses classes, string name, bool value)
{
Contract.Requires<ArgumentNullException>(classes != null);
if (value)
{
classes.Add(name);
}
else
{
classes.Remove(name);
}
}
/// <summary>
/// Sets a pseudoclass depending on an observable trigger.
/// </summary>

12
src/Avalonia.Controls/DefinitionBase.cs

@ -356,9 +356,13 @@ namespace Avalonia.Controls
/// b) contains only letters, digits and underscore ('_').
/// c) does not start with a digit.
/// </remarks>
private static string SharedSizeGroupPropertyValueValid(Control _, string value)
private static bool SharedSizeGroupPropertyValueValid(string value)
{
Contract.Requires<ArgumentNullException>(value != null);
// null is default value
if (value == null)
{
return true;
}
string id = (string)value;
@ -380,11 +384,11 @@ namespace Avalonia.Controls
if (i == id.Length)
{
return value;
return true;
}
}
throw new ArgumentException("Invalid SharedSizeGroup string.");
return false;
}
/// <remark>

4
src/Avalonia.Controls/DrawingPresenter.cs

@ -1,9 +1,11 @@
using Avalonia.Controls.Shapes;
using System;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Metadata;
namespace Avalonia.Controls
{
[Obsolete("Use Image control with DrawingImage source")]
public class DrawingPresenter : Control
{
static DrawingPresenter()

41
src/Avalonia.Controls/Expander.cs

@ -1,5 +1,6 @@
using Avalonia.Animation;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
namespace Avalonia.Controls
{
@ -30,16 +31,14 @@ namespace Avalonia.Controls
static Expander()
{
PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Down, ":down");
PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Up, ":up");
PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Left, ":left");
PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Right, ":right");
PseudoClass<Expander>(IsExpandedProperty, ":expanded");
IsExpandedProperty.Changed.AddClassHandler<Expander>((x, e) => x.OnIsExpandedChanged(e));
}
public Expander()
{
UpdatePseudoClasses(ExpandDirection);
}
public IPageTransition ContentTransition
{
get => GetValue(ContentTransitionProperty);
@ -55,7 +54,11 @@ namespace Avalonia.Controls
public bool IsExpanded
{
get { return _isExpanded; }
set { SetAndRaise(IsExpandedProperty, ref _isExpanded, value); }
set
{
SetAndRaise(IsExpandedProperty, ref _isExpanded, value);
PseudoClasses.Set(":expanded", value);
}
}
protected virtual void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e)
@ -74,5 +77,27 @@ 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 == ExpandDirectionProperty)
{
UpdatePseudoClasses(newValue.GetValueOrDefault<ExpandDirection>());
}
}
private void UpdatePseudoClasses(ExpandDirection d)
{
PseudoClasses.Set(":up", d == ExpandDirection.Up);
PseudoClasses.Set(":down", d == ExpandDirection.Down);
PseudoClasses.Set(":left", d == ExpandDirection.Left);
PseudoClasses.Set(":right", d == ExpandDirection.Right);
}
}
}

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

@ -127,7 +127,7 @@ namespace Avalonia.Controls.Generators
Index = new TreeContainerIndex();
_treeView = treeViewOwner;
}
else if (Owner.IsAttachedToLogicalTree)
else
{
var treeView = Owner.GetSelfAndLogicalAncestors().OfType<TreeView>().FirstOrDefault();
@ -138,12 +138,6 @@ namespace Avalonia.Controls.Generators
_treeView = treeView;
}
}
else
{
Clear();
Index = null;
_treeView = null;
}
}
class WrapperTreeDataTemplate : ITreeDataTemplate

24
src/Avalonia.Controls/Grid.cs

@ -2741,11 +2741,7 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterAttached<Grid, Control, int>(
"Column",
defaultValue: 0,
validate: (_, v) =>
{
if (v >= 0) return v;
else throw new ArgumentException("Invalid Grid.Column value.");
});
validate: v => v >= 0);
/// <summary>
/// Row property. This is an attached property.
@ -2762,11 +2758,7 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterAttached<Grid, Control, int>(
"Row",
defaultValue: 0,
validate: (_, v) =>
{
if (v >= 0) return v;
else throw new ArgumentException("Invalid Grid.Row value.");
});
validate: v => v >= 0);
/// <summary>
/// ColumnSpan property. This is an attached property.
@ -2782,11 +2774,7 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterAttached<Grid, Control, int>(
"ColumnSpan",
defaultValue: 1,
validate: (_, v) =>
{
if (v >= 1) return v;
else throw new ArgumentException("Invalid Grid.ColumnSpan value.");
});
validate: v => v >= 0);
/// <summary>
/// RowSpan property. This is an attached property.
@ -2802,11 +2790,7 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterAttached<Grid, Control, int>(
"RowSpan",
defaultValue: 1,
validate: (_, v) =>
{
if (v >= 1) return v;
else throw new ArgumentException("Invalid Grid.RowSpan value.");
});
validate: v => v >= 0);
/// <summary>
/// IsSharedSizeScope property marks scoping element for shared size.

12
src/Avalonia.Controls/GridSplitter.cs

@ -23,37 +23,37 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="ResizeDirection"/> property.
/// </summary>
public static readonly AvaloniaProperty<GridResizeDirection> ResizeDirectionProperty =
public static readonly StyledProperty<GridResizeDirection> ResizeDirectionProperty =
AvaloniaProperty.Register<GridSplitter, GridResizeDirection>(nameof(ResizeDirection));
/// <summary>
/// Defines the <see cref="ResizeBehavior"/> property.
/// </summary>
public static readonly AvaloniaProperty<GridResizeBehavior> ResizeBehaviorProperty =
public static readonly StyledProperty<GridResizeBehavior> ResizeBehaviorProperty =
AvaloniaProperty.Register<GridSplitter, GridResizeBehavior>(nameof(ResizeBehavior));
/// <summary>
/// Defines the <see cref="ShowsPreview"/> property.
/// </summary>
public static readonly AvaloniaProperty<bool> ShowsPreviewProperty =
public static readonly StyledProperty<bool> ShowsPreviewProperty =
AvaloniaProperty.Register<GridSplitter, bool>(nameof(ShowsPreview));
/// <summary>
/// Defines the <see cref="KeyboardIncrement"/> property.
/// </summary>
public static readonly AvaloniaProperty<double> KeyboardIncrementProperty =
public static readonly StyledProperty<double> KeyboardIncrementProperty =
AvaloniaProperty.Register<GridSplitter, double>(nameof(KeyboardIncrement), 10d);
/// <summary>
/// Defines the <see cref="DragIncrement"/> property.
/// </summary>
public static readonly AvaloniaProperty<double> DragIncrementProperty =
public static readonly StyledProperty<double> DragIncrementProperty =
AvaloniaProperty.Register<GridSplitter, double>(nameof(DragIncrement), 1d);
/// <summary>
/// Defines the <see cref="PreviewContent"/> property.
/// </summary>
public static readonly AvaloniaProperty<ITemplate<IControl>> PreviewContentProperty =
public static readonly StyledProperty<ITemplate<IControl>> PreviewContentProperty =
AvaloniaProperty.Register<GridSplitter, ITemplate<IControl>>(nameof(PreviewContent));
private static readonly Cursor s_columnSplitterCursor = new Cursor(StandardCursorType.SizeWestEast);

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

Loading…
Cancel
Save