Browse Source

Merge branch 'master' into pull_refresh

pull/9437/head
Max Katz 4 years ago
committed by GitHub
parent
commit
ee823737d3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      .gitignore
  2. 54
      Avalonia.sln
  3. 2
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  4. 2
      native/Avalonia.Native/src/OSX/WindowBaseImpl.h
  5. 5
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  6. 1
      packages/Avalonia/AvaloniaBuildTasks.targets
  7. 0
      samples/ControlCatalog.Browser.Blazor/App.razor
  8. 4
      samples/ControlCatalog.Browser.Blazor/App.razor.cs
  9. 6
      samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj
  10. 2
      samples/ControlCatalog.Browser.Blazor/Pages/Index.razor
  11. 2
      samples/ControlCatalog.Browser.Blazor/Program.cs
  12. 0
      samples/ControlCatalog.Browser.Blazor/Properties/launchSettings.json
  13. 0
      samples/ControlCatalog.Browser.Blazor/Shared/MainLayout.razor
  14. 2
      samples/ControlCatalog.Browser.Blazor/_Imports.razor
  15. 0
      samples/ControlCatalog.Browser.Blazor/wwwroot/css/app.css
  16. 0
      samples/ControlCatalog.Browser.Blazor/wwwroot/favicon.ico
  17. 0
      samples/ControlCatalog.Browser.Blazor/wwwroot/index.html
  18. 6
      samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj
  19. 4
      samples/ControlCatalog.Browser/EmbedSample.Browser.cs
  20. 0
      samples/ControlCatalog.Browser/Logo.svg
  21. 4
      samples/ControlCatalog.Browser/Program.cs
  22. 0
      samples/ControlCatalog.Browser/Roots.xml
  23. 0
      samples/ControlCatalog.Browser/app.css
  24. 0
      samples/ControlCatalog.Browser/embed.js
  25. 0
      samples/ControlCatalog.Browser/favicon.ico
  26. 0
      samples/ControlCatalog.Browser/index.html
  27. 0
      samples/ControlCatalog.Browser/main.js
  28. 0
      samples/ControlCatalog.Browser/runtimeconfig.template.json
  29. 9
      samples/ControlCatalog/App.xaml
  30. 158
      samples/ControlCatalog/App.xaml.cs
  31. 39
      samples/ControlCatalog/MainView.xaml.cs
  32. 2
      src/Avalonia.Base/Animation/Animatable.cs
  33. 2
      src/Avalonia.Base/Animation/AnimationInstance`1.cs
  34. 3
      src/Avalonia.Base/Avalonia.Base.csproj
  35. 542
      src/Avalonia.Base/AvaloniaObject.cs
  36. 36
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  37. 15
      src/Avalonia.Base/AvaloniaProperty.cs
  38. 24
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
  39. 28
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs
  40. 32
      src/Avalonia.Base/Compatibility/CollectionCompatibilityExtensions.cs
  41. 20
      src/Avalonia.Base/Data/BindingNotification.cs
  42. 5
      src/Avalonia.Base/Data/BindingPriority.cs
  43. 130
      src/Avalonia.Base/Data/BindingValue.cs
  44. 9
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  45. 50
      src/Avalonia.Base/DirectPropertyBase.cs
  46. 7
      src/Avalonia.Base/IStyledPropertyAccessor.cs
  47. 4
      src/Avalonia.Base/Properties/AssemblyInfo.cs
  48. 25
      src/Avalonia.Base/PropertyStore/AvaloniaPropertyDictionaryPool.cs
  49. 154
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  50. 148
      src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
  51. 82
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  52. 76
      src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs
  53. 55
      src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs
  54. 168
      src/Avalonia.Base/PropertyStore/EffectiveValue.cs
  55. 270
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  56. 8
      src/Avalonia.Base/PropertyStore/IBatchUpdate.cs
  57. 18
      src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs
  58. 28
      src/Avalonia.Base/PropertyStore/IValue.cs
  59. 30
      src/Avalonia.Base/PropertyStore/IValueEntry.cs
  60. 16
      src/Avalonia.Base/PropertyStore/IValueEntry`1.cs
  61. 31
      src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
  62. 63
      src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs
  63. 59
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs
  64. 41
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  65. 62
      src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs
  66. 82
      src/Avalonia.Base/PropertyStore/LoggingUtils.cs
  67. 326
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  68. 35
      src/Avalonia.Base/PropertyStore/PropertyNotifying.cs
  69. 35
      src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs
  70. 52
      src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs
  71. 33
      src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs
  72. 55
      src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs
  73. 103
      src/Avalonia.Base/PropertyStore/ValueFrame.cs
  74. 45
      src/Avalonia.Base/PropertyStore/ValueOwner.cs
  75. 962
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  76. 59
      src/Avalonia.Base/Reactive/BindingValueAdapter.cs
  77. 33
      src/Avalonia.Base/Reactive/BindingValueExtensions.cs
  78. 6
      src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs
  79. 62
      src/Avalonia.Base/Reactive/TypedBindingAdapter.cs
  80. 55
      src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs
  81. 149
      src/Avalonia.Base/StyledElement.cs
  82. 86
      src/Avalonia.Base/StyledPropertyBase.cs
  83. 42
      src/Avalonia.Base/Styling/Activators/AndActivator.cs
  84. 28
      src/Avalonia.Base/Styling/Activators/IStyleActivator.cs
  85. 3
      src/Avalonia.Base/Styling/Activators/IStyleActivatorSink.cs
  86. 9
      src/Avalonia.Base/Styling/Activators/NotActivator.cs
  87. 19
      src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
  88. 36
      src/Avalonia.Base/Styling/Activators/OrActivator.cs
  89. 10
      src/Avalonia.Base/Styling/Activators/PropertyEqualsActivator.cs
  90. 65
      src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs
  91. 25
      src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs
  92. 4
      src/Avalonia.Base/Styling/ChildSelector.cs
  93. 9
      src/Avalonia.Base/Styling/ControlTheme.cs
  94. 4
      src/Avalonia.Base/Styling/DescendentSelector.cs
  95. 6
      src/Avalonia.Base/Styling/DirectPropertySetterBindingInstance.cs
  96. 12
      src/Avalonia.Base/Styling/DirectPropertySetterInstance.cs
  97. 6
      src/Avalonia.Base/Styling/ISetter.cs
  98. 34
      src/Avalonia.Base/Styling/ISetterInstance.cs
  99. 19
      src/Avalonia.Base/Styling/IStyleInstance.cs
  100. 20
      src/Avalonia.Base/Styling/IStyleable.cs

10
.gitignore

@ -210,9 +210,9 @@ coc-settings.json
.ccls-cache .ccls-cache
.ccls .ccls
*.map *.map
src/Web/Avalonia.Web.Blazor/wwwroot/*.js src/Browser/Avalonia.Browser.Blazor/wwwroot/*.js
src/Web/Avalonia.Web.Blazor/Interop/Typescript/*.js src/Browser/Avalonia.Browser.Blazor/Interop/Typescript/*.js
node_modules node_modules
src/Web/Avalonia.Web.Blazor/webapp/package-lock.json src/Browser/Avalonia.Browser.Blazor/webapp/package-lock.json
src/Web/Avalonia.Web.Blazor/wwwroot src/Browser/Avalonia.Browser.Blazor/wwwroot
src/Web/Avalonia.Web/wwwroot src/Browser/Avalonia.Browser/wwwroot

54
Avalonia.sln

@ -198,9 +198,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestApp", "sampl
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.IntegrationTests.Appium", "tests\Avalonia.IntegrationTests.Appium\Avalonia.IntegrationTests.Appium.csproj", "{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.IntegrationTests.Appium", "tests\Avalonia.IntegrationTests.Appium\Avalonia.IntegrationTests.Appium.csproj", "{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Browser", "Browser", "{86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Web.Blazor", "src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.csproj", "{25831348-EB2A-483E-9576-E8F6528674A5}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsInteropTest", "samples\interop\WindowsInteropTest\WindowsInteropTest.csproj", "{26A98DA1-D89D-4A95-8152-349F404DA2E2}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsInteropTest", "samples\interop\WindowsInteropTest\WindowsInteropTest.csproj", "{26A98DA1-D89D-4A95-8152-349F404DA2E2}"
EndProject EndProject
@ -216,8 +214,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Te
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Web", "src\Web\Avalonia.Web\Avalonia.Web.csproj", "{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox", "samples\MobileSandbox\MobileSandbox.csproj", "{3B8519C1-2F51-4F12-A348-120AB91D4532}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox", "samples\MobileSandbox\MobileSandbox.csproj", "{3B8519C1-2F51-4F12-A348-120AB91D4532}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox.Android", "samples\MobileSandbox.Android\MobileSandbox.Android.csproj", "{C90FE60B-B01E-4F35-91D6-379D6966030F}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox.Android", "samples\MobileSandbox.Android\MobileSandbox.Android.csproj", "{C90FE60B-B01E-4F35-91D6-379D6966030F}"
@ -226,9 +222,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox.iOS", "sample
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox.Desktop", "samples\MobileSandbox.Desktop\MobileSandbox.Desktop.csproj", "{62D392C9-81CF-487F-92E8-598B2AF3FDCE}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox.Desktop", "samples\MobileSandbox.Desktop\MobileSandbox.Desktop.csproj", "{62D392C9-81CF-487F-92E8-598B2AF3FDCE}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Blazor.Web", "samples\ControlCatalog.Blazor.Web\ControlCatalog.Blazor.Web.csproj", "{6A710364-AE6D-40BD-968B-024311527AC2}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Browser", "src\Browser\Avalonia.Browser\Avalonia.Browser.csproj", "{4A39637C-9338-4925-A4DB-D072E292EC78}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Browser.Blazor", "src\Browser\Avalonia.Browser.Blazor\Avalonia.Browser.Blazor.csproj", "{47F8530C-F19B-4B1A-B4D6-EB231522AE5D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser", "samples\ControlCatalog.Browser\ControlCatalog.Browser.csproj", "{15B93A4C-1B46-43F6-B534-7B25B6E99932}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Web", "samples\ControlCatalog.Web\ControlCatalog.Web.csproj", "{8B3E8405-DE18-4048-A459-9CA4AC3319A2}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser.Blazor", "samples\ControlCatalog.Browser.Blazor\ControlCatalog.Browser.Blazor.csproj", "{90B08091-9BBD-4362-B712-E9F2CC62B218}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -480,10 +480,6 @@ Global
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|Any CPU.Build.0 = Debug|Any CPU {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|Any CPU.ActiveCfg = Release|Any CPU {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|Any CPU.Build.0 = Release|Any CPU {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|Any CPU.Build.0 = Release|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Release|Any CPU.Build.0 = Release|Any CPU
{26A98DA1-D89D-4A95-8152-349F404DA2E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{26A98DA1-D89D-4A95-8152-349F404DA2E2}.Debug|Any CPU.Build.0 = Debug|Any CPU {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{26A98DA1-D89D-4A95-8152-349F404DA2E2}.Release|Any CPU.ActiveCfg = Release|Any CPU {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -512,10 +508,6 @@ Global
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.Build.0 = Debug|Any CPU {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.ActiveCfg = Release|Any CPU {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.Build.0 = Release|Any CPU {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.Build.0 = Release|Any CPU
{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Release|Any CPU.Build.0 = Release|Any CPU
{3B8519C1-2F51-4F12-A348-120AB91D4532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3B8519C1-2F51-4F12-A348-120AB91D4532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B8519C1-2F51-4F12-A348-120AB91D4532}.Debug|Any CPU.Build.0 = Debug|Any CPU {3B8519C1-2F51-4F12-A348-120AB91D4532}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B8519C1-2F51-4F12-A348-120AB91D4532}.Release|Any CPU.ActiveCfg = Release|Any CPU {3B8519C1-2F51-4F12-A348-120AB91D4532}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -533,14 +525,22 @@ Global
{62D392C9-81CF-487F-92E8-598B2AF3FDCE}.Debug|Any CPU.Build.0 = Debug|Any CPU {62D392C9-81CF-487F-92E8-598B2AF3FDCE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{62D392C9-81CF-487F-92E8-598B2AF3FDCE}.Release|Any CPU.ActiveCfg = Release|Any CPU {62D392C9-81CF-487F-92E8-598B2AF3FDCE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{62D392C9-81CF-487F-92E8-598B2AF3FDCE}.Release|Any CPU.Build.0 = Release|Any CPU {62D392C9-81CF-487F-92E8-598B2AF3FDCE}.Release|Any CPU.Build.0 = Release|Any CPU
{6A710364-AE6D-40BD-968B-024311527AC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4A39637C-9338-4925-A4DB-D072E292EC78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6A710364-AE6D-40BD-968B-024311527AC2}.Debug|Any CPU.Build.0 = Debug|Any CPU {4A39637C-9338-4925-A4DB-D072E292EC78}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A710364-AE6D-40BD-968B-024311527AC2}.Release|Any CPU.ActiveCfg = Release|Any CPU {4A39637C-9338-4925-A4DB-D072E292EC78}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6A710364-AE6D-40BD-968B-024311527AC2}.Release|Any CPU.Build.0 = Release|Any CPU {4A39637C-9338-4925-A4DB-D072E292EC78}.Release|Any CPU.Build.0 = Release|Any CPU
{8B3E8405-DE18-4048-A459-9CA4AC3319A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {47F8530C-F19B-4B1A-B4D6-EB231522AE5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B3E8405-DE18-4048-A459-9CA4AC3319A2}.Debug|Any CPU.Build.0 = Debug|Any CPU {47F8530C-F19B-4B1A-B4D6-EB231522AE5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B3E8405-DE18-4048-A459-9CA4AC3319A2}.Release|Any CPU.ActiveCfg = Release|Any CPU {47F8530C-F19B-4B1A-B4D6-EB231522AE5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B3E8405-DE18-4048-A459-9CA4AC3319A2}.Release|Any CPU.Build.0 = Release|Any CPU {47F8530C-F19B-4B1A-B4D6-EB231522AE5D}.Release|Any CPU.Build.0 = Release|Any CPU
{15B93A4C-1B46-43F6-B534-7B25B6E99932}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{15B93A4C-1B46-43F6-B534-7B25B6E99932}.Debug|Any CPU.Build.0 = Debug|Any CPU
{15B93A4C-1B46-43F6-B534-7B25B6E99932}.Release|Any CPU.ActiveCfg = Release|Any CPU
{15B93A4C-1B46-43F6-B534-7B25B6E99932}.Release|Any CPU.Build.0 = Release|Any CPU
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -591,20 +591,20 @@ Global
{BC594FD5-4AF2-409E-A1E6-04123F54D7C5} = {9B9E3891-2366-4253-A952-D08BCEB71098} {BC594FD5-4AF2-409E-A1E6-04123F54D7C5} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{676D6BFD-029D-4E43-BFC7-3892265CE251} = {9B9E3891-2366-4253-A952-D08BCEB71098} {676D6BFD-029D-4E43-BFC7-3892265CE251} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{25831348-EB2A-483E-9576-E8F6528674A5} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
{26A98DA1-D89D-4A95-8152-349F404DA2E2} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {26A98DA1-D89D-4A95-8152-349F404DA2E2} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270} = {9B9E3891-2366-4253-A952-D08BCEB71098} {A0D0A6A4-5C72-4ADA-9B27-621C7D94F270} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{76D39FF6-6B4F-46C4-93CD-E6FC4665739E} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
{3B8519C1-2F51-4F12-A348-120AB91D4532} = {9B9E3891-2366-4253-A952-D08BCEB71098} {3B8519C1-2F51-4F12-A348-120AB91D4532} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C90FE60B-B01E-4F35-91D6-379D6966030F} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C90FE60B-B01E-4F35-91D6-379D6966030F} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{FED9A71D-00D7-4F40-A9E4-1229EEA28EEB} = {9B9E3891-2366-4253-A952-D08BCEB71098} {FED9A71D-00D7-4F40-A9E4-1229EEA28EEB} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{62D392C9-81CF-487F-92E8-598B2AF3FDCE} = {9B9E3891-2366-4253-A952-D08BCEB71098} {62D392C9-81CF-487F-92E8-598B2AF3FDCE} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{6A710364-AE6D-40BD-968B-024311527AC2} = {9B9E3891-2366-4253-A952-D08BCEB71098} {4A39637C-9338-4925-A4DB-D072E292EC78} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
{8B3E8405-DE18-4048-A459-9CA4AC3319A2} = {9B9E3891-2366-4253-A952-D08BCEB71098} {47F8530C-F19B-4B1A-B4D6-EB231522AE5D} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
{15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

2
native/Avalonia.Native/src/OSX/AvnWindow.mm

@ -171,9 +171,7 @@
_closed = false; _closed = false;
_isEnabled = true; _isEnabled = true;
[self backingScaleFactor];
[self setOpaque:NO]; [self setOpaque:NO];
[self setBackgroundColor: [NSColor clearColor]];
_isExtended = false; _isExtended = false;
_isTransitioningToFullScreen = false; _isTransitioningToFullScreen = false;

2
native/Avalonia.Native/src/OSX/WindowBaseImpl.h

@ -90,7 +90,7 @@ BEGIN_INTERFACE_MAP()
virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost **retOut) override; virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost **retOut) override;
virtual HRESULT SetBlurEnabled(bool enable) override; virtual HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode) override;
virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point,
IAvnClipboard *clipboard, IAvnDndResultCallback *cb, IAvnClipboard *clipboard, IAvnDndResultCallback *cb,

5
native/Avalonia.Native/src/OSX/WindowBaseImpl.mm

@ -489,10 +489,11 @@ HRESULT WindowBaseImpl::CreateNativeControlHost(IAvnNativeControlHost **retOut)
return S_OK; return S_OK;
} }
HRESULT WindowBaseImpl::SetBlurEnabled(bool enable) { HRESULT WindowBaseImpl::SetTransparencyMode(AvnWindowTransparencyMode mode) {
START_COM_CALL; START_COM_CALL;
[StandardContainer ShowBlur:enable]; [Window setBackgroundColor: (mode != Transparent ? [NSColor windowBackgroundColor] : [NSColor clearColor])];
[StandardContainer ShowBlur: mode == Blur];
return S_OK; return S_OK;
} }

1
packages/Avalonia/AvaloniaBuildTasks.targets

@ -99,6 +99,7 @@
AssemblyFile="@(IntermediateAssembly)" AssemblyFile="@(IntermediateAssembly)"
ReferencesFilePath="$(AvaloniaXamlReferencesTemporaryFilePath)" ReferencesFilePath="$(AvaloniaXamlReferencesTemporaryFilePath)"
OriginalCopyPath="$(AvaloniaXamlOriginalCopyFilePath)" OriginalCopyPath="$(AvaloniaXamlOriginalCopyFilePath)"
RefAssemblyFile="@(IntermediateRefAssembly)"
ProjectDirectory="$(MSBuildProjectDirectory)" ProjectDirectory="$(MSBuildProjectDirectory)"
VerifyIl="$(AvaloniaXamlIlVerifyIl)" VerifyIl="$(AvaloniaXamlIlVerifyIl)"
ReportImportance="$(AvaloniaXamlReportImportance)" ReportImportance="$(AvaloniaXamlReportImportance)"

0
samples/ControlCatalog.Blazor.Web/App.razor → samples/ControlCatalog.Browser.Blazor/App.razor

4
samples/ControlCatalog.Blazor.Web/App.razor.cs → samples/ControlCatalog.Browser.Blazor/App.razor.cs

@ -1,7 +1,7 @@
using Avalonia; using Avalonia;
using Avalonia.Web.Blazor; using Avalonia.Browser.Blazor;
namespace ControlCatalog.Blazor.Web; namespace ControlCatalog.Browser.Blazor;
public partial class App public partial class App
{ {

6
samples/ControlCatalog.Blazor.Web/ControlCatalog.Blazor.Web.csproj → samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj

@ -15,15 +15,15 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" /> <ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<ProjectReference Include="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.csproj" /> <ProjectReference Include="..\..\src\Browser\Avalonia.Browser.Blazor\Avalonia.Browser.Blazor.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" /> <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\ReferenceCoreLibraries.props" /> <Import Project="..\..\build\ReferenceCoreLibraries.props" />
<Import Project="..\..\build\BuildTargets.targets" /> <Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\src\Web\Avalonia.Web\Avalonia.Web.props" /> <Import Project="..\..\src\Browser\Avalonia.Browser\Avalonia.Browser.props" />
<Import Project="..\..\src\Web\Avalonia.Web\Avalonia.Web.targets" /> <Import Project="..\..\src\Browser\Avalonia.Browser\Avalonia.Browser.targets" />
</Project> </Project>

2
samples/ControlCatalog.Blazor.Web/Pages/Index.razor → samples/ControlCatalog.Browser.Blazor/Pages/Index.razor

@ -1,5 +1,5 @@
@page "/" @page "/"
@using Avalonia.Web.Blazor @using Avalonia.Browser.Blazor
<AvaloniaView /> <AvaloniaView />

2
samples/ControlCatalog.Blazor.Web/Program.cs → samples/ControlCatalog.Browser.Blazor/Program.cs

@ -3,7 +3,7 @@ using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using ControlCatalog.Blazor.Web; using ControlCatalog.Browser.Blazor;
public class Program public class Program
{ {

0
samples/ControlCatalog.Blazor.Web/Properties/launchSettings.json → samples/ControlCatalog.Browser.Blazor/Properties/launchSettings.json

0
samples/ControlCatalog.Blazor.Web/Shared/MainLayout.razor → samples/ControlCatalog.Browser.Blazor/Shared/MainLayout.razor

2
samples/ControlCatalog.Blazor.Web/_Imports.razor → samples/ControlCatalog.Browser.Blazor/_Imports.razor

@ -6,5 +6,5 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http @using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using ControlCatalog.Blazor.Web.Shared @using ControlCatalog.Browser.Blazor.Shared
@using SkiaSharp @using SkiaSharp

0
samples/ControlCatalog.Blazor.Web/wwwroot/css/app.css → samples/ControlCatalog.Browser.Blazor/wwwroot/css/app.css

0
samples/ControlCatalog.Blazor.Web/wwwroot/favicon.ico → samples/ControlCatalog.Browser.Blazor/wwwroot/favicon.ico

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB

0
samples/ControlCatalog.Blazor.Web/wwwroot/index.html → samples/ControlCatalog.Browser.Blazor/wwwroot/index.html

6
samples/ControlCatalog.Web/ControlCatalog.Web.csproj → samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj

@ -26,7 +26,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" /> <ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<ProjectReference Include="..\..\src\Web\Avalonia.Web\Avalonia.Web.csproj" /> <ProjectReference Include="..\..\src\Browser\Avalonia.Browser\Avalonia.Browser.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" /> <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
</ItemGroup> </ItemGroup>
@ -39,6 +39,6 @@
<WasmExtraFilesToDeploy Include="app.css" /> <WasmExtraFilesToDeploy Include="app.css" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\src\Web\Avalonia.Web\Avalonia.Web.props" /> <Import Project="..\..\src\Browser\Avalonia.Browser\Avalonia.Browser.props" />
<Import Project="..\..\src\Web\Avalonia.Web\Avalonia.Web.targets" /> <Import Project="..\..\src\Browser\Avalonia.Browser\Avalonia.Browser.targets" />
</Project> </Project>

4
samples/ControlCatalog.Web/EmbedSample.Browser.cs → samples/ControlCatalog.Browser/EmbedSample.Browser.cs

@ -1,11 +1,11 @@
using System; using System;
using System.Runtime.InteropServices.JavaScript; using System.Runtime.InteropServices.JavaScript;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Web; using Avalonia.Browser;
using ControlCatalog.Pages; using ControlCatalog.Pages;
namespace ControlCatalog.Web; namespace ControlCatalog.Browser;
public class EmbedSampleWeb : INativeDemoControl public class EmbedSampleWeb : INativeDemoControl
{ {

0
samples/ControlCatalog.Web/Logo.svg → samples/ControlCatalog.Browser/Logo.svg

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

4
samples/ControlCatalog.Web/Program.cs → samples/ControlCatalog.Browser/Program.cs

@ -1,8 +1,8 @@
using System.Runtime.Versioning; using System.Runtime.Versioning;
using Avalonia; using Avalonia;
using Avalonia.Web; using Avalonia.Browser;
using ControlCatalog; using ControlCatalog;
using ControlCatalog.Web; using ControlCatalog.Browser;
[assembly:SupportedOSPlatform("browser")] [assembly:SupportedOSPlatform("browser")]

0
samples/ControlCatalog.Web/Roots.xml → samples/ControlCatalog.Browser/Roots.xml

0
samples/ControlCatalog.Web/app.css → samples/ControlCatalog.Browser/app.css

0
samples/ControlCatalog.Web/embed.js → samples/ControlCatalog.Browser/embed.js

0
samples/ControlCatalog.Web/favicon.ico → samples/ControlCatalog.Browser/favicon.ico

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB

0
samples/ControlCatalog.Web/index.html → samples/ControlCatalog.Browser/index.html

0
samples/ControlCatalog.Web/main.js → samples/ControlCatalog.Browser/main.js

0
samples/ControlCatalog.Web/runtimeconfig.template.json → samples/ControlCatalog.Browser/runtimeconfig.template.json

9
samples/ControlCatalog/App.xaml

@ -9,6 +9,15 @@
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://ControlSamples/HamburgerMenu/HamburgerMenu.xaml" /> <ResourceInclude Source="avares://ControlSamples/HamburgerMenu/HamburgerMenu.xaml" />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
<StyleInclude x:Key="DataGridFluent" Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml" />
<StyleInclude x:Key="DataGridSimple" Source="avares://Avalonia.Controls.DataGrid/Themes/Simple.xaml" />
<StyleInclude x:Key="ColorPickerFluent" Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml" />
<StyleInclude x:Key="ColorPickerSimple" Source="avares://Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml" />
<ResourceInclude x:Key="FluentAccentColors" Source="avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml" />
<ResourceInclude x:Key="FluentBaseLightColors" Source="avares://Avalonia.Themes.Fluent/Accents/BaseLight.xaml" />
<ResourceInclude x:Key="FluentBaseDarkColors" Source="avares://Avalonia.Themes.Fluent/Accents/BaseDark.xaml" />
<ResourceInclude x:Key="FluentBaseColors" Source="avares://Avalonia.Themes.Fluent/Accents/Base.xaml" />
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>
<Application.Styles> <Application.Styles>

158
samples/ControlCatalog/App.xaml.cs

@ -3,85 +3,46 @@ using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Themes.Simple; using Avalonia.Themes.Simple;
using Avalonia.Themes.Fluent; using Avalonia.Themes.Fluent;
using ControlCatalog.Models;
using ControlCatalog.ViewModels; using ControlCatalog.ViewModels;
namespace ControlCatalog namespace ControlCatalog
{ {
public class App : Application public class App : Application
{ {
private readonly Styles _themeStylesContainer = new();
private FluentTheme? _fluentTheme;
private SimpleTheme? _simpleTheme;
private IResourceDictionary? _fluentBaseLightColors, _fluentBaseDarkColors;
private IStyle? _colorPickerFluent, _colorPickerSimple;
private IStyle? _dataGridFluent, _dataGridSimple;
public App() public App()
{ {
DataContext = new ApplicationViewModel(); DataContext = new ApplicationViewModel();
} }
public static readonly StyleInclude ColorPickerFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml")
};
public static readonly StyleInclude ColorPickerSimple = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml")
};
public static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml")
};
public static readonly StyleInclude DataGridSimple = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Simple.xaml")
};
public static FluentTheme Fluent = new FluentTheme(new Uri("avares://ControlCatalog/Styles"));
public static SimpleTheme Simple = new SimpleTheme(new Uri("avares://ControlCatalog/Styles"));
public static Styles SimpleLight = new Styles
{
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml")
},
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml")
},
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseLight.xaml")
},
Simple
};
public static Styles SimpleDark = new Styles
{
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml")
},
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml")
},
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseDark.xaml")
},
Simple
};
public override void Initialize() public override void Initialize()
{ {
Styles.Insert(0, Fluent); Styles.Add(_themeStylesContainer);
Styles.Insert(1, ColorPickerFluent);
Styles.Insert(2, DataGridFluent);
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
_fluentTheme = new FluentTheme();
_simpleTheme = new SimpleTheme();
_simpleTheme.Resources.MergedDictionaries.Add((IResourceDictionary)Resources["FluentAccentColors"]!);
_simpleTheme.Resources.MergedDictionaries.Add((IResourceDictionary)Resources["FluentBaseColors"]!);
_colorPickerFluent = (IStyle)Resources["ColorPickerFluent"]!;
_colorPickerSimple = (IStyle)Resources["ColorPickerSimple"]!;
_dataGridFluent = (IStyle)Resources["DataGridFluent"]!;
_dataGridSimple = (IStyle)Resources["DataGridSimple"]!;
_fluentBaseLightColors = (IResourceDictionary)Resources["FluentBaseLightColors"]!;
_fluentBaseDarkColors = (IResourceDictionary)Resources["FluentBaseDarkColors"]!;
SetThemeVariant(CatalogTheme.FluentLight);
} }
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
@ -97,5 +58,78 @@ namespace ControlCatalog
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
} }
private CatalogTheme _prevTheme;
public static CatalogTheme CurrentTheme => ((App)Current!)._prevTheme;
public static void SetThemeVariant(CatalogTheme theme)
{
var app = (App)Current!;
var prevTheme = app._prevTheme;
app._prevTheme = theme;
var shouldReopenWindow = theme switch
{
CatalogTheme.FluentLight => prevTheme is CatalogTheme.SimpleDark or CatalogTheme.SimpleLight,
CatalogTheme.FluentDark => prevTheme is CatalogTheme.SimpleDark or CatalogTheme.SimpleLight,
CatalogTheme.SimpleLight => prevTheme is CatalogTheme.FluentDark or CatalogTheme.FluentLight,
CatalogTheme.SimpleDark => prevTheme is CatalogTheme.FluentDark or CatalogTheme.FluentLight,
_ => throw new ArgumentOutOfRangeException(nameof(theme), theme, null)
};
if (app._themeStylesContainer.Count == 0)
{
app._themeStylesContainer.Add(new Style());
app._themeStylesContainer.Add(new Style());
app._themeStylesContainer.Add(new Style());
}
if (theme == CatalogTheme.FluentLight)
{
app._fluentTheme!.Mode = FluentThemeMode.Light;
app._themeStylesContainer[0] = app._fluentTheme;
app._themeStylesContainer[1] = app._colorPickerFluent!;
app._themeStylesContainer[2] = app._dataGridFluent!;
}
else if (theme == CatalogTheme.FluentDark)
{
app._fluentTheme!.Mode = FluentThemeMode.Dark;
app._themeStylesContainer[0] = app._fluentTheme;
app._themeStylesContainer[1] = app._colorPickerFluent!;
app._themeStylesContainer[2] = app._dataGridFluent!;
}
else if (theme == CatalogTheme.SimpleLight)
{
app._simpleTheme!.Mode = SimpleThemeMode.Light;
app._simpleTheme.Resources.MergedDictionaries.Remove(app._fluentBaseDarkColors!);
app._simpleTheme.Resources.MergedDictionaries.Add(app._fluentBaseLightColors!);
app._themeStylesContainer[0] = app._simpleTheme;
app._themeStylesContainer[1] = app._colorPickerSimple!;
app._themeStylesContainer[2] = app._dataGridSimple!;
}
else if (theme == CatalogTheme.SimpleDark)
{
app._simpleTheme!.Mode = SimpleThemeMode.Dark;
app._simpleTheme.Resources.MergedDictionaries.Remove(app._fluentBaseLightColors!);
app._simpleTheme.Resources.MergedDictionaries.Add(app._fluentBaseDarkColors!);
app._themeStylesContainer[0] = app._simpleTheme;
app._themeStylesContainer[1] = app._colorPickerSimple!;
app._themeStylesContainer[2] = app._dataGridSimple!;
}
if (shouldReopenWindow)
{
if (app.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
{
var oldWindow = desktopLifetime.MainWindow;
var newWindow = new MainWindow();
desktopLifetime.MainWindow = newWindow;
newWindow.Show();
oldWindow?.Close();
}
else if (app.ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
{
singleViewLifetime.MainView = new MainView();
}
}
}
} }
} }

39
samples/ControlCatalog/MainView.xaml.cs

@ -6,7 +6,6 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Themes.Fluent;
using ControlCatalog.Models; using ControlCatalog.Models;
using ControlCatalog.Pages; using ControlCatalog.Pages;
@ -31,46 +30,12 @@ namespace ControlCatalog
} }
var themes = this.Get<ComboBox>("Themes"); var themes = this.Get<ComboBox>("Themes");
themes.SelectedItem = App.CurrentTheme;
themes.SelectionChanged += (sender, e) => themes.SelectionChanged += (sender, e) =>
{ {
if (themes.SelectedItem is CatalogTheme theme) if (themes.SelectedItem is CatalogTheme theme)
{ {
var themeStyle = Application.Current!.Styles[0]; App.SetThemeVariant(theme);
if (theme == CatalogTheme.FluentLight)
{
if (App.Fluent.Mode != FluentThemeMode.Light)
{
App.Fluent.Mode = FluentThemeMode.Light;
}
Application.Current.Styles[0] = App.Fluent;
Application.Current.Styles[1] = App.ColorPickerFluent;
Application.Current.Styles[2] = App.DataGridFluent;
}
else if (theme == CatalogTheme.FluentDark)
{
if (App.Fluent.Mode != FluentThemeMode.Dark)
{
App.Fluent.Mode = FluentThemeMode.Dark;
}
Application.Current.Styles[0] = App.Fluent;
Application.Current.Styles[1] = App.ColorPickerFluent;
Application.Current.Styles[2] = App.DataGridFluent;
}
else if (theme == CatalogTheme.SimpleLight)
{
App.Simple.Mode = Avalonia.Themes.Simple.SimpleThemeMode.Light;
Application.Current.Styles[0] = App.SimpleLight;
Application.Current.Styles[1] = App.ColorPickerSimple;
Application.Current.Styles[2] = App.DataGridSimple;
}
else if (theme == CatalogTheme.SimpleDark)
{
App.Simple.Mode = Avalonia.Themes.Simple.SimpleThemeMode.Dark;
Application.Current.Styles[0] = App.SimpleDark;
Application.Current.Styles[1] = App.ColorPickerSimple;
Application.Current.Styles[2] = App.DataGridSimple;
}
} }
}; };

2
src/Avalonia.Base/Animation/Animatable.cs

@ -235,7 +235,7 @@ namespace Avalonia.Animation
private object? GetAnimationBaseValue(AvaloniaProperty property) private object? GetAnimationBaseValue(AvaloniaProperty property)
{ {
var value = this.GetBaseValue(property, BindingPriority.LocalValue); var value = this.GetBaseValue(property);
if (value == AvaloniaProperty.UnsetValue) if (value == AvaloniaProperty.UnsetValue)
{ {

2
src/Avalonia.Base/Animation/AnimationInstance`1.cs

@ -229,7 +229,7 @@ namespace Avalonia.Animation
private void UpdateNeutralValue() private void UpdateNeutralValue()
{ {
var property = _animator.Property ?? throw new InvalidOperationException("Animator has no property specified."); var property = _animator.Property ?? throw new InvalidOperationException("Animator has no property specified.");
var baseValue = _targetControl.GetBaseValue(property, BindingPriority.LocalValue); var baseValue = _targetControl.GetBaseValue(property);
_neutralValue = baseValue != AvaloniaProperty.UnsetValue ? _neutralValue = baseValue != AvaloniaProperty.UnsetValue ?
(T)baseValue! : (T)_targetControl.GetValue(property)!; (T)baseValue! : (T)_targetControl.GetValue(property)!;

3
src/Avalonia.Base/Avalonia.Base.csproj

@ -28,6 +28,7 @@
<ItemGroup Label="InternalsVisibleTo"> <ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.Base.UnitTests, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Base.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Controls, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.ColorPicker, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Controls.ColorPicker, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.DataGrid, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Controls.DataGrid, PublicKey=$(AvaloniaPublicKey)" />
@ -39,8 +40,8 @@
<InternalsVisibleTo Include="Avalonia.Skia.RenderTests, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Skia.RenderTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.UnitTests, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Win32, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Win32, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Web.Blazor, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Dialogs, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Dialogs, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Diagnostics, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Diagnostics, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" /> <InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" />

542
src/Avalonia.Base/AvaloniaObject.cs

@ -1,11 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Diagnostics; using Avalonia.Diagnostics;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.PropertyStore; using Avalonia.PropertyStore;
using Avalonia.Reactive;
using Avalonia.Threading; using Avalonia.Threading;
namespace Avalonia namespace Avalonia
@ -18,13 +18,11 @@ namespace Avalonia
/// </remarks> /// </remarks>
public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged
{ {
private readonly ValueStore _values;
private AvaloniaObject? _inheritanceParent; private AvaloniaObject? _inheritanceParent;
private List<IDisposable>? _directBindings;
private PropertyChangedEventHandler? _inpcChanged; private PropertyChangedEventHandler? _inpcChanged;
private EventHandler<AvaloniaPropertyChangedEventArgs>? _propertyChanged; private EventHandler<AvaloniaPropertyChangedEventArgs>? _propertyChanged;
private List<AvaloniaObject>? _inheritanceChildren; private List<AvaloniaObject>? _inheritanceChildren;
private ValueStore? _values;
private bool _batchUpdate;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AvaloniaObject"/> class. /// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
@ -32,6 +30,7 @@ namespace Avalonia
public AvaloniaObject() public AvaloniaObject()
{ {
VerifyAccess(); VerifyAccess();
_values = new ValueStore(this);
} }
/// <summary> /// <summary>
@ -59,7 +58,7 @@ namespace Avalonia
/// <value> /// <value>
/// The inheritance parent. /// The inheritance parent.
/// </value> /// </value>
protected AvaloniaObject? InheritanceParent protected internal AvaloniaObject? InheritanceParent
{ {
get get
{ {
@ -72,28 +71,10 @@ namespace Avalonia
if (_inheritanceParent != value) if (_inheritanceParent != value)
{ {
var oldParent = _inheritanceParent;
var valuestore = _values;
_inheritanceParent?.RemoveInheritanceChild(this); _inheritanceParent?.RemoveInheritanceChild(this);
_inheritanceParent = value; _inheritanceParent = value;
var properties = AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType());
var propertiesCount = properties.Count;
for (var i = 0; i < propertiesCount; i++)
{
var property = properties[i];
if (valuestore?.IsSet(property) == true)
{
// If local value set there can be no change.
continue;
}
property.RouteInheritanceParentChanged(this, oldParent);
}
_inheritanceParent?.AddInheritanceChild(this); _inheritanceParent?.AddInheritanceChild(this);
_values.SetInheritanceParent(value);
} }
} }
} }
@ -118,24 +99,15 @@ namespace Avalonia
set { this.Bind(binding.Property!, value); } set { this.Bind(binding.Property!, value); }
} }
private ValueStore Values /// <summary>
{ /// Returns a value indicating whether the current thread is the UI thread.
get /// </summary>
{ /// <returns>true if the current thread is the UI thread; otherwise false.</returns>
if (_values is null)
{
_values = new ValueStore(this);
if (_batchUpdate)
_values.BeginBatchUpdate();
}
return _values;
}
}
public bool CheckAccess() => Dispatcher.UIThread.CheckAccess(); public bool CheckAccess() => Dispatcher.UIThread.CheckAccess();
/// <summary>
/// Checks that the current thread is the UI thread and throws if not.
/// </summary>
public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess(); public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess();
/// <summary> /// <summary>
@ -144,9 +116,9 @@ namespace Avalonia
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
public void ClearValue(AvaloniaProperty property) public void ClearValue(AvaloniaProperty property)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); _ = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
property.RouteClearValue(this); _values.ClearLocalValue(property);
} }
/// <summary> /// <summary>
@ -234,9 +206,12 @@ namespace Avalonia
/// <returns>The value.</returns> /// <returns>The value.</returns>
public object? GetValue(AvaloniaProperty property) public object? GetValue(AvaloniaProperty property)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); _ = property ?? throw new ArgumentNullException(nameof(property));
return property.RouteGetValue(this); if (property.IsDirect)
return property.RouteGetValue(this);
else
return _values.GetValue(property);
} }
/// <summary> /// <summary>
@ -247,10 +222,9 @@ namespace Avalonia
/// <returns>The value.</returns> /// <returns>The value.</returns>
public T GetValue<T>(StyledPropertyBase<T> property) public T GetValue<T>(StyledPropertyBase<T> property)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); _ = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess(); VerifyAccess();
return _values.GetValue(property);
return GetValueOrInheritedOrDefault(property);
} }
/// <summary> /// <summary>
@ -269,18 +243,11 @@ namespace Avalonia
} }
/// <inheritdoc/> /// <inheritdoc/>
public Optional<T> GetBaseValue<T>(StyledPropertyBase<T> property, BindingPriority maxPriority) public Optional<T> GetBaseValue<T>(StyledPropertyBase<T> property)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); _ = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess(); VerifyAccess();
return _values.GetBaseValue(property);
if (_values is object &&
_values.TryGetValue(property, maxPriority, out var value))
{
return value;
}
return default;
} }
/// <summary> /// <summary>
@ -346,26 +313,20 @@ namespace Avalonia
T value, T value,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority = BindingPriority.LocalValue)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); _ = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess(); VerifyAccess();
ValidatePriority(priority);
LogPropertySet(property, value, priority); LogPropertySet(property, value, BindingPriority.LocalValue);
if (value is UnsetValueType) if (value is UnsetValueType)
{ {
if (priority == BindingPriority.LocalValue) if (priority == BindingPriority.LocalValue)
{ _values.ClearLocalValue(property);
Values.ClearLocalValue(property);
}
else
{
throw new NotSupportedException(
"Cannot set property to Unset at non-local value priority.");
}
} }
else if (!(value is DoNothingType)) else if (value is not DoNothingType)
{ {
return Values.SetValue(property, value, priority); return _values.SetValue(property, value, priority);
} }
return null; return null;
@ -382,6 +343,7 @@ namespace Avalonia
property = property ?? throw new ArgumentNullException(nameof(property)); property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess(); VerifyAccess();
property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
LogPropertySet(property, value, BindingPriority.LocalValue); LogPropertySet(property, value, BindingPriority.LocalValue);
SetDirectValueUnchecked(property, value); SetDirectValueUnchecked(property, value);
} }
@ -398,12 +360,52 @@ namespace Avalonia
public IDisposable Bind( public IDisposable Bind(
AvaloniaProperty property, AvaloniaProperty property,
IObservable<object?> source, IObservable<object?> source,
BindingPriority priority = BindingPriority.LocalValue) => property.RouteBind(this, source, priority);
/// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an observable.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="source">The observable.</param>
/// <param name="priority">The priority of the binding.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
public IDisposable Bind<T>(
StyledPropertyBase<T> property,
IObservable<object?> source,
BindingPriority priority = BindingPriority.LocalValue)
{
property = property ?? throw new ArgumentNullException(nameof(property));
source = source ?? throw new ArgumentNullException(nameof(source));
VerifyAccess();
ValidatePriority(priority);
return _values.AddBinding(property, source, priority);
}
/// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an observable.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="source">The observable.</param>
/// <param name="priority">The priority of the binding.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
public IDisposable Bind<T>(
StyledPropertyBase<T> property,
IObservable<T> source,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority = BindingPriority.LocalValue)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); property = property ?? throw new ArgumentNullException(nameof(property));
source = source ?? throw new ArgumentNullException(nameof(source)); source = source ?? throw new ArgumentNullException(nameof(source));
VerifyAccess();
ValidatePriority(priority);
return property.RouteBind(this, source.ToBindingValue(), priority); return _values.AddBinding(property, source, priority);
} }
/// <summary> /// <summary>
@ -424,8 +426,9 @@ namespace Avalonia
property = property ?? throw new ArgumentNullException(nameof(property)); property = property ?? throw new ArgumentNullException(nameof(property));
source = source ?? throw new ArgumentNullException(nameof(source)); source = source ?? throw new ArgumentNullException(nameof(source));
VerifyAccess(); VerifyAccess();
ValidatePriority(priority);
return Values.AddBinding(property, source, priority); return _values.AddBinding(property, source, priority);
} }
/// <summary> /// <summary>
@ -439,10 +442,9 @@ namespace Avalonia
/// </returns> /// </returns>
public IDisposable Bind<T>( public IDisposable Bind<T>(
DirectPropertyBase<T> property, DirectPropertyBase<T> property,
IObservable<BindingValue<T>> source) IObservable<object?> source)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); property = property ?? throw new ArgumentNullException(nameof(property));
source = source ?? throw new ArgumentNullException(nameof(source));
VerifyAccess(); VerifyAccess();
property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
@ -452,48 +454,67 @@ namespace Avalonia
throw new ArgumentException($"The property {property.Name} is readonly."); throw new ArgumentException($"The property {property.Name} is readonly.");
} }
Logger.TryGet(LogEventLevel.Verbose, LogArea.Property)?.Log( return _values.AddBinding(property, source);
this,
"Bound {Property} to {Binding} with priority LocalValue",
property,
GetDescription(source));
_directBindings ??= new List<IDisposable>();
return new DirectBindingSubscription<T>(this, property, source);
} }
/// <summary> /// <summary>
/// Coerces the specified <see cref="AvaloniaProperty"/>. /// Binds a <see cref="AvaloniaProperty"/> to an observable.
/// </summary> /// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
public void CoerceValue(AvaloniaProperty property) /// <param name="source">The observable.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
public IDisposable Bind<T>(
DirectPropertyBase<T> property,
IObservable<T> source)
{ {
_values?.CoerceValue(property); property = property ?? throw new ArgumentNullException(nameof(property));
} VerifyAccess();
public void BeginBatchUpdate() property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
{
if (_batchUpdate) if (property.IsReadOnly)
{ {
throw new InvalidOperationException("Batch update already in progress."); throw new ArgumentException($"The property {property.Name} is readonly.");
} }
_batchUpdate = true; return _values.AddBinding(property, source);
_values?.BeginBatchUpdate();
} }
public void EndBatchUpdate() /// <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>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
public IDisposable Bind<T>(
DirectPropertyBase<T> property,
IObservable<BindingValue<T>> source)
{ {
if (!_batchUpdate) property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
if (property.IsReadOnly)
{ {
throw new InvalidOperationException("No batch update in progress."); throw new ArgumentException($"The property {property.Name} is readonly.");
} }
_batchUpdate = false; return _values.AddBinding(property, source);
_values?.EndBatchUpdate();
} }
/// <summary>
/// Coerces the specified <see cref="AvaloniaProperty"/>.
/// </summary>
/// <param name="property">The property.</param>
public void CoerceValue(AvaloniaProperty property) => _values.CoerceValue(property);
/// <inheritdoc/> /// <inheritdoc/>
internal void AddInheritanceChild(AvaloniaObject child) internal void AddInheritanceChild(AvaloniaObject child)
{ {
@ -507,98 +528,12 @@ namespace Avalonia
_inheritanceChildren?.Remove(child); _inheritanceChildren?.Remove(child);
} }
internal void InheritedPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
Optional<T> newValue)
{
if (property.Inherits && (_values == null || !_values.IsSet(property)))
{
RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue);
}
}
/// <inheritdoc/> /// <inheritdoc/>
Delegate[]? IAvaloniaObjectDebug.GetPropertyChangedSubscribers() Delegate[]? IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
{ {
return _propertyChanged?.GetInvocationList(); return _propertyChanged?.GetInvocationList();
} }
internal void ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
var property = (StyledPropertyBase<T>)change.Property;
LogIfError(property, change.NewValue);
// If the change is to the effective value of the property and no old/new value is set
// then fill in the old/new value from property inheritance/default value. We don't do
// this for non-effective value changes because these are only needed for property
// transitions, where knowing e.g. that an inherited value is active at an arbitrary
// priority isn't of any use and would introduce overhead.
if (change.IsEffectiveValueChange && !change.OldValue.HasValue)
{
change.SetOldValue(GetInheritedOrDefault<T>(property));
}
if (change.IsEffectiveValueChange && !change.NewValue.HasValue)
{
change.SetNewValue(GetInheritedOrDefault(property));
}
if (!change.IsEffectiveValueChange ||
!EqualityComparer<T>.Default.Equals(change.OldValue.Value, change.NewValue.Value))
{
RaisePropertyChanged(change);
if (change.IsEffectiveValueChange)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.Property)?.Log(
this,
"{Property} changed from {$Old} to {$Value} with priority {Priority}",
property,
change.OldValue,
change.NewValue,
change.Priority);
}
}
}
internal void Completed<T>(
StyledPropertyBase<T> property,
IPriorityValueEntry entry,
Optional<T> oldValue)
{
var change = new AvaloniaPropertyChangedEventArgs<T>(
this,
property,
oldValue,
default,
BindingPriority.Unset);
ValueChanged(change);
}
/// <summary>
/// Called for each inherited property when the <see cref="InheritanceParent"/> changes.
/// </summary>
/// <typeparam name="T">The type of the property value.</typeparam>
/// <param name="property">The property.</param>
/// <param name="oldParent">The old inheritance parent.</param>
internal void InheritanceParentChanged<T>(
StyledPropertyBase<T> property,
AvaloniaObject? oldParent)
{
var oldValue = oldParent is not null ?
oldParent.GetValueOrInheritedOrDefault(property) :
property.GetDefaultValue(GetType());
var newValue = GetInheritedOrDefault(property);
if (!EqualityComparer<T>.Default.Equals(oldValue, newValue))
{
RaisePropertyChanged(property, oldValue, newValue);
}
}
internal AvaloniaPropertyValue GetDiagnosticInternal(AvaloniaProperty property) internal AvaloniaPropertyValue GetDiagnosticInternal(AvaloniaProperty property)
{ {
if (property.IsDirect) if (property.IsDirect)
@ -626,19 +561,23 @@ namespace Avalonia
"Unset"); "Unset");
} }
internal ValueStore GetValueStore() => _values;
internal IReadOnlyList<AvaloniaObject>? GetInheritanceChildren() => _inheritanceChildren;
/// <summary> /// <summary>
/// Logs a binding error for a property. /// Gets a logger to which a binding warning may be written.
/// </summary> /// </summary>
/// <param name="property">The property that the error occurred on.</param> /// <param name="property">The property that the error occurred on.</param>
/// <param name="e">The binding error.</param> /// <param name="e">The binding exception, if any.</param>
protected internal virtual void LogBindingError(AvaloniaProperty property, Exception e) /// <remarks>
/// This is overridden in <see cref="Visual"/> to prevent logging binding errors when a
/// control is not attached to the visual tree.
/// </remarks>
internal virtual ParametrizedLogger? GetBindingWarningLogger(
AvaloniaProperty property,
Exception? e)
{ {
Logger.TryGet(LogEventLevel.Warning, LogArea.Binding)?.Log( return Logger.TryGet(LogEventLevel.Warning, LogArea.Binding);
this,
"Error in binding to {Target}.{Property}: {Message}",
this,
property,
e.Message);
} }
/// <summary> /// <summary>
@ -675,6 +614,22 @@ namespace Avalonia
{ {
} }
/// <summary>
/// Raises the <see cref="PropertyChanged"/> event for a direct property.
/// </summary>
/// <param name="property">The property that has changed.</param>
/// <param name="oldValue">The old property value.</param>
/// <param name="newValue">The new property value.</param>
/// <param name="priority">The priority of the binding that produced the value.</param>
private protected void RaisePropertyChanged<T>(
DirectPropertyBase<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority = BindingPriority.LocalValue)
{
RaisePropertyChanged(property, oldValue, newValue, priority, true);
}
/// <summary> /// <summary>
/// Raises the <see cref="PropertyChanged"/> event. /// Raises the <see cref="PropertyChanged"/> event.
/// </summary> /// </summary>
@ -682,18 +637,32 @@ namespace Avalonia
/// <param name="oldValue">The old property value.</param> /// <param name="oldValue">The old property value.</param>
/// <param name="newValue">The new property value.</param> /// <param name="newValue">The new property value.</param>
/// <param name="priority">The priority of the binding that produced the value.</param> /// <param name="priority">The priority of the binding that produced the value.</param>
protected internal void RaisePropertyChanged<T>( /// <param name="isEffectiveValue">
/// Whether the notification represents a change to the effective value of the property.
/// </param>
internal void RaisePropertyChanged<T>(
AvaloniaProperty<T> property, AvaloniaProperty<T> property,
Optional<T> oldValue, Optional<T> oldValue,
BindingValue<T> newValue, BindingValue<T> newValue,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority,
bool isEffectiveValue)
{ {
RaisePropertyChanged(new AvaloniaPropertyChangedEventArgs<T>( var e = new AvaloniaPropertyChangedEventArgs<T>(
this, this,
property, property,
oldValue, oldValue,
newValue, newValue,
priority)); priority,
isEffectiveValue);
OnPropertyChangedCore(e);
if (isEffectiveValue)
{
property.NotifyChanged(e);
_propertyChanged?.Invoke(this, e);
_inpcChanged?.Invoke(this, new PropertyChangedEventArgs(property.Name));
}
} }
/// <summary> /// <summary>
@ -718,110 +687,24 @@ namespace Avalonia
var old = field; var old = field;
field = value; field = value;
RaisePropertyChanged(property, old, value); RaisePropertyChanged(property, old, value, BindingPriority.LocalValue, true);
return true; return true;
} }
private T GetInheritedOrDefault<T>(StyledPropertyBase<T> property)
{
if (property.Inherits && InheritanceParent is AvaloniaObject o)
{
return o.GetValueOrInheritedOrDefault(property);
}
return property.GetDefaultValue(GetType());
}
private T GetValueOrInheritedOrDefault<T>(
StyledPropertyBase<T> property,
BindingPriority maxPriority = BindingPriority.Animation)
{
var o = this;
var inherits = property.Inherits;
var value = default(T);
while (o != null)
{
var values = o._values;
if (values != null
&& values.TryGetValue(property, maxPriority, out value) == true)
{
return value;
}
if (!inherits)
{
break;
}
o = o.InheritanceParent as AvaloniaObject;
}
return property.GetDefaultValue(GetType());
}
protected internal void RaisePropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
VerifyAccess();
if (change.IsEffectiveValueChange)
{
change.Property.Notifying?.Invoke(this, true);
}
try
{
OnPropertyChangedCore(change);
if (change.IsEffectiveValueChange)
{
change.Property.NotifyChanged(change);
_propertyChanged?.Invoke(this, change);
if (_inpcChanged != null)
{
var inpce = new PropertyChangedEventArgs(change.Property.Name);
_inpcChanged(this, inpce);
}
if (change.Property.Inherits && _inheritanceChildren != null)
{
foreach (var child in _inheritanceChildren)
{
child.InheritedPropertyChanged(
change.Property,
change.OldValue,
change.NewValue.ToOptional());
}
}
}
}
finally
{
if (change.IsEffectiveValueChange)
{
change.Property.Notifying?.Invoke(this, false);
}
}
}
/// <summary> /// <summary>
/// Sets the value of a direct property. /// Sets the value of a direct property.
/// </summary> /// </summary>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
private void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, T value) internal void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, T value)
{ {
var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
if (value is UnsetValueType) if (value is UnsetValueType)
{ {
p.InvokeSetter(this, p.GetUnsetValue(GetType())); property.InvokeSetter(this, property.GetUnsetValue(GetType()));
} }
else if (!(value is DoNothingType)) else if (!(value is DoNothingType))
{ {
p.InvokeSetter(this, value); property.InvokeSetter(this, value);
} }
} }
@ -830,16 +713,9 @@ namespace Avalonia
/// </summary> /// </summary>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
private void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, BindingValue<T> value) internal void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, BindingValue<T> value)
{ {
var p = AvaloniaPropertyRegistry.Instance.FindRegisteredDirect(this, property); LoggingUtils.LogIfNecessary(this, property, value);
if (p == null)
{
throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}");
}
LogIfError(property, value);
switch (value.Type) switch (value.Type)
{ {
@ -858,7 +734,7 @@ namespace Avalonia
break; break;
} }
var metadata = p.GetMetadata(GetType()); var metadata = property.GetMetadata(GetType());
if (metadata.EnableDataValidation == true) if (metadata.EnableDataValidation == true)
{ {
@ -877,29 +753,6 @@ namespace Avalonia
return description?.Description ?? o.ToString() ?? o.GetType().Name; return description?.Description ?? o.ToString() ?? o.GetType().Name;
} }
/// <summary>
/// Logs a message if the notification represents a binding error.
/// </summary>
/// <param name="property">The property being bound.</param>
/// <param name="value">The binding notification.</param>
private void LogIfError<T>(AvaloniaProperty property, BindingValue<T> value)
{
if (value.HasError)
{
if (value.Error is AggregateException aggregate)
{
foreach (var inner in aggregate.InnerExceptions)
{
LogBindingError(property, inner);
}
}
else
{
LogBindingError(property, value.Error!);
}
}
}
/// <summary> /// <summary>
/// Logs a property set message. /// Logs a property set message.
/// </summary> /// </summary>
@ -916,49 +769,16 @@ namespace Avalonia
priority); priority);
} }
private class DirectBindingSubscription<T> : IObserver<BindingValue<T>>, IDisposable [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ValidatePriority(BindingPriority priority)
{ {
private readonly AvaloniaObject _owner; if (priority < BindingPriority.Animation || priority >= BindingPriority.Inherited)
private readonly DirectPropertyBase<T> _property; ThrowInvalidPriority(priority);
private readonly IDisposable _subscription; }
public DirectBindingSubscription(
AvaloniaObject owner,
DirectPropertyBase<T> property,
IObservable<BindingValue<T>> source)
{
_owner = owner;
_property = property;
_owner._directBindings!.Add(this);
_subscription = source.Subscribe(this);
}
public void Dispose()
{
// _subscription can be null, if Subscribe failed with an exception.
_subscription?.Dispose();
_owner._directBindings!.Remove(this);
}
public void OnCompleted() => Dispose();
public void OnError(Exception error) => Dispose();
public void OnNext(BindingValue<T> value)
{
if (Dispatcher.UIThread.CheckAccess())
{
_owner.SetDirectValueUnchecked(_property, value);
}
else
{
// To avoid allocating closure in the outer scope we need to capture variables
// locally. This allows us to skip most of the allocations when on UI thread.
var instance = _owner;
var property = _property;
var newValue = value;
Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, newValue)); private static void ThrowInvalidPriority(BindingPriority priority)
} {
} throw new ArgumentException($"Invalid priority ${priority}", nameof(priority));
} }
} }
} }

36
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -261,7 +261,6 @@ namespace Avalonia
} }
throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported."); throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported.");
} }
/// <summary> /// <summary>
@ -280,14 +279,17 @@ namespace Avalonia
IObservable<T> source, IObservable<T> source,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority = BindingPriority.LocalValue)
{ {
target = target ?? throw new ArgumentNullException(nameof(target)); if (target is AvaloniaObject ao)
property = property ?? throw new ArgumentNullException(nameof(property)); {
source = source ?? throw new ArgumentNullException(nameof(source)); return property switch
{
StyledPropertyBase<T> styled => ao.Bind(styled, source, priority),
DirectPropertyBase<T> direct => ao.Bind(direct, source),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type."),
};
}
return target.Bind( throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported.");
property,
source.ToBindingValue(),
priority);
} }
/// <summary> /// <summary>
@ -362,10 +364,8 @@ namespace Avalonia
/// </summary> /// </summary>
/// <param name="target">The object.</param> /// <param name="target">The object.</param>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="maxPriority">The maximum priority for the value.</param>
/// <remarks> /// <remarks>
/// For styled properties, gets the value of the property if set on the object with a /// For styled properties, gets the value of the property excluding animated values, otherwise
/// priority equal or lower to <paramref name="maxPriority"/>, otherwise
/// <see cref="AvaloniaProperty.UnsetValue"/>. Note that this method does not return /// <see cref="AvaloniaProperty.UnsetValue"/>. Note that this method does not return
/// property values that come from inherited or default values. /// property values that come from inherited or default values.
/// ///
@ -373,14 +373,13 @@ namespace Avalonia
/// </remarks> /// </remarks>
public static object? GetBaseValue( public static object? GetBaseValue(
this IAvaloniaObject target, this IAvaloniaObject target,
AvaloniaProperty property, AvaloniaProperty property)
BindingPriority maxPriority)
{ {
target = target ?? throw new ArgumentNullException(nameof(target)); target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property)); property = property ?? throw new ArgumentNullException(nameof(property));
if (target is AvaloniaObject ao) if (target is AvaloniaObject ao)
return property.RouteGetBaseValue(ao, maxPriority); return property.RouteGetBaseValue(ao);
throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported."); throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported.");
} }
@ -389,10 +388,8 @@ namespace Avalonia
/// </summary> /// </summary>
/// <param name="target">The object.</param> /// <param name="target">The object.</param>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="maxPriority">The maximum priority for the value.</param>
/// <remarks> /// <remarks>
/// For styled properties, gets the value of the property if set on the object with a /// For styled properties, gets the value of the property excluding animated values, otherwise
/// priority equal or lower to <paramref name="maxPriority"/>, otherwise
/// <see cref="Optional{T}.Empty"/>. Note that this method does not return property values /// <see cref="Optional{T}.Empty"/>. Note that this method does not return property values
/// that come from inherited or default values. /// that come from inherited or default values.
/// ///
@ -400,8 +397,7 @@ namespace Avalonia
/// </remarks> /// </remarks>
public static Optional<T> GetBaseValue<T>( public static Optional<T> GetBaseValue<T>(
this IAvaloniaObject target, this IAvaloniaObject target,
AvaloniaProperty<T> property, AvaloniaProperty<T> property)
BindingPriority maxPriority)
{ {
target = target ?? throw new ArgumentNullException(nameof(target)); target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property)); property = property ?? throw new ArgumentNullException(nameof(property));
@ -410,7 +406,7 @@ namespace Avalonia
{ {
return property switch return property switch
{ {
StyledPropertyBase<T> styled => ao.GetBaseValue(styled, maxPriority), StyledPropertyBase<T> styled => ao.GetBaseValue(styled),
DirectPropertyBase<T> direct => ao.GetValue(direct), DirectPropertyBase<T> direct => ao.GetValue(direct),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.")
}; };

15
src/Avalonia.Base/AvaloniaProperty.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.PropertyStore;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -455,6 +456,12 @@ namespace Avalonia
return Name; return Name;
} }
/// <summary>
/// Creates an effective value for the property.
/// </summary>
/// <param name="o">The effective value owner.</param>
internal abstract EffectiveValue CreateEffectiveValue(AvaloniaObject o);
/// <summary> /// <summary>
/// Routes an untyped ClearValue call to a typed call. /// Routes an untyped ClearValue call to a typed call.
/// </summary> /// </summary>
@ -471,8 +478,7 @@ namespace Avalonia
/// Routes an untyped GetBaseValue call to a typed call. /// Routes an untyped GetBaseValue call to a typed call.
/// </summary> /// </summary>
/// <param name="o">The object instance.</param> /// <param name="o">The object instance.</param>
/// <param name="maxPriority">The maximum priority for the value.</param> internal abstract object? RouteGetBaseValue(AvaloniaObject o);
internal abstract object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority);
/// <summary> /// <summary>
/// Routes an untyped SetValue call to a typed call. /// Routes an untyped SetValue call to a typed call.
@ -496,12 +502,9 @@ namespace Avalonia
/// <param name="priority">The priority.</param> /// <param name="priority">The priority.</param>
internal abstract IDisposable RouteBind( internal abstract IDisposable RouteBind(
AvaloniaObject o, AvaloniaObject o,
IObservable<BindingValue<object?>> source, IObservable<object?> source,
BindingPriority priority); BindingPriority priority);
internal abstract void RouteInheritanceParentChanged(AvaloniaObject o, AvaloniaObject? oldParent);
internal abstract ISetterInstance CreateSetterInstance(IStyleable target, object? value);
/// <summary> /// <summary>
/// Overrides the metadata for the property on the specified type. /// Overrides the metadata for the property on the specified type.
/// </summary> /// </summary>

24
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs

@ -17,6 +17,16 @@ namespace Avalonia
IsEffectiveValueChange = true; IsEffectiveValueChange = true;
} }
internal AvaloniaPropertyChangedEventArgs(
IAvaloniaObject sender,
BindingPriority priority,
bool isEffectiveValueChange)
{
Sender = sender;
Priority = priority;
IsEffectiveValueChange = isEffectiveValueChange;
}
/// <summary> /// <summary>
/// Gets the <see cref="AvaloniaObject"/> that the property changed on. /// Gets the <see cref="AvaloniaObject"/> that the property changed on.
/// </summary> /// </summary>
@ -49,20 +59,8 @@ namespace Avalonia
/// </value> /// </value>
public BindingPriority Priority { get; private set; } public BindingPriority Priority { get; private set; }
/// <summary> internal bool IsEffectiveValueChange { get; private set; }
/// Gets a value indicating whether the change represents a change to the effective value of
/// the property.
/// </summary>
/// <remarks>
/// This will usually be true, except in
/// <see cref="AvaloniaObject.OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs)"/>
/// which receives notifications for all changes to property values, whether a value with a higher
/// priority is present or not. When this property is false, the change that is being signaled
/// has not resulted in a change to the property value on the object.
/// </remarks>
public bool IsEffectiveValueChange { get; private set; }
internal void MarkNonEffectiveValue() => IsEffectiveValueChange = false;
protected abstract AvaloniaProperty GetProperty(); protected abstract AvaloniaProperty GetProperty();
protected abstract object? GetOldValue(); protected abstract object? GetOldValue();
protected abstract object? GetNewValue(); protected abstract object? GetNewValue();

28
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs

@ -21,7 +21,18 @@ namespace Avalonia
Optional<T> oldValue, Optional<T> oldValue,
BindingValue<T> newValue, BindingValue<T> newValue,
BindingPriority priority) BindingPriority priority)
: base(sender, priority) : this(sender, property, oldValue, newValue, priority, true)
{
}
internal AvaloniaPropertyChangedEventArgs(
IAvaloniaObject sender,
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority,
bool isEffectiveValueChange)
: base(sender, priority, isEffectiveValueChange)
{ {
Property = property; Property = property;
OldValue = oldValue; OldValue = oldValue;
@ -39,28 +50,13 @@ namespace Avalonia
/// <summary> /// <summary>
/// Gets the old value of the property. /// Gets the old value of the property.
/// </summary> /// </summary>
/// <remarks>
/// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is true, returns the
/// old value of the property on the object.
/// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is false, returns
/// <see cref="Optional{T}.Empty"/>.
/// </remarks>
public new Optional<T> OldValue { get; private set; } public new Optional<T> OldValue { get; private set; }
/// <summary> /// <summary>
/// Gets the new value of the property. /// Gets the new value of the property.
/// </summary> /// </summary>
/// <remarks>
/// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is true, returns the
/// value of the property on the object.
/// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is false returns the
/// changed value, or <see cref="Optional{T}.Empty"/> if the value was removed.
/// </remarks>
public new BindingValue<T> NewValue { get; private set; } public new BindingValue<T> NewValue { get; private set; }
internal void SetOldValue(Optional<T> value) => OldValue = value;
internal void SetNewValue(BindingValue<T> value) => NewValue = value;
protected override AvaloniaProperty GetProperty() => Property; protected override AvaloniaProperty GetProperty() => Property;
protected override object? GetOldValue() => OldValue.GetValueOrDefault(AvaloniaProperty.UnsetValue); protected override object? GetOldValue() => OldValue.GetValueOrDefault(AvaloniaProperty.UnsetValue);

32
src/Avalonia.Base/Compatibility/CollectionCompatibilityExtensions.cs

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace System;
#if !NET6_0_OR_GREATER
internal static class CollectionCompatibilityExtensions
{
public static bool Remove<TKey, TValue>(
this Dictionary<TKey, TValue> o,
TKey key,
[MaybeNullWhen(false)] out TValue value)
where TKey : notnull
{
if (o.TryGetValue(key, out value))
return o.Remove(key);
return false;
}
public static bool TryAdd<TKey, TValue>(this Dictionary<TKey, TValue> o, TKey key, TValue value)
where TKey : notnull
{
if (!o.ContainsKey(key))
{
o.Add(key, value);
return true;
}
return false;
}
}
#endif

20
src/Avalonia.Base/Data/BindingNotification.cs

@ -241,26 +241,6 @@ namespace Avalonia.Data
_value = value; _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/> /// <inheritdoc/>
public override string ToString() public override string ToString()
{ {

5
src/Avalonia.Base/Data/BindingPriority.cs

@ -35,6 +35,11 @@ namespace Avalonia.Data
/// A style binding. /// A style binding.
/// </summary> /// </summary>
Style, Style,
/// <summary>
/// The value is inherited from an ancestor element.
/// </summary>
Inherited,
/// <summary> /// <summary>
/// The binding is uninitialized. /// The binding is uninitialized.

130
src/Avalonia.Base/Data/BindingValue.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -230,19 +231,77 @@ namespace Avalonia.Data
/// <summary> /// <summary>
/// Creates a <see cref="BindingValue{T}"/> from an object, handling the special values /// Creates a <see cref="BindingValue{T}"/> from an object, handling the special values
/// <see cref="AvaloniaProperty.UnsetValue"/> and <see cref="BindingOperations.DoNothing"/>. /// <see cref="AvaloniaProperty.UnsetValue"/>, <see cref="BindingOperations.DoNothing"/> and
/// <see cref="BindingNotification"/>.
/// </summary> /// </summary>
/// <param name="value">The untyped value.</param> /// <param name="value">The untyped value.</param>
/// <returns>The typed binding value.</returns> /// <returns>The typed binding value.</returns>
public static BindingValue<T> FromUntyped(object? value) public static BindingValue<T> FromUntyped(object? value)
{ {
return value switch return FromUntyped(value, typeof(T));
}
/// <summary>
/// Creates a <see cref="BindingValue{T}"/> from an object, handling the special values
/// <see cref="AvaloniaProperty.UnsetValue"/>, <see cref="BindingOperations.DoNothing"/> and
/// <see cref="BindingNotification"/>.
/// </summary>
/// <param name="value">The untyped value.</param>
/// <param name="targetType">The runtime target type.</param>
/// <returns>The typed binding value.</returns>
public static BindingValue<T> FromUntyped(object? value, Type targetType)
{
if (value == AvaloniaProperty.UnsetValue)
return Unset;
else if (value == BindingOperations.DoNothing)
return DoNothing;
var type = BindingValueType.Value;
T? v = default;
Exception? error = null;
List<Exception>? errors = null;
if (value is BindingNotification n)
{ {
UnsetValueType _ => Unset, error = n.Error;
DoNothingType _ => DoNothing, type = n.ErrorType switch
BindingNotification n => n.ToBindingValue().Cast<T>(), {
_ => new BindingValue<T>((T)value!) BindingErrorType.Error => BindingValueType.BindingError,
}; BindingErrorType.DataValidationError => BindingValueType.DataValidationError,
_ => BindingValueType.Value,
};
if (n.HasValue)
type |= BindingValueType.HasValue;
value = n.Value;
}
if ((type & BindingValueType.HasValue) != 0)
{
if (TypeUtilities.TryConvertImplicit(targetType, value, out var typed))
v = (T)typed!;
else
{
var e = new InvalidCastException(
$"Unable to convert object '{value ?? "(null)"}' " +
$"of type '{value?.GetType()}' to type '{targetType}'.");
if (error is null)
error = e;
else
{
errors ??= new List<Exception>() { error };
errors.Add(e);
}
type = BindingValueType.BindingError;
}
}
if (errors is not null)
error = new AggregateException(errors);
return new BindingValue<T>(type, v, error);
} }
/// <summary> /// <summary>
@ -372,61 +431,4 @@ namespace Avalonia.Data
} }
} }
} }
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."),
};
}
}
} }

9
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

@ -52,6 +52,7 @@ namespace Avalonia.Data.Core
private static readonly object UninitializedValue = new object(); private static readonly object UninitializedValue = new object();
private readonly ExpressionNode _node; private readonly ExpressionNode _node;
private object? _root; private object? _root;
private Func<object?>? _rootGetter;
private IDisposable? _rootSubscription; private IDisposable? _rootSubscription;
private WeakReference<object?>? _value; private WeakReference<object?>? _value;
private IReadOnlyList<ITransformNode>? _transformNodes; private IReadOnlyList<ITransformNode>? _transformNodes;
@ -109,11 +110,9 @@ namespace Avalonia.Data.Core
IObservable<Unit> update, IObservable<Unit> update,
string? description) string? description)
{ {
_ = rootGetter ?? throw new ArgumentNullException(nameof(rootGetter));
Description = description; Description = description;
_node = node ?? throw new ArgumentNullException(nameof(rootGetter)); _rootGetter = rootGetter ?? throw new ArgumentNullException(nameof(rootGetter));
_node.Target = new WeakReference<object?>(rootGetter()); _node = node ?? throw new ArgumentNullException(nameof(node));
_root = update.Select(x => rootGetter()); _root = update.Select(x => rootGetter());
} }
@ -263,6 +262,8 @@ namespace Avalonia.Data.Core
protected override void Initialize() protected override void Initialize()
{ {
_value = null; _value = null;
if (_rootGetter is not null)
_node.Target = new WeakReference<object?>(_rootGetter());
_node.Subscribe(ValueChanged); _node.Subscribe(ValueChanged);
StartRoot(); StartRoot();
} }

50
src/Avalonia.Base/DirectPropertyBase.cs

@ -1,5 +1,6 @@
using System; using System;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Styling; using Avalonia.Styling;
@ -105,6 +106,11 @@ namespace Avalonia
base.OverrideMetadata(type, metadata); base.OverrideMetadata(type, metadata);
} }
internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o)
{
throw new InvalidOperationException("Cannot create EffectiveValue for direct property.");
}
/// <inheritdoc/> /// <inheritdoc/>
internal override void RouteClearValue(AvaloniaObject o) internal override void RouteClearValue(AvaloniaObject o)
{ {
@ -117,7 +123,7 @@ namespace Avalonia
return o.GetValue<TValue>(this); return o.GetValue<TValue>(this);
} }
internal override object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority) internal override object? RouteGetBaseValue(AvaloniaObject o)
{ {
return o.GetValue<TValue>(this); return o.GetValue<TValue>(this);
} }
@ -146,44 +152,18 @@ namespace Avalonia
return null; return null;
} }
/// <inheritdoc/> /// <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 override IDisposable RouteBind( internal override IDisposable RouteBind(
AvaloniaObject o, AvaloniaObject o,
IObservable<BindingValue<object?>> source, IObservable<object?> source,
BindingPriority priority) BindingPriority priority)
{ {
var adapter = TypedBindingAdapter<TValue>.Create(o, this, source); return o.Bind(this, source);
return o.Bind<TValue>(this, adapter);
}
internal override void RouteInheritanceParentChanged(AvaloniaObject o, AvaloniaObject? oldParent)
{
throw new NotSupportedException("Direct properties do not support inheritance.");
}
internal override ISetterInstance CreateSetterInstance(IStyleable target, object? value)
{
if (value is IBinding binding)
{
return new PropertySetterBindingInstance<TValue>(
target,
this,
binding);
}
else if (value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(PropertyType))
{
return new PropertySetterTemplateInstance<TValue>(
target,
this,
template);
}
else
{
return new PropertySetterInstance<TValue>(
target,
this,
(TValue)value!);
}
} }
} }
} }

7
src/Avalonia.Base/IStyledPropertyAccessor.cs

@ -15,5 +15,12 @@ namespace Avalonia
/// The default value. /// The default value.
/// </returns> /// </returns>
object? GetDefaultValue(Type type); object? GetDefaultValue(Type type);
/// <summary>
/// Validates the specified property value.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>True if the value is valid, otherwise false.</returns>
bool ValidateValue(object? value);
} }
} }

4
src/Avalonia.Base/Properties/AssemblyInfo.cs

@ -29,6 +29,6 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Web.Blazor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Browser.Blazor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Web, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Browser, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

25
src/Avalonia.Base/PropertyStore/AvaloniaPropertyDictionaryPool.cs

@ -0,0 +1,25 @@
using System.Collections.Generic;
using Avalonia.Utilities;
namespace Avalonia.PropertyStore
{
internal static class AvaloniaPropertyDictionaryPool<TValue>
{
private const int MaxPoolSize = 4;
private static readonly Stack<AvaloniaPropertyDictionary<TValue>> _pool = new();
public static AvaloniaPropertyDictionary<TValue> Get()
{
return _pool.Count == 0 ? new() : _pool.Pop();
}
public static void Release(AvaloniaPropertyDictionary<TValue> dictionary)
{
if (_pool.Count < MaxPoolSize)
{
dictionary.Clear();
_pool.Push(dictionary);
}
}
}
}

154
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@ -1,154 +0,0 @@
using System;
using Avalonia.Data;
using Avalonia.Threading;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents an untyped interface to <see cref="BindingEntry{T}"/>.
/// </summary>
internal interface IBindingEntry : IBatchUpdate, IPriorityValueEntry, IDisposable
{
void Start(bool ignoreBatchUpdate);
}
/// <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 AvaloniaObject _owner;
private ValueOwner<T> _sink;
private IDisposable? _subscription;
private bool _isSubscribed;
private bool _batchUpdate;
private Optional<T> _value;
public BindingEntry(
AvaloniaObject owner,
StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source,
BindingPriority priority,
ValueOwner<T> sink)
{
_owner = owner;
Property = property;
Source = source;
Priority = priority;
_sink = sink;
}
public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; private set; }
public IObservable<BindingValue<T>> Source { get; }
Optional<object?> IValue.GetValue() => _value.ToObject();
public void BeginBatchUpdate() => _batchUpdate = true;
public void EndBatchUpdate()
{
_batchUpdate = false;
if (_sink.IsValueStore)
Start();
}
public Optional<T> GetValue(BindingPriority maxPriority)
{
return Priority >= maxPriority ? _value : Optional<T>.Empty;
}
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
OnCompleted();
}
public void OnCompleted()
{
var oldValue = _value;
_value = default;
Priority = BindingPriority.Unset;
_isSubscribed = false;
_sink.Completed(Property, this, oldValue);
}
public void OnError(Exception error)
{
throw new NotImplementedException("BindingEntry.OnError is not implemented", error);
}
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() => Start(false);
public void Start(bool ignoreBatchUpdate)
{
// We can't use _subscription to check whether we're subscribed because it won't be set
// until Subscribe has finished, which will be too late to prevent reentrancy. In addition
// don't re-subscribe to completed/disposed bindings (indicated by Unset priority).
if (!_isSubscribed &&
Priority != BindingPriority.Unset &&
(!_batchUpdate || ignoreBatchUpdate))
{
_isSubscribed = true;
_subscription = Source.Subscribe(this);
}
}
public void Reparent(PriorityValue<T> parent) => _sink = new(parent);
public void RaiseValueChanged(
AvaloniaObject owner,
AvaloniaProperty property,
Optional<object?> oldValue,
Optional<object?> newValue)
{
owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.Cast<T>(),
newValue.Cast<T>(),
Priority));
}
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(new AvaloniaPropertyChangedEventArgs<T>(_owner, Property, old, value, Priority));
}
}
}

148
src/Avalonia.Base/PropertyStore/BindingEntryBase.cs

@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
internal abstract class BindingEntryBase<TValue, TSource> : IValueEntry<TValue>,
IObserver<TSource>,
IObserver<BindingValue<TSource>>,
IDisposable
{
private static readonly IDisposable s_creating = Disposable.Empty;
private static readonly IDisposable s_creatingQuiet = Disposable.Create(() => { });
private IDisposable? _subscription;
private bool _hasValue;
private TValue? _value;
protected BindingEntryBase(
ValueFrame frame,
AvaloniaProperty property,
IObservable<BindingValue<TSource>> source)
{
Frame = frame;
Source = source;
Property = property;
}
protected BindingEntryBase(
ValueFrame frame,
AvaloniaProperty property,
IObservable<TSource> source)
{
Frame = frame;
Source = source;
Property = property;
}
public bool HasValue
{
get
{
Start(produceValue: false);
return _hasValue;
}
}
public bool IsSubscribed => _subscription is not null;
public AvaloniaProperty Property { get; }
AvaloniaProperty IValueEntry.Property => Property;
protected ValueFrame Frame { get; }
protected object Source { get; }
public void Dispose()
{
Unsubscribe();
BindingCompleted();
}
public TValue GetValue()
{
Start(produceValue: false);
if (!_hasValue)
throw new AvaloniaInternalException("The binding entry has no value.");
return _value!;
}
public void Start() => Start(true);
public void OnCompleted() => BindingCompleted();
public void OnError(Exception error) => BindingCompleted();
public void OnNext(TSource value) => SetValue(ConvertAndValidate(value));
public void OnNext(BindingValue<TSource> value) => SetValue(ConvertAndValidate(value));
public virtual void Unsubscribe()
{
_subscription?.Dispose();
_subscription = null;
}
object? IValueEntry.GetValue()
{
Start(produceValue: false);
if (!_hasValue)
throw new AvaloniaInternalException("The BindingEntry<T> has no value.");
return _value!;
}
protected abstract BindingValue<TValue> ConvertAndValidate(TSource value);
protected abstract BindingValue<TValue> ConvertAndValidate(BindingValue<TSource> value);
protected virtual void Start(bool produceValue)
{
if (_subscription is not null)
return;
_subscription = produceValue ? s_creating : s_creatingQuiet;
_subscription = Source switch
{
IObservable<BindingValue<TSource>> bv => bv.Subscribe(this),
IObservable<TSource> b => b.Subscribe(this),
_ => throw new AvaloniaInternalException("Unexpected binding source."),
};
}
private void ClearValue()
{
if (_hasValue)
{
_hasValue = false;
_value = default;
if (_subscription is not null)
Frame.Owner?.OnBindingValueCleared(Property, Frame.Priority);
}
}
private void SetValue(BindingValue<TValue> value)
{
if (Frame.Owner is null)
return;
LoggingUtils.LogIfNecessary(Frame.Owner.Owner, Property, value);
if (value.HasValue)
{
if (!_hasValue || !EqualityComparer<TValue>.Default.Equals(_value, value.Value))
{
_value = value.Value;
_hasValue = true;
if (_subscription is not null && _subscription != s_creatingQuiet)
Frame.Owner?.OnBindingValueChanged(this, Frame.Priority);
}
}
else if (value.Type != BindingValueType.DoNothing)
{
ClearValue();
if (_subscription is not null && _subscription != s_creatingQuiet)
Frame.Owner?.OnBindingValueCleared(Property, Frame.Priority);
}
}
private void BindingCompleted()
{
_subscription = null;
Frame.OnBindingCompleted(this);
}
}
}

82
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@ -1,82 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents an untyped interface to <see cref="ConstantValueEntry{T}"/>.
/// </summary>
internal interface IConstantValueEntry : IPriorityValueEntry, IDisposable
{
}
/// <summary>
/// Stores a value with a priority in a <see cref="ValueStore"/> or
/// <see cref="PriorityValue{T}"/>.
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IConstantValueEntry
{
private ValueOwner<T> _sink;
private Optional<T> _value;
public ConstantValueEntry(
StyledPropertyBase<T> property,
T value,
BindingPriority priority,
ValueOwner<T> sink)
{
Property = property;
_value = value;
Priority = priority;
_sink = sink;
}
public ConstantValueEntry(
StyledPropertyBase<T> property,
Optional<T> value,
BindingPriority priority,
ValueOwner<T> sink)
{
Property = property;
_value = value;
Priority = priority;
_sink = sink;
}
public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; private set; }
Optional<object?> IValue.GetValue() => _value.ToObject();
public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation)
{
return Priority >= maxPriority ? _value : Optional<T>.Empty;
}
public void Dispose()
{
var oldValue = _value;
_value = default;
Priority = BindingPriority.Unset;
_sink.Completed(Property, this, oldValue);
}
public void Reparent(PriorityValue<T> sink) => _sink = new(sink);
public void Start() { }
public void RaiseValueChanged(
AvaloniaObject owner,
AvaloniaProperty property,
Optional<object?> oldValue,
Optional<object?> newValue)
{
owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.Cast<T>(),
newValue.Cast<T>(),
Priority));
}
}
}

76
src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs

@ -0,0 +1,76 @@
using System;
using Avalonia.Data;
using Avalonia.Threading;
namespace Avalonia.PropertyStore
{
internal class DirectBindingObserver<T> : IObserver<T>,
IObserver<BindingValue<T>>,
IDisposable
{
private readonly ValueStore _owner;
private IDisposable? _subscription;
public DirectBindingObserver(ValueStore owner, DirectPropertyBase<T> property)
{
_owner = owner;
Property = property;
}
public DirectPropertyBase<T> Property { get;}
public void Start(IObservable<T> source)
{
_subscription = source.Subscribe(this);
}
public void Start(IObservable<BindingValue<T>> source)
{
_subscription = source.Subscribe(this);
}
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
_owner.OnLocalValueBindingCompleted(Property, this);
}
public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this);
public void OnError(Exception error) => OnCompleted();
public void OnNext(T value)
{
if (Dispatcher.UIThread.CheckAccess())
{
_owner.Owner.SetDirectValueUnchecked<T>(Property, value);
}
else
{
// To avoid allocating closure in the outer scope we need to capture variables
// locally. This allows us to skip most of the allocations when on UI thread.
var instance = _owner.Owner;
var property = Property;
var newValue = value;
Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, newValue));
}
}
public void OnNext(BindingValue<T> value)
{
if (Dispatcher.UIThread.CheckAccess())
{
_owner.Owner.SetDirectValueUnchecked<T>(Property, value);
}
else
{
// To avoid allocating closure in the outer scope we need to capture variables
// locally. This allows us to skip most of the allocations when on UI thread.
var instance = _owner.Owner;
var property = Property;
var newValue = value;
Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, newValue));
}
}
}
}

55
src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs

@ -0,0 +1,55 @@
using System;
using Avalonia.Data;
using Avalonia.Threading;
namespace Avalonia.PropertyStore
{
internal class DirectUntypedBindingObserver<T> : IObserver<object?>,
IDisposable
{
private readonly ValueStore _owner;
private IDisposable? _subscription;
public DirectUntypedBindingObserver(ValueStore owner, DirectPropertyBase<T> property)
{
_owner = owner;
Property = property;
}
public DirectPropertyBase<T> Property { get;}
public void Start(IObservable<object?> source)
{
_subscription = source.Subscribe(this);
}
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
_owner.OnLocalValueBindingCompleted(Property, this);
}
public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this);
public void OnError(Exception error) => OnCompleted();
public void OnNext(object? value)
{
var typed = BindingValue<T>.FromUntyped(value);
if (Dispatcher.UIThread.CheckAccess())
{
_owner.Owner.SetDirectValueUnchecked<T>(Property, typed);
}
else
{
// To avoid allocating closure in the outer scope we need to capture variables
// locally. This allows us to skip most of the allocations when on UI thread.
var instance = _owner.Owner;
var property = Property;
var newValue = value;
Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, typed));
}
}
}
}

168
src/Avalonia.Base/PropertyStore/EffectiveValue.cs

@ -0,0 +1,168 @@
using System.Diagnostics;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents the active value for a property in a <see cref="ValueStore"/>.
/// </summary>
/// <remarks>
/// This class is an abstract base for the generic <see cref="EffectiveValue{T}"/>.
/// </remarks>
internal abstract class EffectiveValue
{
private IValueEntry? _valueEntry;
private IValueEntry? _baseValueEntry;
/// <summary>
/// Gets the current effective value as a boxed value.
/// </summary>
public object? Value => GetBoxedValue();
/// <summary>
/// Gets the priority of the current effective value.
/// </summary>
public BindingPriority Priority { get; protected set; }
/// <summary>
/// Gets the priority of the current base value.
/// </summary>
public BindingPriority BasePriority { get; protected set; }
/// <summary>
/// Begins a reevaluation pass on the effective value.
/// </summary>
/// <param name="clearLocalValue">
/// Determines whether any current local value should be cleared.
/// </param>
/// <remarks>
/// This method resets the <see cref="Priority"/> and <see cref="BasePriority"/> properties
/// to Unset, pending reevaluation.
/// </remarks>
public void BeginReevaluation(bool clearLocalValue = false)
{
if (clearLocalValue || Priority != BindingPriority.LocalValue)
Priority = BindingPriority.Unset;
if (clearLocalValue || BasePriority != BindingPriority.LocalValue)
BasePriority = BindingPriority.Unset;
}
/// <summary>
/// Ends a reevaluation pass on the effective value.
/// </summary>
/// <remarks>
/// This method unsubscribes from any unused value entries.
/// </remarks>
public void EndReevaluation()
{
if (Priority == BindingPriority.Unset)
{
_valueEntry?.Unsubscribe();
_valueEntry = null;
}
if (BasePriority == BindingPriority.Unset)
{
_baseValueEntry?.Unsubscribe();
_baseValueEntry = null;
}
}
/// <summary>
/// Sets the value and base value for a non-LocalValue priority, raising
/// <see cref="AvaloniaObject.PropertyChanged"/> where necessary.
/// </summary>
/// <param name="owner">The associated value store.</param>
/// <param name="value">The new value of the property.</param>
/// <param name="priority">The priority of the new value.</param>
public abstract void SetAndRaise(
ValueStore owner,
IValueEntry value,
BindingPriority priority);
/// <summary>
/// Raises <see cref="AvaloniaObject.PropertyChanged"/> in response to an inherited value
/// change.
/// </summary>
/// <param name="owner">The owner object.</param>
/// <param name="property">The property being changed.</param>
/// <param name="oldValue">The old value of the property.</param>
/// <param name="newValue">The new value of the property.</param>
public abstract void RaiseInheritedValueChanged(
AvaloniaObject owner,
AvaloniaProperty property,
EffectiveValue? oldValue,
EffectiveValue? newValue);
/// <summary>
/// Removes the current animation value and reverts to the base value, raising
/// <see cref="AvaloniaObject.PropertyChanged"/> where necessary.
/// </summary>
/// <param name="owner">The associated value store.</param>
/// <param name="property">The property being changed.</param>
public abstract void RemoveAnimationAndRaise(ValueStore owner, AvaloniaProperty property);
/// <summary>
/// Coerces the property value.
/// </summary>
/// <param name="owner">The associated value store.</param>
/// <param name="property">The property to coerce.</param>
public abstract void CoerceValue(ValueStore owner, AvaloniaProperty property);
/// <summary>
/// Disposes the effective value, raising <see cref="AvaloniaObject.PropertyChanged"/>
/// where necessary.
/// </summary>
/// <param name="owner">The associated value store.</param>
/// <param name="property">The property being cleared.</param>
public abstract void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property);
protected abstract object? GetBoxedValue();
protected void UpdateValueEntry(IValueEntry? entry, BindingPriority priority)
{
Debug.Assert(priority != BindingPriority.LocalValue);
if (priority <= BindingPriority.Animation)
{
// If we've received an animation value and the current value is a non-animation
// value, then the current entry becomes our base entry.
if (Priority > BindingPriority.LocalValue && Priority < BindingPriority.Inherited)
{
Debug.Assert(_valueEntry is not null);
_baseValueEntry = _valueEntry;
_valueEntry = null;
}
if (_valueEntry != entry)
{
_valueEntry?.Unsubscribe();
_valueEntry = entry;
}
}
else if (Priority <= BindingPriority.Animation)
{
// We've received a non-animation value and have an active animation value, so the
// new entry becomes our base entry.
if (_baseValueEntry != entry)
{
_baseValueEntry?.Unsubscribe();
_baseValueEntry = entry;
}
}
else if (_valueEntry != entry)
{
// Both the current value and the new value are non-animation values, so the new
// entry replaces the existing entry.
_valueEntry?.Unsubscribe();
_valueEntry = entry;
}
}
protected void UnsubscribeValueEntries()
{
_valueEntry?.Unsubscribe();
_baseValueEntry?.Unsubscribe();
}
}
}

270
src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs

@ -0,0 +1,270 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents the active value for a property in a <see cref="ValueStore"/>.
/// </summary>
/// <remarks>
/// Stores the active value in an <see cref="AvaloniaObject"/>'s <see cref="ValueStore"/>
/// for a single property, when the value is not inherited or unset/default.
/// </remarks>
internal sealed class EffectiveValue<T> : EffectiveValue
{
private readonly StyledPropertyMetadata<T> _metadata;
private T? _baseValue;
private UncommonFields? _uncommon;
public EffectiveValue(AvaloniaObject owner, StyledPropertyBase<T> property)
{
Priority = BindingPriority.Unset;
BasePriority = BindingPriority.Unset;
_metadata = property.GetMetadata(owner.GetType());
var value = _metadata.DefaultValue;
if (property.HasCoercion && _metadata.CoerceValue is { } coerce)
{
_uncommon = new()
{
_coerce = coerce,
_uncoercedValue = value,
_uncoercedBaseValue = value,
};
Value = coerce(owner, value);
}
else
{
Value = value;
}
}
/// <summary>
/// Gets the current effective value.
/// </summary>
public new T Value { get; private set; }
public override void SetAndRaise(
ValueStore owner,
IValueEntry value,
BindingPriority priority)
{
Debug.Assert(priority != BindingPriority.LocalValue);
UpdateValueEntry(value, priority);
SetAndRaiseCore(owner, (StyledPropertyBase<T>)value.Property, GetValue(value), priority);
}
public void SetLocalValueAndRaise(
ValueStore owner,
StyledPropertyBase<T> property,
T value)
{
SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue);
}
public bool TryGetBaseValue([MaybeNullWhen(false)] out T value)
{
value = _baseValue!;
return BasePriority != BindingPriority.Unset;
}
public override void RaiseInheritedValueChanged(
AvaloniaObject owner,
AvaloniaProperty property,
EffectiveValue? oldValue,
EffectiveValue? newValue)
{
Debug.Assert(oldValue is not null || newValue is not null);
var p = (StyledPropertyBase<T>)property;
var o = oldValue is not null ? ((EffectiveValue<T>)oldValue).Value : _metadata.DefaultValue;
var n = newValue is not null ? ((EffectiveValue<T>)newValue).Value : _metadata.DefaultValue;
var priority = newValue is not null ? BindingPriority.Inherited : BindingPriority.Unset;
if (!EqualityComparer<T>.Default.Equals(o, n))
{
owner.RaisePropertyChanged(p, o, n, priority, true);
}
}
public override void RemoveAnimationAndRaise(ValueStore owner, AvaloniaProperty property)
{
Debug.Assert(Priority != BindingPriority.Animation);
Debug.Assert(BasePriority != BindingPriority.Unset);
UpdateValueEntry(null, BindingPriority.Animation);
SetAndRaiseCore(owner, (StyledPropertyBase<T>)property, _baseValue!, BasePriority);
}
public override void CoerceValue(ValueStore owner, AvaloniaProperty property)
{
if (_uncommon is null)
return;
SetAndRaiseCore(
owner,
(StyledPropertyBase<T>)property,
_uncommon._uncoercedValue!,
Priority,
_uncommon._uncoercedBaseValue!,
BasePriority);
}
public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property)
{
UnsubscribeValueEntries();
DisposeAndRaiseUnset(owner, (StyledPropertyBase<T>)property);
}
public void DisposeAndRaiseUnset(ValueStore owner, StyledPropertyBase<T> property)
{
BindingPriority priority;
T oldValue;
if (property.Inherits && owner.TryGetInheritedValue(property, out var i))
{
oldValue = ((EffectiveValue<T>)i).Value;
priority = BindingPriority.Inherited;
}
else
{
oldValue = _metadata.DefaultValue;
priority = BindingPriority.Unset;
}
if (!EqualityComparer<T>.Default.Equals(oldValue, Value))
{
owner.Owner.RaisePropertyChanged(property, Value, oldValue, priority, true);
if (property.Inherits)
owner.OnInheritedEffectiveValueDisposed(property, Value);
}
}
protected override object? GetBoxedValue() => Value;
private static T GetValue(IValueEntry entry)
{
if (entry is IValueEntry<T> typed)
return typed.GetValue();
else
return (T)entry.GetValue()!;
}
private void SetAndRaiseCore(
ValueStore owner,
StyledPropertyBase<T> property,
T value,
BindingPriority priority)
{
Debug.Assert(priority < BindingPriority.Inherited);
var oldValue = Value;
var valueChanged = false;
var baseValueChanged = false;
var v = value;
if (_uncommon?._coerce is { } coerce)
v = coerce(owner.Owner, value);
if (priority <= Priority)
{
valueChanged = !EqualityComparer<T>.Default.Equals(Value, v);
Value = v;
Priority = priority;
if (_uncommon is not null)
_uncommon._uncoercedValue = value;
}
if (priority <= BasePriority && priority >= BindingPriority.LocalValue)
{
baseValueChanged = !EqualityComparer<T>.Default.Equals(_baseValue, v);
_baseValue = v;
BasePriority = priority;
if (_uncommon is not null)
_uncommon._uncoercedBaseValue = value;
}
if (valueChanged)
{
using var notifying = PropertyNotifying.Start(owner.Owner, property);
owner.Owner.RaisePropertyChanged(property, oldValue, Value, Priority, true);
if (property.Inherits)
owner.OnInheritedEffectiveValueChanged(property, oldValue, this);
}
else if (baseValueChanged)
{
owner.Owner.RaisePropertyChanged(property, default, _baseValue!, BasePriority, false);
}
}
private void SetAndRaiseCore(
ValueStore owner,
StyledPropertyBase<T> property,
T value,
BindingPriority priority,
T baseValue,
BindingPriority basePriority)
{
Debug.Assert(priority < BindingPriority.Inherited);
Debug.Assert(basePriority > BindingPriority.Animation);
Debug.Assert(priority <= basePriority);
var oldValue = Value;
var valueChanged = false;
var baseValueChanged = false;
var v = value;
var bv = baseValue;
if (_uncommon?._coerce is { } coerce)
{
v = coerce(owner.Owner, value);
bv = coerce(owner.Owner, baseValue);
}
if (priority != BindingPriority.Unset && !EqualityComparer<T>.Default.Equals(Value, v))
{
Value = v;
valueChanged = true;
if (_uncommon is not null)
_uncommon._uncoercedValue = value;
}
if (priority != BindingPriority.Unset &&
(BasePriority == BindingPriority.Unset ||
!EqualityComparer<T>.Default.Equals(_baseValue, bv)))
{
_baseValue = v;
baseValueChanged = true;
if (_uncommon is not null)
_uncommon._uncoercedValue = baseValue;
}
Priority = priority;
BasePriority = basePriority;
if (valueChanged)
{
using var notifying = PropertyNotifying.Start(owner.Owner, property);
owner.Owner.RaisePropertyChanged(property, oldValue, Value, Priority, true);
if (property.Inherits)
owner.OnInheritedEffectiveValueChanged(property, oldValue, this);
}
if (baseValueChanged)
{
owner.Owner.RaisePropertyChanged(property, default, _baseValue!, BasePriority, false);
}
}
private class UncommonFields
{
public Func<IAvaloniaObject, T, T>? _coerce;
public T? _uncoercedValue;
public T? _uncoercedBaseValue;
}
}
}

8
src/Avalonia.Base/PropertyStore/IBatchUpdate.cs

@ -1,8 +0,0 @@
namespace Avalonia.PropertyStore
{
internal interface IBatchUpdate
{
void BeginBatchUpdate();
void EndBatchUpdate();
}
}

18
src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs

@ -1,18 +0,0 @@
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents an untyped interface to <see cref="IPriorityValueEntry{T}"/>.
/// </summary>
internal interface IPriorityValueEntry : IValue
{
}
/// <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>
{
void Reparent(PriorityValue<T> parent);
}
}

28
src/Avalonia.Base/PropertyStore/IValue.cs

@ -1,28 +0,0 @@
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents an untyped interface to <see cref="IValue{T}"/>.
/// </summary>
internal interface IValue
{
BindingPriority Priority { get; }
Optional<object?> GetValue();
void Start();
void RaiseValueChanged(
AvaloniaObject owner,
AvaloniaProperty property,
Optional<object?> oldValue,
Optional<object?> newValue);
}
/// <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
{
Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation);
}
}

30
src/Avalonia.Base/PropertyStore/IValueEntry.cs

@ -0,0 +1,30 @@
using System;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents an untyped value entry in a <see cref="ValueFrame"/>.
/// </summary>
internal interface IValueEntry
{
bool HasValue { get; }
/// <summary>
/// Gets the property that this value applies to.
/// </summary>
AvaloniaProperty Property { get; }
/// <summary>
/// Gets the value associated with the entry.
/// </summary>
/// <exception cref="AvaloniaInternalException">
/// The entry has no value.
/// </exception>
object? GetValue();
/// <summary>
/// Called when the value entry is removed from the value store.
/// </summary>
void Unsubscribe();
}
}

16
src/Avalonia.Base/PropertyStore/IValueEntry`1.cs

@ -0,0 +1,16 @@
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents a typed value entry in a <see cref="ValueFrame"/>.
/// </summary>
internal interface IValueEntry<T> : IValueEntry
{
/// <summary>
/// Gets the value associated with the entry.
/// </summary>
/// <exception cref="AvaloniaInternalException">
/// The entry has no value.
/// </exception>
new T GetValue();
}
}

31
src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs

@ -0,0 +1,31 @@
using System;
namespace Avalonia.PropertyStore
{
internal class ImmediateValueEntry<T> : IValueEntry<T>, IDisposable
{
private readonly ImmediateValueFrame _owner;
private readonly T _value;
public ImmediateValueEntry(
ImmediateValueFrame owner,
StyledPropertyBase<T> property,
T value)
{
_owner = owner;
_value = value;
Property = property;
}
public StyledPropertyBase<T> Property { get; }
public bool HasValue => true;
AvaloniaProperty IValueEntry.Property => Property;
public void Unsubscribe() { }
public void Dispose() => _owner.OnEntryDisposed(this);
object? IValueEntry.GetValue() => _value;
T IValueEntry<T>.GetValue() => _value;
}
}

63
src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs

@ -0,0 +1,63 @@
using System;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Holds values in a <see cref="ValueStore"/> set by one of the SetValue or AddBinding
/// overloads with non-LocalValue priority.
/// </summary>
internal class ImmediateValueFrame : ValueFrame
{
public ImmediateValueFrame(BindingPriority priority)
{
Priority = priority;
}
public TypedBindingEntry<T> AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source)
{
var e = new TypedBindingEntry<T>(this, property, source);
Add(e);
return e;
}
public TypedBindingEntry<T> AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<T> source)
{
var e = new TypedBindingEntry<T>(this, property, source);
Add(e);
return e;
}
public SourceUntypedBindingEntry<T> AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<object?> source)
{
var e = new SourceUntypedBindingEntry<T>(this, property, source);
Add(e);
return e;
}
public ImmediateValueEntry<T> AddValue<T>(StyledPropertyBase<T> property, T value)
{
var e = new ImmediateValueEntry<T>(this, property, value);
Add(e);
return e;
}
public void OnEntryDisposed(IValueEntry value)
{
Remove(value.Property);
Owner?.OnValueEntryRemoved(this, value.Property);
}
protected override bool GetIsActive(out bool hasChanged)
{
hasChanged = false;
return true;
}
}
}

59
src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs

@ -0,0 +1,59 @@
using System;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
internal class LocalValueBindingObserver<T> : IObserver<T>,
IObserver<BindingValue<T>>,
IDisposable
{
private readonly ValueStore _owner;
private IDisposable? _subscription;
public LocalValueBindingObserver(ValueStore owner, StyledPropertyBase<T> property)
{
_owner = owner;
Property = property;
}
public StyledPropertyBase<T> Property { get;}
public void Start(IObservable<T> source)
{
_subscription = source.Subscribe(this);
}
public void Start(IObservable<BindingValue<T>> source)
{
_subscription = source.Subscribe(this);
}
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
_owner.OnLocalValueBindingCompleted(Property, this);
}
public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this);
public void OnError(Exception error) => OnCompleted();
public void OnNext(T value)
{
if (Property.ValidateValue?.Invoke(value) != false)
_owner.SetValue(Property, value, BindingPriority.LocalValue);
else
_owner.ClearLocalValue(Property);
}
public void OnNext(BindingValue<T> value)
{
LoggingUtils.LogIfNecessary(_owner.Owner, Property, value);
if (value.HasValue)
_owner.SetValue(Property, value.Value, BindingPriority.LocalValue);
else if (value.Type != BindingValueType.DataValidationError)
_owner.ClearLocalValue(Property);
}
}
}

41
src/Avalonia.Base/PropertyStore/LocalValueEntry.cs

@ -1,41 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
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 BindingPriority Priority => BindingPriority.LocalValue;
Optional<object?> IValue.GetValue() => new Optional<object?>(_value);
public Optional<T> GetValue(BindingPriority maxPriority)
{
return BindingPriority.LocalValue >= maxPriority ? _value : Optional<T>.Empty;
}
public void SetValue(T value) => _value = value;
public void Start() { }
public void RaiseValueChanged(
AvaloniaObject owner,
AvaloniaProperty property,
Optional<object?> oldValue,
Optional<object?> newValue)
{
owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.Cast<T>(),
newValue.Cast<T>(),
BindingPriority.LocalValue));
}
}
}

62
src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs

@ -0,0 +1,62 @@
using System;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
internal class LocalValueUntypedBindingObserver<T> : IObserver<object?>,
IDisposable
{
private readonly ValueStore _owner;
private IDisposable? _subscription;
public LocalValueUntypedBindingObserver(ValueStore owner, StyledPropertyBase<T> property)
{
_owner = owner;
Property = property;
}
public StyledPropertyBase<T> Property { get; }
public void Start(IObservable<object?> source)
{
_subscription = source.Subscribe(this);
}
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
_owner.OnLocalValueBindingCompleted(Property, this);
}
public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this);
public void OnError(Exception error) => OnCompleted();
public void OnNext(object? value)
{
if (value is BindingNotification n)
{
value = n.Value;
LoggingUtils.LogIfNecessary(_owner.Owner, Property, n);
}
if (value == AvaloniaProperty.UnsetValue)
{
_owner.ClearLocalValue(Property);
}
else if (value == BindingOperations.DoNothing)
{
// Do nothing!
}
else if (UntypedValueUtils.TryConvertAndValidate(Property, value, out var typedValue))
{
_owner.SetValue(Property, typedValue, BindingPriority.LocalValue);
}
else
{
_owner.ClearLocalValue(Property);
LoggingUtils.LogInvalidValue(_owner.Owner, Property, typeof(T), value);
}
}
}
}

82
src/Avalonia.Base/PropertyStore/LoggingUtils.cs

@ -0,0 +1,82 @@
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
internal static class LoggingUtils
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogIfNecessary(
AvaloniaObject owner,
AvaloniaProperty property,
BindingNotification value)
{
if (value.ErrorType != BindingErrorType.None)
Log(owner, property, value.Error!);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogIfNecessary<T>(
AvaloniaObject owner,
AvaloniaProperty property,
BindingValue<T> value)
{
if (value.HasError)
Log(owner, property, value.Error!);
}
public static void LogInvalidValue(
AvaloniaObject owner,
AvaloniaProperty property,
Type expectedType,
object? value)
{
if (value is not null)
{
owner.GetBindingWarningLogger(property, null)?.Log(
owner,
"Error in binding to {Target}.{Property}: expected {ExpectedType}, got {Value} ({ValueType})",
owner,
property,
expectedType,
value,
value.GetType());
}
else
{
owner.GetBindingWarningLogger(property, null)?.Log(
owner,
"Error in binding to {Target}.{Property}: expected {ExpectedType}, got null",
owner,
property,
expectedType);
}
}
private static void Log(
AvaloniaObject owner,
AvaloniaProperty property,
Exception e)
{
if (e is TargetInvocationException t)
e = t.InnerException!;
if (e is AggregateException a)
{
foreach (var i in a.InnerExceptions)
Log(owner, property, i);
}
else
{
owner.GetBindingWarningLogger(property, e)?.Log(
owner,
"Error in binding to {Target}.{Property}: {Message}",
owner,
property,
e.Message);
}
}
}
}

326
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@ -1,326 +0,0 @@
using System;
using System.Collections.Generic;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents an untyped interface to <see cref="PriorityValue{T}"/>.
/// </summary>
interface IPriorityValue : IValue
{
void UpdateEffectiveValue();
}
/// <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> : IPriorityValue, IValue<T>, IBatchUpdate
{
private readonly AvaloniaObject _owner;
private readonly ValueStore _store;
private readonly List<IPriorityValueEntry<T>> _entries = new List<IPriorityValueEntry<T>>();
private readonly Func<IAvaloniaObject, T, T>? _coerceValue;
private Optional<T> _localValue;
private Optional<T> _value;
private bool _isCalculatingValue;
private bool _batchUpdate;
public PriorityValue(
AvaloniaObject owner,
StyledPropertyBase<T> property,
ValueStore store)
{
_owner = owner;
Property = property;
_store = store;
if (property.HasCoercion)
{
var metadata = property.GetMetadata(owner.GetType());
_coerceValue = metadata.CoerceValue;
}
}
public PriorityValue(
AvaloniaObject owner,
StyledPropertyBase<T> property,
ValueStore store,
IPriorityValueEntry<T> existing)
: this(owner, property, store)
{
existing.Reparent(this);
_entries.Add(existing);
if (existing is IBindingEntry binding &&
existing.Priority == BindingPriority.LocalValue)
{
// Bit of a special case here: if we have a local value binding that is being
// promoted to a priority value we need to make sure the binding is subscribed
// even if we've got a batch operation in progress because otherwise we don't know
// whether the binding or a subsequent SetValue with local priority will win. A
// notification won't be sent during batch update anyway because it will be
// caught and stored for later by the ValueStore.
binding.Start(ignoreBatchUpdate: true);
}
var v = existing.GetValue();
if (v.HasValue)
{
_value = v;
Priority = existing.Priority;
}
}
public PriorityValue(
AvaloniaObject owner,
StyledPropertyBase<T> property,
ValueStore sink,
LocalValueEntry<T> existing)
: this(owner, property, sink)
{
_value = _localValue = existing.GetValue(BindingPriority.LocalValue);
Priority = BindingPriority.LocalValue;
}
public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; private set; } = BindingPriority.Unset;
public IReadOnlyList<IPriorityValueEntry<T>> Entries => _entries;
Optional<object?> IValue.GetValue() => _value.ToObject();
public void BeginBatchUpdate()
{
_batchUpdate = true;
foreach (var entry in _entries)
{
(entry as IBatchUpdate)?.BeginBatchUpdate();
}
}
public void EndBatchUpdate()
{
_batchUpdate = false;
foreach (var entry in _entries)
{
(entry as IBatchUpdate)?.EndBatchUpdate();
}
UpdateEffectiveValue(null);
}
public void ClearLocalValue()
{
_localValue = default;
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
Property,
default,
default,
BindingPriority.LocalValue));
}
public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation)
{
if (Priority == BindingPriority.Unset)
{
return default;
}
if (Priority >= maxPriority)
{
return _value;
}
return CalculateValue(maxPriority).Item1;
}
public IDisposable? SetValue(T value, BindingPriority priority)
{
IDisposable? result = null;
if (priority == BindingPriority.LocalValue)
{
_localValue = value;
}
else
{
var insert = FindInsertPoint(priority);
var entry = new ConstantValueEntry<T>(Property, value, priority, new ValueOwner<T>(this));
_entries.Insert(insert, entry);
result = entry;
}
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
Property,
default,
value,
priority));
return result;
}
public BindingEntry<T> AddBinding(IObservable<BindingValue<T>> source, BindingPriority priority)
{
var binding = new BindingEntry<T>(_owner, Property, source, priority, new(this));
var insert = FindInsertPoint(binding.Priority);
_entries.Insert(insert, binding);
if (_batchUpdate)
{
binding.BeginBatchUpdate();
if (priority == BindingPriority.LocalValue)
{
binding.Start(ignoreBatchUpdate: true);
}
}
return binding;
}
public void UpdateEffectiveValue() => UpdateEffectiveValue(null);
public void Start() => UpdateEffectiveValue(null);
public void RaiseValueChanged(
AvaloniaObject owner,
AvaloniaProperty property,
Optional<object?> oldValue,
Optional<object?> newValue)
{
owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.Cast<T>(),
newValue.Cast<T>(),
Priority));
}
public void ValueChanged<TValue>(AvaloniaPropertyChangedEventArgs<TValue> change)
{
if (change.Priority == BindingPriority.LocalValue)
{
_localValue = default;
}
if (!_isCalculatingValue && change is AvaloniaPropertyChangedEventArgs<T> c)
{
UpdateEffectiveValue(c);
}
}
public void Completed(IPriorityValueEntry entry, Optional<T> oldValue)
{
_entries.Remove((IPriorityValueEntry<T>)entry);
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
Property,
oldValue,
default,
entry.Priority));
}
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;
}
public (Optional<T>, BindingPriority) CalculateValue(BindingPriority maxPriority)
{
_isCalculatingValue = true;
try
{
for (var i = _entries.Count - 1; i >= 0; --i)
{
var entry = _entries[i];
if (entry.Priority < maxPriority)
{
continue;
}
entry.Start();
if (entry.Priority >= BindingPriority.LocalValue &&
maxPriority <= BindingPriority.LocalValue &&
_localValue.HasValue)
{
return (_localValue, BindingPriority.LocalValue);
}
var entryValue = entry.GetValue();
if (entryValue.HasValue)
{
return (entryValue, entry.Priority);
}
}
if (maxPriority <= BindingPriority.LocalValue && _localValue.HasValue)
{
return (_localValue, BindingPriority.LocalValue);
}
return (default, BindingPriority.Unset);
}
finally
{
_isCalculatingValue = false;
}
}
private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs<T>? change)
{
var (value, priority) = CalculateValue(BindingPriority.Animation);
if (value.HasValue && _coerceValue != null)
{
value = _coerceValue(_owner, value.Value);
}
Priority = priority;
if (value != _value)
{
var old = _value;
_value = value;
_store.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
Property,
old,
value,
Priority));
}
else if (change is object)
{
change.MarkNonEffectiveValue();
change.SetOldValue(default);
_store.ValueChanged(change);
}
}
}
}

35
src/Avalonia.Base/PropertyStore/PropertyNotifying.cs

@ -0,0 +1,35 @@
using System;
using System.Diagnostics;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Raises <see cref="AvaloniaProperty.Notifying"/> where necessary.
/// </summary>
/// <remarks>
/// Uses the disposable pattern to ensure that the closing Notifying call is made even in the
/// presence of exceptions.
/// </remarks>
internal readonly struct PropertyNotifying : IDisposable
{
private readonly AvaloniaObject _owner;
private readonly AvaloniaProperty _property;
private PropertyNotifying(AvaloniaObject owner, AvaloniaProperty property)
{
Debug.Assert(property.Notifying is not null);
_owner = owner;
_property = property;
_property.Notifying!(owner, true);
}
public void Dispose() => _property.Notifying!(_owner, false);
public static PropertyNotifying? Start(AvaloniaObject owner, AvaloniaProperty property)
{
if (property.Notifying is null)
return null;
return new PropertyNotifying(owner, property);
}
}
}

35
src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs

@ -0,0 +1,35 @@
using System;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// An <see cref="IValueEntry"/> that holds a binding whose source observable is untyped and
/// target property is typed.
/// </summary>
internal sealed class SourceUntypedBindingEntry<TTarget> : BindingEntryBase<TTarget, object?>
{
private readonly Func<TTarget, bool>? _validate;
public SourceUntypedBindingEntry(
ValueFrame frame,
StyledPropertyBase<TTarget> property,
IObservable<object?> source)
: base(frame, property, source)
{
_validate = property.ValidateValue;
}
public new StyledPropertyBase<TTarget> Property => (StyledPropertyBase<TTarget>)base.Property;
protected override BindingValue<TTarget> ConvertAndValidate(object? value)
{
return UntypedValueUtils.ConvertAndValidate(value, Property.PropertyType, _validate);
}
protected override BindingValue<TTarget> ConvertAndValidate(BindingValue<object?> value)
{
throw new NotSupportedException();
}
}
}

52
src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs

@ -0,0 +1,52 @@
using System;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// An <see cref="IValueEntry"/> that holds a binding whose source observable and target
/// property are both typed.
/// </summary>
internal sealed class TypedBindingEntry<T> : BindingEntryBase<T, T>
{
public TypedBindingEntry(
ValueFrame frame,
StyledPropertyBase<T> property,
IObservable<T> source)
: base(frame, property, source)
{
}
public TypedBindingEntry(
ValueFrame frame,
StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source)
: base(frame, property, source)
{
}
public new StyledPropertyBase<T> Property => (StyledPropertyBase<T>)base.Property;
protected override BindingValue<T> ConvertAndValidate(T value)
{
if (Property.ValidateValue?.Invoke(value) == false)
{
return BindingValue<T>.BindingError(
new InvalidCastException($"'{value}' is not a valid value."));
}
return value;
}
protected override BindingValue<T> ConvertAndValidate(BindingValue<T> value)
{
if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false)
{
return BindingValue<T>.BindingError(
new InvalidCastException($"'{value.Value}' is not a valid value."));
}
return value;
}
}
}

33
src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs

@ -0,0 +1,33 @@
using System;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// An <see cref="IValueEntry"/> that holds a binding whose source observable and target
/// property are both untyped.
/// </summary>
internal class UntypedBindingEntry : BindingEntryBase<object?, object?>
{
private readonly Func<object?, bool>? _validate;
public UntypedBindingEntry(
ValueFrame frame,
AvaloniaProperty property,
IObservable<object?> source)
: base(frame, property, source)
{
_validate = ((IStyledPropertyAccessor)property).ValidateValue;
}
protected override BindingValue<object?> ConvertAndValidate(object? value)
{
return UntypedValueUtils.ConvertAndValidate(value, Property.PropertyType, _validate);
}
protected override BindingValue<object?> ConvertAndValidate(BindingValue<object?> value)
{
throw new NotSupportedException();
}
}
}

55
src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs

@ -0,0 +1,55 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.Utilities;
namespace Avalonia.PropertyStore
{
internal static class UntypedValueUtils
{
public static BindingValue<T> ConvertAndValidate<T>(
object? value,
Type targetType,
Func<T, bool>? validate)
{
var v = BindingValue<T>.FromUntyped(value, targetType);
if (v.HasValue && validate?.Invoke(v.Value) == false)
{
return BindingValue<T>.BindingError(
new InvalidCastException($"'{v.Value}' is not a valid value."));
}
return v;
}
public static bool TryConvertAndValidate(
AvaloniaProperty property,
object? value,
out object? result)
{
if (TypeUtilities.TryConvertImplicit(property.PropertyType, value, out result))
return ((IStyledPropertyAccessor)property).ValidateValue(result);
result = default;
return false;
}
public static bool TryConvertAndValidate<T>(
StyledPropertyBase<T> property,
object? value,
[MaybeNullWhen(false)] out T result)
{
if (TypeUtilities.TryConvertImplicit(typeof(T), value, out var v))
{
result = (T)v!;
if (property.ValidateValue?.Invoke(result) != false)
return true;
}
result = default;
return false;
}
}
}

103
src/Avalonia.Base/PropertyStore/ValueFrame.cs

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.Utilities;
using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot;
namespace Avalonia.PropertyStore
{
internal abstract class ValueFrame
{
private List<IValueEntry>? _entries;
private AvaloniaPropertyDictionary<IValueEntry> _index;
private ValueStore? _owner;
private bool _isShared;
public int EntryCount => _index.Count;
public bool IsActive => GetIsActive(out _);
public ValueStore? Owner => !_isShared ? _owner :
throw new AvaloniaInternalException("Cannot get owner for shared ValueFrame");
public BindingPriority Priority { get; protected set; }
public bool Contains(AvaloniaProperty property) => _index.ContainsKey(property);
public IValueEntry GetEntry(int index) => _entries?[index] ?? _index[0];
public void SetOwner(ValueStore? owner)
{
if (_owner is not null && owner is not null)
throw new AvaloniaInternalException("ValueFrame already has an owner.");
if (!_isShared)
_owner = owner;
}
public bool TryGetEntryIfActive(
AvaloniaProperty property,
[NotNullWhen(true)] out IValueEntry? entry,
out bool activeChanged)
{
if (_index.TryGetValue(property, out entry))
return GetIsActive(out activeChanged);
activeChanged = false;
return false;
}
public void OnBindingCompleted(IValueEntry binding)
{
var property = binding.Property;
Remove(property);
Owner?.OnValueEntryRemoved(this, property);
}
public virtual void Dispose()
{
for (var i = 0; i < _index.Count; ++i)
_index[i].Unsubscribe();
}
protected abstract bool GetIsActive(out bool hasChanged);
protected void MakeShared()
{
_isShared = true;
_owner = null;
}
protected void Add(IValueEntry value)
{
Debug.Assert(!value.Property.IsDirect);
if (_entries is null && _index.Count == 1)
{
_entries = new();
_entries.Add(_index[0]);
}
_index.Add(value.Property, value);
_entries?.Add(value);
}
protected void Remove(AvaloniaProperty property)
{
Debug.Assert(!property.IsDirect);
if (_entries is not null)
{
var count = _entries.Count;
for (var i = 0; i < count; ++i)
{
if (_entries[i].Property == property)
{
_entries.RemoveAt(i);
break;
}
}
}
_index.Remove(property);
}
}
}

45
src/Avalonia.Base/PropertyStore/ValueOwner.cs

@ -1,45 +0,0 @@
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents a union type of <see cref="ValueStore"/> and <see cref="PriorityValue{T}"/>,
/// which are the valid owners of a value store <see cref="IValue"/>.
/// </summary>
/// <typeparam name="T">The value type.</typeparam>
internal readonly struct ValueOwner<T>
{
private readonly ValueStore? _store;
private readonly PriorityValue<T>? _priorityValue;
public ValueOwner(ValueStore o)
{
_store = o;
_priorityValue = null;
}
public ValueOwner(PriorityValue<T> v)
{
_store = null;
_priorityValue = v;
}
public bool IsValueStore => _store is not null;
public void Completed(StyledPropertyBase<T> property, IPriorityValueEntry entry, Optional<T> oldValue)
{
if (_store is not null)
_store?.Completed(property, entry, oldValue);
else
_priorityValue!.Completed(entry, oldValue);
}
public void ValueChanged(AvaloniaPropertyChangedEventArgs<T> e)
{
if (_store is not null)
_store?.ValueChanged(e);
else
_priorityValue!.ValueChanged(e);
}
}
}

962
src/Avalonia.Base/PropertyStore/ValueStore.cs

@ -0,0 +1,962 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.Diagnostics;
using Avalonia.Logging;
using Avalonia.Utilities;
namespace Avalonia.PropertyStore
{
internal class ValueStore
{
private readonly List<ValueFrame> _frames = new();
private Dictionary<int, IDisposable>? _localValueBindings;
private AvaloniaPropertyDictionary<EffectiveValue> _effectiveValues;
private int _inheritedValueCount;
private int _isEvaluating;
private int _frameGeneration;
private int _styling;
public ValueStore(AvaloniaObject owner) => Owner = owner;
public AvaloniaObject Owner { get; }
public ValueStore? InheritanceAncestor { get; private set; }
public bool IsEvaluating => _isEvaluating > 0;
public IReadOnlyList<ValueFrame> Frames => _frames;
public void BeginStyling() => ++_styling;
public void EndStyling()
{
if (--_styling == 0)
ReevaluateEffectiveValues();
}
public void AddFrame(ValueFrame style)
{
InsertFrame(style);
ReevaluateEffectiveValues();
}
public IDisposable AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source,
BindingPriority priority)
{
if (priority == BindingPriority.LocalValue)
{
var observer = new LocalValueBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;
observer.Start(source);
return observer;
}
else
{
var effective = GetEffectiveValue(property);
var frame = GetOrCreateImmediateValueFrame(property, priority, out _);
var result = frame.AddBinding(property, source);
if (effective is null || priority <= effective.Priority)
result.Start();
return result;
}
}
public IDisposable AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<T> source,
BindingPriority priority)
{
if (priority == BindingPriority.LocalValue)
{
var observer = new LocalValueBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;
observer.Start(source);
return observer;
}
else
{
var effective = GetEffectiveValue(property);
var frame = GetOrCreateImmediateValueFrame(property, priority, out _);
var result = frame.AddBinding(property, source);
if (effective is null || priority <= effective.Priority)
result.Start();
return result;
}
}
public IDisposable AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<object?> source,
BindingPriority priority)
{
if (priority == BindingPriority.LocalValue)
{
var observer = new LocalValueUntypedBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;
observer.Start(source);
return observer;
}
else
{
var effective = GetEffectiveValue(property);
var frame = GetOrCreateImmediateValueFrame(property, priority, out _);
var result = frame.AddBinding(property, source);
if (effective is null || priority <= effective.Priority)
result.Start();
return result;
}
}
public IDisposable AddBinding<T>(DirectPropertyBase<T> property, IObservable<BindingValue<T>> source)
{
var observer = new DirectBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;
observer.Start(source);
return observer;
}
public IDisposable AddBinding<T>(DirectPropertyBase<T> property, IObservable<T> source)
{
var observer = new DirectBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;
observer.Start(source);
return observer;
}
public IDisposable AddBinding<T>(DirectPropertyBase<T> property, IObservable<object?> source)
{
var observer = new DirectUntypedBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;
observer.Start(source);
return observer;
}
public void ClearLocalValue(AvaloniaProperty property)
{
if (TryGetEffectiveValue(property, out var effective) &&
effective.Priority == BindingPriority.LocalValue)
{
ReevaluateEffectiveValue(property, effective, ignoreLocalValue: true);
}
}
public IDisposable? SetValue<T>(StyledPropertyBase<T> property, T value, BindingPriority priority)
{
if (property.ValidateValue?.Invoke(value) == false)
{
throw new ArgumentException($"{value} is not a valid value for '{property.Name}.");
}
if (priority != BindingPriority.LocalValue)
{
var frame = GetOrCreateImmediateValueFrame(property, priority, out _);
var result = frame.AddValue(property, value);
if (TryGetEffectiveValue(property, out var existing))
{
var effective = (EffectiveValue<T>)existing;
effective.SetAndRaise(this, result, priority);
}
else
{
var effectiveValue = new EffectiveValue<T>(Owner, property);
AddEffectiveValue(property, effectiveValue);
effectiveValue.SetAndRaise(this, result, priority);
}
return result;
}
else
{
if (TryGetEffectiveValue(property, out var existing))
{
var effective = (EffectiveValue<T>)existing;
effective.SetLocalValueAndRaise(this, property, value);
}
else
{
var effectiveValue = new EffectiveValue<T>(Owner, property);
AddEffectiveValue(property, effectiveValue);
effectiveValue.SetLocalValueAndRaise(this, property, value);
}
return null;
}
}
public object? GetValue(AvaloniaProperty property)
{
if (_effectiveValues.TryGetValue(property, out var v))
return v.Value;
if (property.Inherits && TryGetInheritedValue(property, out v))
return v.Value;
return GetDefaultValue(property);
}
public T GetValue<T>(StyledPropertyBase<T> property)
{
if (_effectiveValues.TryGetValue(property, out var v))
return ((EffectiveValue<T>)v).Value;
if (property.Inherits && TryGetInheritedValue(property, out v))
return ((EffectiveValue<T>)v).Value;
return property.GetDefaultValue(Owner.GetType());
}
public bool IsAnimating(AvaloniaProperty property)
{
if (_effectiveValues.TryGetValue(property, out var v))
return v.Priority <= BindingPriority.Animation;
return false;
}
public bool IsSet(AvaloniaProperty property)
{
if (_effectiveValues.TryGetValue(property, out var v))
return v.Priority < BindingPriority.Inherited;
return false;
}
public void CoerceValue(AvaloniaProperty property)
{
if (_effectiveValues.TryGetValue(property, out var v))
v.CoerceValue(this, property);
}
public Optional<T> GetBaseValue<T>(StyledPropertyBase<T> property)
{
if (TryGetEffectiveValue(property, out var v) &&
((EffectiveValue<T>)v).TryGetBaseValue(out var baseValue))
{
return baseValue;
}
return default;
}
public bool TryGetInheritedValue(
AvaloniaProperty property,
[NotNullWhen(true)] out EffectiveValue? result)
{
Debug.Assert(property.Inherits);
var i = InheritanceAncestor;
while (i is not null)
{
if (i.TryGetEffectiveValue(property, out result))
return true;
i = i.InheritanceAncestor;
}
result = null;
return false;
}
public void SetInheritanceParent(AvaloniaObject? newParent)
{
var values = AvaloniaPropertyDictionaryPool<OldNewValue>.Get();
var oldAncestor = InheritanceAncestor;
var newAncestor = newParent?.GetValueStore();
if (newAncestor?._inheritedValueCount == 0)
newAncestor = newAncestor.InheritanceAncestor;
// The old and new inheritance ancestors are the same, nothing to do here.
if (oldAncestor == newAncestor)
return;
// First get the old values from the old inheritance ancestor.
var f = oldAncestor;
while (f is not null)
{
var count = f._effectiveValues.Count;
for (var i = 0; i < count; ++i)
{
f._effectiveValues.GetKeyValue(i, out var key, out var value);
if (key.Inherits)
values.TryAdd(key, new(value));
}
f = f.InheritanceAncestor;
}
f = newAncestor;
// Get the new values from the new inheritance ancestor.
while (f is not null)
{
var count = f._effectiveValues.Count;
for (var i = 0; i < count; ++i)
{
f._effectiveValues.GetKeyValue(i, out var key, out var value);
if (!key.Inherits)
continue;
if (values.TryGetValue(key, out var existing))
{
if (existing.NewValue is null)
values[key] = existing.WithNewValue(value);
}
else
{
values.Add(key, new(null, value));
}
}
f = f.InheritanceAncestor;
}
OnInheritanceAncestorChanged(newAncestor);
// Raise PropertyChanged events where necessary on this object and inheritance children.
{
var count = values.Count;
for (var i = 0; i < count; ++i)
{
values.GetKeyValue(i, out var key, out var v);
var oldValue = v.OldValue;
var newValue = v.NewValue;
if (oldValue != newValue)
InheritedValueChanged(key, oldValue, newValue);
}
}
AvaloniaPropertyDictionaryPool<OldNewValue>.Release(values);
}
/// <summary>
/// Called by non-LocalValue binding entries to re-evaluate the effective value when the
/// binding produces a new value.
/// </summary>
/// <param name="entry">The binding entry.</param>
/// <param name="priority">The priority of binding which produced a new value.</param>
public void OnBindingValueChanged(
IValueEntry entry,
BindingPriority priority)
{
Debug.Assert(priority != BindingPriority.LocalValue);
var property = entry.Property;
if (TryGetEffectiveValue(property, out var existing))
{
if (priority <= existing.BasePriority)
ReevaluateEffectiveValue(property, existing);
}
else
{
AddEffectiveValueAndRaise(property, entry, priority);
}
}
/// <summary>
/// Called by non-LocalValue binding entries to re-evaluate the effective value when the
/// binding produces an unset value.
/// </summary>
/// <param name="property">The bound property.</param>
/// <param name="priority">The priority of binding which produced a new value.</param>
public void OnBindingValueCleared(AvaloniaProperty property, BindingPriority priority)
{
Debug.Assert(priority != BindingPriority.LocalValue);
if (TryGetEffectiveValue(property, out var existing))
{
if (priority <= existing.Priority)
ReevaluateEffectiveValue(property, existing);
}
}
/// <summary>
/// Called by a <see cref="ValueFrame"/> when its <see cref="ValueFrame.IsActive"/>
/// state changes.
/// </summary>
/// <param name="frame">The frame which produced the change.</param>
public void OnFrameActivationChanged(ValueFrame frame)
{
if (frame.EntryCount == 0)
return;
else if (frame.EntryCount == 1)
{
var property = frame.GetEntry(0).Property;
_effectiveValues.TryGetValue(property, out var current);
ReevaluateEffectiveValue(property, current);
}
else
ReevaluateEffectiveValues();
}
/// <summary>
/// Called by the parent value store when its inheritance ancestor changes.
/// </summary>
/// <param name="ancestor">The new inheritance ancestor.</param>
public void OnInheritanceAncestorChanged(ValueStore? ancestor)
{
if (ancestor != this)
{
InheritanceAncestor = ancestor;
if (_inheritedValueCount > 0)
return;
}
var children = Owner.GetInheritanceChildren();
if (children is null)
return;
var count = children.Count;
for (var i = 0; i < count; ++i)
{
children[i].GetValueStore().OnInheritanceAncestorChanged(ancestor);
}
}
/// <summary>
/// Called by <see cref="EffectiveValue{T}"/> when an property with inheritance enabled
/// changes its value on this value store.
/// </summary>
/// <param name="property">The property whose value changed.</param>
/// <param name="oldValue">The old value of the property.</param>
/// <param name="value">The effective value instance.</param>
public void OnInheritedEffectiveValueChanged<T>(
StyledPropertyBase<T> property,
T oldValue,
EffectiveValue<T> value)
{
Debug.Assert(property.Inherits);
var children = Owner.GetInheritanceChildren();
if (children is null)
return;
var count = children.Count;
for (var i = 0; i < count; ++i)
{
children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, value.Value);
}
}
/// <summary>
/// Called by <see cref="EffectiveValue{T}"/> when an property with inheritance enabled
/// is removed from the effective values.
/// </summary>
/// <param name="property">The property whose value changed.</param>
/// <param name="oldValue">The old value of the property.</param>
public void OnInheritedEffectiveValueDisposed<T>(StyledPropertyBase<T> property, T oldValue)
{
Debug.Assert(property.Inherits);
var children = Owner.GetInheritanceChildren();
if (children is not null)
{
var defaultValue = property.GetDefaultValue(Owner.GetType());
var count = children.Count;
for (var i = 0; i < count; ++i)
{
children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, defaultValue);
}
}
}
/// <summary>
/// Called when a <see cref="LocalValueBindingObserver{T}"/> or
/// <see cref="DirectBindingObserver{T}"/> completes.
/// </summary>
/// <param name="property">The previously bound property.</param>
/// <param name="observer">The observer.</param>
public void OnLocalValueBindingCompleted(AvaloniaProperty property, IDisposable observer)
{
if (_localValueBindings is not null &&
_localValueBindings.TryGetValue(property.Id, out var existing))
{
if (existing == observer)
{
_localValueBindings?.Remove(property.Id);
ClearLocalValue(property);
}
}
}
/// <summary>
/// Called when an inherited property changes on the value store of the inheritance ancestor.
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
/// <param name="property">The property.</param>
/// <param name="oldValue">The old value of the property.</param>
/// <param name="newValue">The new value of the property.</param>
public void OnAncestorInheritedValueChanged<T>(
StyledPropertyBase<T> property,
T oldValue,
T newValue)
{
Debug.Assert(property.Inherits);
// If the inherited value is set locally, propagation stops here.
if (_effectiveValues.ContainsKey(property))
return;
using var notifying = PropertyNotifying.Start(Owner, property);
Owner.RaisePropertyChanged(
property,
oldValue,
newValue,
BindingPriority.Inherited,
true);
var children = Owner.GetInheritanceChildren();
if (children is null)
return;
var count = children.Count;
for (var i = 0; i < count; ++i)
{
children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, newValue);
}
}
/// <summary>
/// Called by a <see cref="ValueFrame"/> to re-evaluate the effective value when a value
/// is removed.
/// </summary>
/// <param name="frame">The frame on which the change occurred.</param>
/// <param name="property">The property whose value was removed.</param>
public void OnValueEntryRemoved(ValueFrame frame, AvaloniaProperty property)
{
if (frame.EntryCount == 0)
_frames.Remove(frame);
if (TryGetEffectiveValue(property, out var existing))
{
if (frame.Priority <= existing.Priority)
ReevaluateEffectiveValue(property, existing);
}
}
public bool RemoveFrame(ValueFrame frame)
{
if (_frames.Remove(frame))
{
frame.Dispose();
++_frameGeneration;
ReevaluateEffectiveValues();
}
return false;
}
public AvaloniaPropertyValue GetDiagnostic(AvaloniaProperty property)
{
object? value;
BindingPriority priority;
if (_effectiveValues.TryGetValue(property, out var v))
{
value = v.Value;
priority = v.Priority;
}
else if (property.Inherits && TryGetInheritedValue(property, out v))
{
value = v.Value;
priority = BindingPriority.Inherited;
}
else
{
value = GetDefaultValue(property);
priority = BindingPriority.Unset;
}
return new AvaloniaPropertyValue(
property,
value,
priority,
null);
}
private int InsertFrame(ValueFrame frame)
{
// Uncomment this line when #8549 is fixed.
//Debug.Assert(!_frames.Contains(frame));
var index = BinarySearchFrame(frame.Priority);
_frames.Insert(index, frame);
++_frameGeneration;
frame.SetOwner(this);
return index;
}
private ImmediateValueFrame GetOrCreateImmediateValueFrame(
AvaloniaProperty property,
BindingPriority priority,
out int frameIndex)
{
Debug.Assert(priority != BindingPriority.LocalValue);
var index = BinarySearchFrame(priority);
if (index > 0 && _frames[index - 1] is ImmediateValueFrame f &&
f.Priority == priority &&
!f.Contains(property))
{
frameIndex = index - 1;
return f;
}
var result = new ImmediateValueFrame(priority);
frameIndex = InsertFrame(result);
return result;
}
private void AddEffectiveValue(AvaloniaProperty property, EffectiveValue effectiveValue)
{
_effectiveValues.Add(property, effectiveValue);
if (property.Inherits && _inheritedValueCount++ == 0)
OnInheritanceAncestorChanged(this);
}
/// <summary>
/// Adds a new effective value, raises the initial <see cref="AvaloniaObject.PropertyChanged"/>
/// event and notifies inheritance children if necessary .
/// </summary>
/// <param name="property">The property.</param>
/// <param name="entry">The value entry.</param>
/// <param name="priority">The value priority.</param>
private void AddEffectiveValueAndRaise(AvaloniaProperty property, IValueEntry entry, BindingPriority priority)
{
Debug.Assert(priority < BindingPriority.Inherited);
var effectiveValue = property.CreateEffectiveValue(Owner);
AddEffectiveValue(property, effectiveValue);
effectiveValue.SetAndRaise(this, entry, priority);
}
private void RemoveEffectiveValue(AvaloniaProperty property, int index)
{
_effectiveValues.RemoveAt(index);
if (property.Inherits && --_inheritedValueCount == 0)
OnInheritanceAncestorChanged(InheritanceAncestor);
}
private bool RemoveEffectiveValue(AvaloniaProperty property)
{
if (_effectiveValues.Remove(property))
{
if (property.Inherits && --_inheritedValueCount == 0)
OnInheritanceAncestorChanged(InheritanceAncestor);
return true;
}
return false;
}
private void InheritedValueChanged(
AvaloniaProperty property,
EffectiveValue? oldValue,
EffectiveValue? newValue)
{
Debug.Assert(oldValue != newValue);
Debug.Assert(oldValue is not null || newValue is not null);
// If the value is set locally, propagaton ends here.
if (_effectiveValues.ContainsKey(property) == true)
return;
using var notifying = PropertyNotifying.Start(Owner, property);
// Raise PropertyChanged on this object if necessary.
(oldValue ?? newValue!).RaiseInheritedValueChanged(Owner, property, oldValue, newValue);
var children = Owner.GetInheritanceChildren();
if (children is null)
return;
var count = children.Count;
for (var i = 0; i < count; ++i)
{
children[i].GetValueStore().InheritedValueChanged(property, oldValue, newValue);
}
}
private void ReevaluateEffectiveValue(
AvaloniaProperty property,
EffectiveValue? current,
bool ignoreLocalValue = false)
{
++_isEvaluating;
try
{
restart:
// Don't reevaluate if a styling pass is in effect, reevaluation will be done when
// it has finished.
if (_styling > 0)
return;
var generation = _frameGeneration;
// Notify the existing effective value that reevaluation is starting.
current?.BeginReevaluation(ignoreLocalValue);
// Iterate the frames to get the effective value.
for (var i = _frames.Count - 1; i >= 0; --i)
{
var frame = _frames[i];
var priority = frame.Priority;
var foundEntry = frame.TryGetEntryIfActive(property, out var entry, out var activeChanged);
// If the active state of the frame has changed since the last read, and
// the frame holds multiple values then we need to re-evaluate the
// effective values of all properties.
if (activeChanged && frame.EntryCount > 1)
{
ReevaluateEffectiveValues();
return;
}
// We're interested in the value if:
// - There is no current effective value, or
// - The value's priority is higher than the current effective value's priority, or
// - The value is a non-animation value and its priority is higher than the current
// effective value's base priority
var isRelevantPriority = current is null ||
priority < current.Priority ||
(priority > BindingPriority.Animation && priority < current.BasePriority);
if (foundEntry && isRelevantPriority && entry!.HasValue)
{
if (current is not null)
{
current.SetAndRaise(this, entry, priority);
}
else
{
current = property.CreateEffectiveValue(Owner);
AddEffectiveValue(property, current);
current.SetAndRaise(this, entry, priority);
}
}
if (generation != _frameGeneration)
goto restart;
if (current?.Priority < BindingPriority.Unset &&
current?.BasePriority < BindingPriority.Unset)
break;
}
current?.EndReevaluation();
if (current?.Priority == BindingPriority.Unset)
{
if (current.BasePriority == BindingPriority.Unset)
{
RemoveEffectiveValue(property);
current.DisposeAndRaiseUnset(this, property);
}
else
{
current.RemoveAnimationAndRaise(this, property);
}
}
}
finally
{
--_isEvaluating;
}
}
private void ReevaluateEffectiveValues()
{
++_isEvaluating;
try
{
restart:
// Don't reevaluate if a styling pass is in effect, reevaluation will be done when
// it has finished.
if (_styling > 0)
return;
var generation = _frameGeneration;
var count = _effectiveValues.Count;
// Notify the existing effective values that reevaluation is starting.
for (var i = 0; i < count; ++i)
_effectiveValues[i].BeginReevaluation();
// Iterate the frames, setting and creating effective values.
for (var i = _frames.Count - 1; i >= 0; --i)
{
var frame = _frames[i];
if (!frame.IsActive)
continue;
var priority = frame.Priority;
count = frame.EntryCount;
for (var j = 0; j < count; ++j)
{
var entry = frame.GetEntry(j);
var property = entry.Property;
// Skip if we already have a value/base value for this property.
if (_effectiveValues.TryGetValue(property, out var effectiveValue) &&
effectiveValue.BasePriority < BindingPriority.Unset)
continue;
if (!entry.HasValue)
continue;
if (effectiveValue is not null)
{
effectiveValue.SetAndRaise(this, entry, priority);
}
else
{
var v = property.CreateEffectiveValue(Owner);
AddEffectiveValue(property, v);
v.SetAndRaise(this, entry, priority);
}
if (generation != _frameGeneration)
goto restart;
}
}
// Remove all effective values that are still unset.
for (var i = _effectiveValues.Count - 1; i >= 0; --i)
{
_effectiveValues.GetKeyValue(i, out var key, out var e);
e.EndReevaluation();
if (e.Priority == BindingPriority.Unset)
{
RemoveEffectiveValue(key, i);
e.DisposeAndRaiseUnset(this, key);
if (i > _effectiveValues.Count)
break;
}
}
}
finally
{
--_isEvaluating;
}
}
private bool TryGetEffectiveValue(
AvaloniaProperty property,
[NotNullWhen(true)] out EffectiveValue? value)
{
if (_effectiveValues.TryGetValue(property, out value))
return true;
value = null;
return false;
}
private EffectiveValue? GetEffectiveValue(AvaloniaProperty property)
{
if (_effectiveValues.TryGetValue(property, out var value))
return value;
return null;
}
private object? GetDefaultValue(AvaloniaProperty property)
{
return ((IStyledPropertyAccessor)property).GetDefaultValue(Owner.GetType());
}
private void DisposeExistingLocalValueBinding(AvaloniaProperty property)
{
if (_localValueBindings is not null &&
_localValueBindings.TryGetValue(property.Id, out var existing))
{
existing.Dispose();
}
}
private int BinarySearchFrame(BindingPriority priority)
{
var lo = 0;
var hi = _frames.Count - 1;
// Binary search insertion point.
while (lo <= hi)
{
var i = lo + ((hi - lo) >> 1);
var order = priority - _frames[i].Priority;
if (order <= 0)
{
lo = i + 1;
}
else
{
hi = i - 1;
}
}
return lo;
}
private readonly struct OldNewValue
{
public OldNewValue(EffectiveValue? oldValue)
{
OldValue = oldValue;
NewValue = null;
}
public OldNewValue(EffectiveValue? oldValue, EffectiveValue? newValue)
{
OldValue = oldValue;
NewValue = newValue;
}
public readonly EffectiveValue? OldValue;
public readonly EffectiveValue? NewValue;
public OldNewValue WithNewValue(EffectiveValue newValue) => new(OldValue, newValue);
}
}
}

59
src/Avalonia.Base/Reactive/BindingValueAdapter.cs

@ -1,59 +0,0 @@
using System;
using System.Reactive.Subjects;
using Avalonia.Data;
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));
}
}
}

33
src/Avalonia.Base/Reactive/BindingValueExtensions.cs

@ -1,33 +0,0 @@
using System;
using System.Reactive.Subjects;
using Avalonia.Data;
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);
}
}
}

6
src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs

@ -51,10 +51,11 @@ namespace Avalonia.Reactive
protected void PublishCompleted() protected void PublishCompleted()
{ {
_completed = true;
if (_observer != null) if (_observer != null)
{ {
_observer.OnCompleted(); _observer.OnCompleted();
_completed = true;
Unsubscribed(); Unsubscribed();
_observer = null; _observer = null;
} }
@ -62,10 +63,11 @@ namespace Avalonia.Reactive
protected void PublishError(Exception error) protected void PublishError(Exception error)
{ {
_error = error;
if (_observer != null) if (_observer != null)
{ {
_observer.OnError(error); _observer.OnError(error);
_error = error;
Unsubscribed(); Unsubscribed();
_observer = null; _observer = null;
} }

62
src/Avalonia.Base/Reactive/TypedBindingAdapter.cs

@ -1,62 +0,0 @@
using System;
using Avalonia.Data;
using Avalonia.Logging;
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)
{
var unwrappedValue = value.HasValue ? value.Value : null;
Logger.TryGet(LogEventLevel.Error, LogArea.Binding)?.Log(
_target,
"Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
_property.Name,
_property.PropertyType,
unwrappedValue,
unwrappedValue?.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();
}
}

55
src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs

@ -1,55 +0,0 @@
using System;
using System.Reactive.Subjects;
using Avalonia.Data;
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());
}
}
}

149
src/Avalonia.Base/StyledElement.cs

@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Linq;
using Avalonia.Animation; using Avalonia.Animation;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
@ -69,7 +70,6 @@ namespace Avalonia
private IResourceDictionary? _resources; private IResourceDictionary? _resources;
private Styles? _styles; private Styles? _styles;
private bool _styled; private bool _styled;
private List<IStyleInstance>? _appliedStyles;
private ITemplatedControl? _templatedParent; private ITemplatedControl? _templatedParent;
private bool _dataContextUpdating; private bool _dataContextUpdating;
private bool _hasPromotedTheme; private bool _hasPromotedTheme;
@ -333,7 +333,7 @@ namespace Avalonia
throw new InvalidOperationException("BeginInit was not called."); throw new InvalidOperationException("BeginInit was not called.");
} }
if (--_initCount == 0 && _logicalRoot != null) if (--_initCount == 0 && _logicalRoot is not null)
{ {
ApplyStyling(); ApplyStyling();
InitializeIfNeeded(); InitializeIfNeeded();
@ -351,18 +351,25 @@ namespace Avalonia
{ {
if (_initCount == 0 && !_styled) if (_initCount == 0 && !_styled)
{ {
try var styler = AvaloniaLocator.Current.GetService<IStyler>();
{ var hasPromotedTheme = _hasPromotedTheme;
BeginBatchUpdate();
AvaloniaLocator.Current.GetService<IStyler>()?.ApplyStyles(this); if (styler is object)
}
finally
{ {
_styled = true; GetValueStore().BeginStyling();
EndBatchUpdate();
try
{
styler.ApplyStyles(this);
}
finally
{
_styled = true;
GetValueStore().EndStyling();
}
} }
if (_hasPromotedTheme) if (hasPromotedTheme)
{ {
_hasPromotedTheme = false; _hasPromotedTheme = false;
ClearValue(ThemeProperty); ClearValue(ThemeProperty);
@ -389,14 +396,15 @@ namespace Avalonia
internal StyleDiagnostics GetStyleDiagnosticsInternal() internal StyleDiagnostics GetStyleDiagnosticsInternal()
{ {
IReadOnlyList<IStyleInstance>? appliedStyles = _appliedStyles; var styles = new List<IStyleInstance>();
if (appliedStyles is null) foreach (var frame in GetValueStore().Frames)
{ {
appliedStyles = Array.Empty<IStyleInstance>(); if (frame is IStyleInstance style)
styles.Add(style);
} }
return new StyleDiagnostics(appliedStyles); return new StyleDiagnostics(styles);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -522,20 +530,8 @@ namespace Avalonia
return null; return null;
} }
void IStyleable.StyleApplied(IStyleInstance instance)
{
instance = instance ?? throw new ArgumentNullException(nameof(instance));
_appliedStyles ??= new List<IStyleInstance>();
_appliedStyles.Add(instance);
}
void IStyleable.DetachStyles() => DetachStyles(); void IStyleable.DetachStyles() => DetachStyles();
void IStyleable.DetachStyles(IReadOnlyList<IStyle> styles) => DetachStyles(styles);
void IStyleable.InvalidateStyles() => InvalidateStyles();
void IStyleHost.StylesAdded(IReadOnlyList<IStyle> styles) void IStyleHost.StylesAdded(IReadOnlyList<IStyle> styles)
{ {
InvalidateStylesOnThisAndDescendents(); InvalidateStylesOnThisAndDescendents();
@ -643,13 +639,15 @@ namespace Avalonia
if (change.Property == ThemeProperty) if (change.Property == ThemeProperty)
{ {
var (oldValue, newValue) = change.GetOldAndNewValue<ControlTheme?>();
// Changing the theme detaches all styles, meaning that if the theme property was // Changing the theme detaches all styles, meaning that if the theme property was
// set via a style, it will get cleared! To work around this, if the value was // set via a style, it will get cleared! To work around this, if the value was
// applied at less than local value priority then promote the value to local value // applied at less than local value priority then promote the value to local value
// priority until styling is re-applied. // priority until styling is re-applied.
if (change.Priority > BindingPriority.LocalValue) if (change.Priority > BindingPriority.LocalValue)
{ {
Theme = change.GetNewValue<ControlTheme?>(); Theme = newValue;
_hasPromotedTheme = true; _hasPromotedTheme = true;
} }
else if (_hasPromotedTheme && change.Priority == BindingPriority.LocalValue) else if (_hasPromotedTheme && change.Priority == BindingPriority.LocalValue)
@ -658,9 +656,16 @@ namespace Avalonia
} }
InvalidateStyles(); InvalidateStyles();
if (oldValue is not null)
DetachControlThemeFromTemplateChildren(oldValue);
} }
} }
internal virtual void DetachControlThemeFromTemplateChildren(ControlTheme theme)
{
}
private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted) private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted)
{ {
if (o is StyledElement element) if (o is StyledElement element)
@ -830,59 +835,25 @@ namespace Avalonia
} }
} }
private void DetachStyles() private void DetachStyles(IReadOnlyList<StyleBase>? styles = null)
{ {
if (_appliedStyles?.Count > 0) var valueStore = GetValueStore();
{
BeginBatchUpdate();
try valueStore.BeginStyling();
{
foreach (var i in _appliedStyles)
{
i.Dispose();
}
_appliedStyles.Clear(); for (var i = valueStore.Frames.Count - 1; i >= 0; --i)
} {
finally if (valueStore.Frames[i] is StyleInstance si &&
(styles is null || styles.Contains(si.Source)))
{ {
EndBatchUpdate(); valueStore.RemoveFrame(si);
} }
} }
valueStore.EndStyling();
_styled = false; _styled = false;
} }
private void DetachStyles(IReadOnlyList<IStyle> styles)
{
styles = styles ?? throw new ArgumentNullException(nameof(styles));
if (_appliedStyles is null)
{
return;
}
var count = styles.Count;
for (var i = 0; i < count; ++i)
{
for (var j = _appliedStyles.Count - 1; j >= 0; --j)
{
var applied = _appliedStyles[j];
if (applied.Source == styles[i])
{
_appliedStyles.RemoveAt(j);
applied.Dispose();
}
if (j > _appliedStyles.Count)
j = _appliedStyles.Count;
}
}
}
private void InvalidateStylesOnThisAndDescendents() private void InvalidateStylesOnThisAndDescendents()
{ {
InvalidateStyles(); InvalidateStyles();
@ -898,7 +869,7 @@ namespace Avalonia
} }
} }
private void DetachStylesFromThisAndDescendents(IReadOnlyList<IStyle> styles) private void DetachStylesFromThisAndDescendents(IReadOnlyList<StyleBase> styles)
{ {
DetachStyles(styles); DetachStyles(styles);
@ -930,38 +901,24 @@ namespace Avalonia
} }
} }
private static IReadOnlyList<IStyle> RecurseStyles(IReadOnlyList<IStyle> styles) private static IReadOnlyList<StyleBase> RecurseStyles(IReadOnlyList<IStyle> styles)
{ {
var count = styles.Count; var result = new List<StyleBase>();
List<IStyle>? result = null; RecurseStyles(styles, result);
return result;
for (var i = 0; i < count; ++i)
{
var style = styles[i];
if (style.Children.Count > 0)
{
if (result is null)
{
result = new List<IStyle>(styles);
}
RecurseStyles(style.Children, result);
}
}
return result ?? styles;
} }
private static void RecurseStyles(IReadOnlyList<IStyle> styles, List<IStyle> result) private static void RecurseStyles(IReadOnlyList<IStyle> styles, List<StyleBase> result)
{ {
var count = styles.Count; var count = styles.Count;
for (var i = 0; i < count; ++i) for (var i = 0; i < count; ++i)
{ {
var style = styles[i]; var s = styles[i];
result.Add(style); if (s is StyleBase style)
RecurseStyles(style.Children, result); result.Add(style);
else if (s is IReadOnlyList<IStyle> children)
RecurseStyles(children, result);
} }
} }
} }

86
src/Avalonia.Base/StyledPropertyBase.cs

@ -1,7 +1,10 @@
using System; using System;
using System.Reflection;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Utilities;
namespace Avalonia namespace Avalonia
{ {
@ -169,6 +172,20 @@ namespace Avalonia
/// <inheritdoc/> /// <inheritdoc/>
object? IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); object? IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type);
bool IStyledPropertyAccessor.ValidateValue(object? value)
{
if (value is null && !typeof(TValue).IsValueType)
return ValidateValue?.Invoke(default!) ?? true;
if (value is TValue typed)
return ValidateValue?.Invoke(typed) ?? true;
return false;
}
internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o)
{
return new EffectiveValue<TValue>(o, this);
}
/// <inheritdoc/> /// <inheritdoc/>
internal override void RouteClearValue(AvaloniaObject o) internal override void RouteClearValue(AvaloniaObject o)
{ {
@ -182,77 +199,44 @@ namespace Avalonia
} }
/// <inheritdoc/> /// <inheritdoc/>
internal override object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority) internal override object? RouteGetBaseValue(AvaloniaObject o)
{ {
var value = o.GetBaseValue<TValue>(this, maxPriority); var value = o.GetBaseValue<TValue>(this);
return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue; return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue;
} }
/// <inheritdoc/> /// <inheritdoc/>
internal override IDisposable? RouteSetValue( internal override IDisposable? RouteSetValue(
AvaloniaObject o, AvaloniaObject target,
object? value, object? value,
BindingPriority priority) BindingPriority priority)
{ {
var v = TryConvert(value); if (value == BindingOperations.DoNothing)
if (v.HasValue)
{ {
return o.SetValue<TValue>(this, (TValue)v.Value!, priority); return null;
} }
else if (v.Type == BindingValueType.UnsetValue) else if (value == UnsetValue)
{ {
o.ClearValue(this); target.ClearValue(this);
return null;
} }
else if (v.HasError) else if (TypeUtilities.TryConvertImplicit(PropertyType, value, out var converted))
{ {
throw v.Error!; return target.SetValue<TValue>(this, (TValue)converted!, priority);
}
else
{
var type = value?.GetType().FullName ?? "(null)";
throw new ArgumentException($"Invalid value for Property '{Name}': '{value}' ({type})");
} }
return null;
} }
/// <inheritdoc/>
internal override IDisposable RouteBind( internal override IDisposable RouteBind(
AvaloniaObject o, AvaloniaObject target,
IObservable<BindingValue<object?>> source, IObservable<object?> source,
BindingPriority priority) BindingPriority priority)
{ {
var adapter = TypedBindingAdapter<TValue>.Create(o, this, source); return target.Bind<TValue>(this, source, priority);
return o.Bind<TValue>(this, adapter, priority);
}
/// <inheritdoc/>
internal override void RouteInheritanceParentChanged(
AvaloniaObject o,
AvaloniaObject? oldParent)
{
o.InheritanceParentChanged(this, oldParent);
}
internal override ISetterInstance CreateSetterInstance(IStyleable target, object? value)
{
if (value is IBinding binding)
{
return new PropertySetterBindingInstance<TValue>(
target,
this,
binding);
}
else if (value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(PropertyType))
{
return new PropertySetterTemplateInstance<TValue>(
target,
this,
template);
}
else
{
return new PropertySetterInstance<TValue>(
target,
this,
(TValue)value!);
}
} }
private object? GetDefaultBoxedValue(Type type) private object? GetDefaultBoxedValue(Type type)

42
src/Avalonia.Base/Styling/Activators/AndActivator.cs

@ -1,6 +1,4 @@
#nullable enable using System.Collections.Generic;
using System.Collections.Generic;
namespace Avalonia.Styling.Activators namespace Avalonia.Styling.Activators
{ {
@ -11,47 +9,45 @@ namespace Avalonia.Styling.Activators
internal class AndActivator : StyleActivatorBase, IStyleActivatorSink internal class AndActivator : StyleActivatorBase, IStyleActivatorSink
{ {
private List<IStyleActivator>? _sources; private List<IStyleActivator>? _sources;
private ulong _flags;
private ulong _mask;
public int Count => _sources?.Count ?? 0; public int Count => _sources?.Count ?? 0;
public void Add(IStyleActivator activator) public void Add(IStyleActivator activator)
{ {
if (IsSubscribed)
throw new AvaloniaInternalException("AndActivator is already subscribed.");
_sources ??= new List<IStyleActivator>(); _sources ??= new List<IStyleActivator>();
_sources.Add(activator); _sources.Add(activator);
} }
void IStyleActivatorSink.OnNext(bool value, int tag) void IStyleActivatorSink.OnNext(bool value) => ReevaluateIsActive();
protected override bool EvaluateIsActive()
{ {
if (value) if (_sources is null || _sources.Count == 0)
{ return true;
_flags |= 1ul << tag;
}
else
{
_flags &= ~(1ul << tag);
}
if (_mask != 0) var count = _sources.Count;
var mask = (1ul << count) - 1;
var flags = 0UL;
for (var i = 0; i < count; ++i)
{ {
PublishNext(_flags == _mask); if (_sources[i].GetIsActive())
flags |= 1ul << i;
} }
return flags == mask;
} }
protected override void Initialize() protected override void Initialize()
{ {
if (_sources is object) if (_sources is object)
{ {
var i = 0;
foreach (var source in _sources) foreach (var source in _sources)
{ {
source.Subscribe(this, i++); source.Subscribe(this);
} }
_mask = (1ul << Count) - 1;
PublishNext(_flags == _mask);
} }
} }
@ -64,8 +60,6 @@ namespace Avalonia.Styling.Activators
source.Unsubscribe(this); source.Unsubscribe(this);
} }
} }
_mask = 0;
} }
} }
} }

28
src/Avalonia.Base/Styling/Activators/IStyleActivator.cs

@ -9,21 +9,39 @@ namespace Avalonia.Styling.Activators
/// <remarks> /// <remarks>
/// A style activator is very similar to an `IObservable{bool}` but is optimized for the /// A style activator is very similar to an `IObservable{bool}` but is optimized for the
/// particular use-case of activating a style according to a selector. It differs from /// particular use-case of activating a style according to a selector. It differs from
/// an observable in two major ways: /// an observable in three major ways:
/// ///
/// - Can only have a single subscription /// - Can only have a single subscription
/// - The subscription can have a tag associated with it, allowing a subscriber to index /// - The activation state can be re-evaluated at any time by calling <see cref="GetIsActive"/>
/// into a list of subscriptions without having to allocate additional objects. /// - No error or completion messages
/// </remarks> /// </remarks>
[Unstable] [Unstable]
public interface IStyleActivator : IDisposable public interface IStyleActivator : IDisposable
{ {
/// <summary>
/// Gets a value indicating whether the style is subscribed.
/// </summary>
bool IsSubscribed { get; }
/// <summary>
/// Gets the current activation state.
/// </summary>
/// <remarks>
/// This method should read directly from its inputs and not rely on any subscriptions
/// to fire in order to be up-to-date. If a change in active state occurs when reading
/// this method then any subscribed <see cref="IStyleActivatorSink"/> should not be
/// notified of the change.
/// </remarks>
bool GetIsActive();
/// <summary> /// <summary>
/// Subscribes to the activator. /// Subscribes to the activator.
/// </summary> /// </summary>
/// <param name="sink">The listener.</param> /// <param name="sink">The listener.</param>
/// <param name="tag">An optional tag.</param> /// <remarks>
void Subscribe(IStyleActivatorSink sink, int tag = 0); /// This method should not call <see cref="IStyleActivatorSink.OnNext(bool, int)"/>.
/// </remarks>
void Subscribe(IStyleActivatorSink sink);
/// <summary> /// <summary>
/// Unsubscribes from the activator. /// Unsubscribes from the activator.

3
src/Avalonia.Base/Styling/Activators/IStyleActivatorSink.cs

@ -12,7 +12,6 @@ namespace Avalonia.Styling.Activators
/// Called when the subscribed activator value changes. /// Called when the subscribed activator value changes.
/// </summary> /// </summary>
/// <param name="value">The new value.</param> /// <param name="value">The new value.</param>
/// <param name="tag">The subscription tag.</param> void OnNext(bool value);
void OnNext(bool value, int tag);
} }
} }

9
src/Avalonia.Base/Styling/Activators/NotActivator.cs

@ -1,6 +1,4 @@
#nullable enable namespace Avalonia.Styling.Activators
namespace Avalonia.Styling.Activators
{ {
/// <summary> /// <summary>
/// An <see cref="IStyleActivator"/> which inverts the state of an input activator. /// An <see cref="IStyleActivator"/> which inverts the state of an input activator.
@ -9,8 +7,9 @@ namespace Avalonia.Styling.Activators
{ {
private readonly IStyleActivator _source; private readonly IStyleActivator _source;
public NotActivator(IStyleActivator source) => _source = source; public NotActivator(IStyleActivator source) => _source = source;
void IStyleActivatorSink.OnNext(bool value, int tag) => PublishNext(!value); void IStyleActivatorSink.OnNext(bool value) => ReevaluateIsActive();
protected override void Initialize() => _source.Subscribe(this, 0); protected override bool EvaluateIsActive() => !_source.GetIsActive();
protected override void Initialize() => _source.Subscribe(this);
protected override void Deinitialize() => _source.Unsubscribe(this); protected override void Deinitialize() => _source.Unsubscribe(this);
} }
} }

19
src/Avalonia.Base/Styling/Activators/NthChildActivator.cs

@ -26,31 +26,26 @@ namespace Avalonia.Styling.Activators
_reversed = reversed; _reversed = reversed;
} }
protected override void Initialize() protected override bool EvaluateIsActive()
{ {
PublishNext(IsMatching()); return NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch;
_provider.ChildIndexChanged += ChildIndexChanged;
} }
protected override void Deinitialize() protected override void Initialize() => _provider.ChildIndexChanged += ChildIndexChanged;
{ protected override void Deinitialize() => _provider.ChildIndexChanged -= ChildIndexChanged;
_provider.ChildIndexChanged -= ChildIndexChanged;
}
private void ChildIndexChanged(object? sender, ChildIndexChangedEventArgs e) private void ChildIndexChanged(object? sender, ChildIndexChangedEventArgs e)
{ {
// Run matching again if: // Run matching again if:
// 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index. // 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index.
// 2. e.Child is null, when all children indeces were changed. // 2. e.Child is null, when all children indices were changed.
// 3. Subscribed child index was changed. // 3. Subscribed child index was changed.
if (_reversed if (_reversed
|| e.Child is null || e.Child is null
|| e.Child == _control) || e.Child == _control)
{ {
PublishNext(IsMatching()); ReevaluateIsActive();
} }
} }
private bool IsMatching() => NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch;
} }
} }

36
src/Avalonia.Base/Styling/Activators/OrActivator.cs

@ -1,6 +1,4 @@
#nullable enable using System.Collections.Generic;
using System.Collections.Generic;
namespace Avalonia.Styling.Activators namespace Avalonia.Styling.Activators
{ {
@ -11,8 +9,6 @@ namespace Avalonia.Styling.Activators
internal class OrActivator : StyleActivatorBase, IStyleActivatorSink internal class OrActivator : StyleActivatorBase, IStyleActivatorSink
{ {
private List<IStyleActivator>? _sources; private List<IStyleActivator>? _sources;
private ulong _flags;
private bool _initializing;
public int Count => _sources?.Count ?? 0; public int Count => _sources?.Count ?? 0;
@ -22,38 +18,30 @@ namespace Avalonia.Styling.Activators
_sources.Add(activator); _sources.Add(activator);
} }
void IStyleActivatorSink.OnNext(bool value, int tag) void IStyleActivatorSink.OnNext(bool value) => ReevaluateIsActive();
protected override bool EvaluateIsActive()
{ {
if (value) if (_sources is null || _sources.Count == 0)
{ return true;
_flags |= 1ul << tag;
}
else
{
_flags &= ~(1ul << tag);
}
if (!_initializing) foreach (var source in _sources)
{ {
PublishNext(_flags != 0); if (source.GetIsActive())
return true;
} }
return false;
} }
protected override void Initialize() protected override void Initialize()
{ {
if (_sources is object) if (_sources is object)
{ {
var i = 0;
_initializing = true;
foreach (var source in _sources) foreach (var source in _sources)
{ {
source.Subscribe(this, i++); source.Subscribe(this);
} }
_initializing = false;
PublishNext(_flags != 0);
} }
} }

10
src/Avalonia.Base/Styling/Activators/PropertyEqualsActivator.cs

@ -1,7 +1,5 @@
using System; using System;
#nullable enable
namespace Avalonia.Styling.Activators namespace Avalonia.Styling.Activators
{ {
/// <summary> /// <summary>
@ -24,6 +22,12 @@ namespace Avalonia.Styling.Activators
_value = value; _value = value;
} }
protected override bool EvaluateIsActive()
{
var value = _control.GetValue(_property);
return PropertyEqualsSelector.Compare(_property.PropertyType, value, _value);
}
protected override void Initialize() protected override void Initialize()
{ {
_subscription = _control.GetObservable(_property).Subscribe(this); _subscription = _control.GetObservable(_property).Subscribe(this);
@ -33,6 +37,6 @@ namespace Avalonia.Styling.Activators
void IObserver<object?>.OnCompleted() { } void IObserver<object?>.OnCompleted() { }
void IObserver<object?>.OnError(Exception error) { } void IObserver<object?>.OnError(Exception error) { }
void IObserver<object?>.OnNext(object? value) => PublishNext(PropertyEqualsSelector.Compare(_property.PropertyType, value, _value)); void IObserver<object?>.OnNext(object? value) => ReevaluateIsActive();
} }
} }

65
src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs

@ -1,5 +1,3 @@
#nullable enable
namespace Avalonia.Styling.Activators namespace Avalonia.Styling.Activators
{ {
/// <summary> /// <summary>
@ -8,26 +6,30 @@ namespace Avalonia.Styling.Activators
internal abstract class StyleActivatorBase : IStyleActivator internal abstract class StyleActivatorBase : IStyleActivator
{ {
private IStyleActivatorSink? _sink; private IStyleActivatorSink? _sink;
private int _tag; private bool _value;
private bool? _value;
public bool GetIsActive() => _value = EvaluateIsActive();
public bool IsSubscribed => _sink is not null;
public void Subscribe(IStyleActivatorSink sink, int tag = 0) public void Subscribe(IStyleActivatorSink sink)
{ {
if (_sink is null) if (_sink is null)
{ {
_sink = sink;
_tag = tag;
_value = null;
Initialize(); Initialize();
_sink = sink;
} }
else else
{ {
throw new AvaloniaInternalException("Cannot subscribe to a StyleActivator more than once."); throw new AvaloniaInternalException("StyleActivator is already subscribed.");
} }
} }
public void Unsubscribe(IStyleActivatorSink sink) public void Unsubscribe(IStyleActivatorSink sink)
{ {
if (_sink is null)
return;
if (_sink != sink) if (_sink != sink)
{ {
throw new AvaloniaInternalException("StyleActivatorSink is not subscribed."); throw new AvaloniaInternalException("StyleActivatorSink is not subscribed.");
@ -37,22 +39,51 @@ namespace Avalonia.Styling.Activators
Deinitialize(); Deinitialize();
} }
public void PublishNext(bool value) public void Dispose()
{ {
if (_value != value) _sink = null;
Deinitialize();
}
/// <summary>
/// Evaluates the activation state.
/// </summary>
/// <remarks>
/// This method should read directly from its inputs and not rely on any subscriptions to
/// fire in order to be up-to-date.
/// </remarks>
protected abstract bool EvaluateIsActive();
/// <summary>
/// Called from a derived class when the activation state should be re-evaluated and the
/// subscriber notified of any change.
/// </summary>
/// <returns>
/// The evaluated active state;
/// </returns>
protected bool ReevaluateIsActive()
{
var value = EvaluateIsActive();
if (value != _value)
{ {
_value = value; _value = value;
_sink?.OnNext(value, _tag); _sink?.OnNext(value);
} }
}
public void Dispose() return value;
{
_sink = null;
Deinitialize();
} }
/// <summary>
/// Called in response to a <see cref="Subscribe(IStyleActivatorSink)"/> to allow the
/// derived class to set up any necessary subscriptions.
/// </summary>
protected abstract void Initialize(); protected abstract void Initialize();
/// <summary>
/// Called in response to an <see cref="Unsubscribe(IStyleActivatorSink)"/> or
/// <see cref="Dispose"/> to allow the derived class to dispose any active subscriptions.
/// </summary>
protected abstract void Deinitialize(); protected abstract void Deinitialize();
} }
} }

25
src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs

@ -1,10 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
#nullable enable
namespace Avalonia.Styling.Activators namespace Avalonia.Styling.Activators
{ {
/// <summary> /// <summary>
@ -52,22 +48,9 @@ namespace Avalonia.Styling.Activators
return remainingMatches == 0; return remainingMatches == 0;
} }
void IClassesChangedListener.Changed() void IClassesChangedListener.Changed() => ReevaluateIsActive();
{ protected override bool EvaluateIsActive() => AreClassesMatching(_classes, _match);
PublishNext(IsMatching()); protected override void Initialize() => _classes.AddListener(this);
} protected override void Deinitialize() => _classes.RemoveListener(this);
protected override void Initialize()
{
PublishNext(IsMatching());
_classes.AddListener(this);
}
protected override void Deinitialize()
{
_classes.RemoveListener(this);
}
private bool IsMatching() => AreClassesMatching(_classes, _match);
} }
} }

4
src/Avalonia.Base/Styling/ChildSelector.cs

@ -27,11 +27,11 @@ namespace Avalonia.Styling
/// <inheritdoc/> /// <inheritdoc/>
public override Type? TargetType => null; public override Type? TargetType => null;
public override string ToString() public override string ToString(Style? owner)
{ {
if (_selectorString == null) if (_selectorString == null)
{ {
_selectorString = _parent.ToString() + " > "; _selectorString = _parent.ToString(owner) + " > ";
} }
return _selectorString; return _selectorString;

9
src/Avalonia.Base/Styling/ControlTheme.cs

@ -1,4 +1,5 @@
using System; using System;
using Avalonia.PropertyStore;
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
@ -51,13 +52,7 @@ namespace Avalonia.Styling
return result; return result;
} }
public override string ToString() public override string ToString() => TargetType?.Name ?? "ControlTheme";
{
if (TargetType is not null)
return "ControlTheme: " + TargetType.Name;
else
return "ControlTheme";
}
internal override void SetParent(StyleBase? parent) internal override void SetParent(StyleBase? parent)
{ {

4
src/Avalonia.Base/Styling/DescendentSelector.cs

@ -25,11 +25,11 @@ namespace Avalonia.Styling
/// <inheritdoc/> /// <inheritdoc/>
public override Type? TargetType => null; public override Type? TargetType => null;
public override string ToString() public override string ToString(Style? owner)
{ {
if (_selectorString == null) if (_selectorString == null)
{ {
_selectorString = _parent.ToString() + ' '; _selectorString = _parent.ToString(owner) + ' ';
} }
return _selectorString; return _selectorString;

6
src/Avalonia.Base/Styling/DirectPropertySetterBindingInstance.cs

@ -0,0 +1,6 @@
namespace Avalonia.Styling
{
internal class DirectPropertySetterBindingInstance : ISetterInstance
{
}
}

12
src/Avalonia.Base/Styling/DirectPropertySetterInstance.cs

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Avalonia.Styling
{
internal class DirectPropertySetterInstance : ISetterInstance
{
}
}

6
src/Avalonia.Base/Styling/ISetter.cs

@ -12,13 +12,13 @@ namespace Avalonia.Styling
/// <summary> /// <summary>
/// Instances a setter on a control. /// Instances a setter on a control.
/// </summary> /// </summary>
/// <param name="styleInstance">The style which contains the setter.</param>
/// <param name="target">The control.</param> /// <param name="target">The control.</param>
/// <returns>An <see cref="ISetterInstance"/>.</returns> /// <returns>An <see cref="ISetterInstance"/>.</returns>
/// <remarks> /// <remarks>
/// This method should return an <see cref="ISetterInstance"/> which can be used to apply /// This method should return an <see cref="ISetterInstance"/> which can be used to apply
/// the setter to the specified control. Note that it should not apply the setter value /// the setter to the specified control.
/// until <see cref="ISetterInstance.Start(bool)"/> is called.
/// </remarks> /// </remarks>
ISetterInstance Instance(IStyleable target); ISetterInstance Instance(IStyleInstance styleInstance, IStyleable target);
} }
} }

34
src/Avalonia.Base/Styling/ISetterInstance.cs

@ -1,40 +1,12 @@
using System; using Avalonia.Metadata;
using Avalonia.Metadata;
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
/// <summary> /// <summary>
/// Represents a setter that has been instanced on a control. /// Represents an <see cref="ISetter"/> that has been instanced on a control.
/// </summary> /// </summary>
[Unstable] [Unstable]
public interface ISetterInstance : IDisposable public interface ISetterInstance
{ {
/// <summary>
/// Starts the setter instance.
/// </summary>
/// <param name="hasActivator">Whether the parent style has an activator.</param>
/// <remarks>
/// If <paramref name="hasActivator"/> is false then the setter should be immediately
/// applied and <see cref="Activate"/> and <see cref="Deactivate"/> should not be called.
/// If true, then bindings etc should be initiated but not produce a value until
/// <see cref="Activate"/> called.
/// </remarks>
public void Start(bool hasActivator);
/// <summary>
/// Activates the setter.
/// </summary>
/// <remarks>
/// Should only be called if hasActivator was true when <see cref="Start(bool)"/> was called.
/// </remarks>
public void Activate();
/// <summary>
/// Deactivates the setter.
/// </summary>
/// <remarks>
/// Should only be called if hasActivator was true when <see cref="Start(bool)"/> was called.
/// </remarks>
public void Deactivate();
} }
} }

19
src/Avalonia.Base/Styling/IStyleInstance.cs

@ -1,13 +1,12 @@
using System; using Avalonia.Metadata;
using Avalonia.Metadata;
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
/// <summary> /// <summary>
/// Represents a style that has been instanced on a control. /// Represents a <see cref="Style"/> that has been instanced on a control.
/// </summary> /// </summary>
[Unstable] [Unstable]
public interface IStyleInstance : IDisposable public interface IStyleInstance
{ {
/// <summary> /// <summary>
/// Gets the source style. /// Gets the source style.
@ -15,18 +14,16 @@ namespace Avalonia.Styling
IStyle Source { get; } IStyle Source { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether this style has an activator. /// Gets a value indicating whether this style instance has an activator.
/// </summary> /// </summary>
/// <remarks>
/// A style instance without an activator will always be active.
/// </remarks>
bool HasActivator { get; } bool HasActivator { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether this style is active. /// Gets a value indicating whether this style is active.
/// </summary> /// </summary>
bool IsActive { get; } bool IsActive { get; }
/// <summary>
/// Instructs the style to start acting upon the control.
/// </summary>
void Start();
} }
} }

20
src/Avalonia.Base/Styling/IStyleable.cs

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Metadata; using Avalonia.Metadata;
@ -31,25 +30,6 @@ namespace Avalonia.Styling
/// </summary> /// </summary>
ControlTheme? GetEffectiveTheme(); ControlTheme? GetEffectiveTheme();
/// <summary>
/// Notifies the element that a style has been applied.
/// </summary>
/// <param name="instance">The style instance.</param>
void StyleApplied(IStyleInstance instance);
/// <summary>
/// Detaches all styles applied to the element.
/// </summary>
void DetachStyles(); void DetachStyles();
/// <summary>
/// Detaches a collection of styles, if applied to the element.
/// </summary>
void DetachStyles(IReadOnlyList<IStyle> styles);
/// <summary>
/// Detaches all styles from the element and queues a restyle.
/// </summary>
void InvalidateStyles();
} }
} }

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

Loading…
Cancel
Save