Browse Source

Merge branch 'master' into fixes/avalonia-animation-nullability

pull/7244/head
Jumar Macato 4 years ago
committed by GitHub
parent
commit
37b1b45e7b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 27
      Avalonia.sln
  2. 33
      azure-pipelines.yml
  3. 35
      build/MicroCom.targets
  4. 8
      nukebuild/MicroComGen.cs
  5. 6
      nukebuild/_build.csproj
  6. 2
      packages/Avalonia/Avalonia.csproj
  7. 1
      samples/ControlCatalog.Web/ControlCatalog.Web.csproj
  8. 40
      samples/ControlCatalog/App.xaml.cs
  9. 1
      samples/ControlCatalog/DecoratedWindow.xaml.cs
  10. 42
      samples/ControlCatalog/MainView.xaml.cs
  11. 5
      samples/ControlCatalog/MainWindow.xaml.cs
  12. 15
      samples/ControlCatalog/Models/Person.cs
  13. 5
      samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml
  14. 12
      samples/ControlCatalog/Pages/DataGridPage.xaml
  15. 22
      samples/ControlCatalog/Pages/DataGridPage.xaml.cs
  16. 13
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  17. 12
      src/Android/Avalonia.Android/AndroidPlatform.cs
  18. 2
      src/Avalonia.Animation/Avalonia.Animation.csproj
  19. 3
      src/Avalonia.Base/ApiCompatBaseline.txt
  20. 2
      src/Avalonia.Base/Avalonia.Base.csproj
  21. 21
      src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs
  22. 10
      src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
  23. 20
      src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs
  24. 24
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  25. 9
      src/Avalonia.Base/Threading/Dispatcher.cs
  26. 9
      src/Avalonia.Base/Threading/IDispatcher.cs
  27. 83
      src/Avalonia.Base/Threading/JobRunner.cs
  28. 12
      src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs
  29. 187
      src/Avalonia.Base/Utilities/WeakEvent.cs
  30. 40
      src/Avalonia.Base/Utilities/WeakEvents.cs
  31. 35
      src/Avalonia.Base/Utilities/WeakObservable.cs
  32. 3
      src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs
  33. 2
      src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj
  34. 1
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  35. 2
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  36. 72
      src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
  37. 7
      src/Avalonia.Controls/ApiCompatBaseline.txt
  38. 2
      src/Avalonia.Controls/Avalonia.Controls.csproj
  39. 7
      src/Avalonia.Controls/ButtonSpinner.cs
  40. 11
      src/Avalonia.Controls/Calendar/CalendarDatePicker.cs
  41. 10
      src/Avalonia.Controls/NativeMenuItem.cs
  42. 32
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  43. 19
      src/Avalonia.Controls/TextBox.cs
  44. 15
      src/Avalonia.Controls/TopLevel.cs
  45. 15
      src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs
  46. 2
      src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj
  47. 4
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  48. 2
      src/Avalonia.Desktop/Avalonia.Desktop.csproj
  49. 2
      src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj
  50. 2
      src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
  51. 44
      src/Avalonia.Diagnostics/DevToolsExtensions.cs
  52. 47
      src/Avalonia.Diagnostics/Diagnostics/Behaviors/ColumnDefinition.cs
  53. 119
      src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs
  54. 24
      src/Avalonia.Diagnostics/Diagnostics/Converters/GetTypeNameConverter.cs
  55. 17
      src/Avalonia.Diagnostics/Diagnostics/Convetions.cs
  56. 95
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  57. 16
      src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
  58. 17
      src/Avalonia.Diagnostics/Diagnostics/IScreenshotHandler.cs
  59. 28
      src/Avalonia.Diagnostics/Diagnostics/KeyGestureExtesions.cs
  60. 29
      src/Avalonia.Diagnostics/Diagnostics/Screenshots/BaseRenderToStreamHandler.cs
  61. 85
      src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs
  62. 33
      src/Avalonia.Diagnostics/Diagnostics/TypeExtesnions.cs
  63. 13
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
  64. 12
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
  65. 54
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  66. 90
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs
  67. 157
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  68. 13
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
  69. 9
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  70. 14
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs
  71. 6
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  72. 80
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
  73. 36
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  74. 49
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
  75. 4
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
  76. 21
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  77. 7
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
  78. 71
      src/Avalonia.Diagnostics/Diagnostics/VisualExtensions.cs
  79. 3
      src/Avalonia.Dialogs/ApiCompatBaseline.txt
  80. 2
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  81. 2
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  82. 2
      src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj
  83. 2
      src/Avalonia.Headless/Avalonia.Headless.csproj
  84. 4
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  85. 2
      src/Avalonia.Input/Avalonia.Input.csproj
  86. 2
      src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs
  87. 11
      src/Avalonia.Input/TouchDevice.cs
  88. 2
      src/Avalonia.Interactivity/Avalonia.Interactivity.csproj
  89. 17
      src/Avalonia.Layout/AttachedLayout.cs
  90. 2
      src/Avalonia.Layout/Avalonia.Layout.csproj
  91. 6
      src/Avalonia.MicroCom/MicroComRuntime.cs
  92. 5
      src/Avalonia.MicroCom/MicroComVtblBase.cs
  93. 7
      src/Avalonia.Native/Avalonia.Native.csproj
  94. 10
      src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
  95. 32
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  96. 28
      src/Avalonia.Native/WindowImplBase.cs
  97. 2
      src/Avalonia.OpenGL/Avalonia.OpenGL.csproj
  98. 2
      src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj
  99. 57
      src/Avalonia.ReactiveUI/RoutedViewHost.cs
  100. 52
      src/Avalonia.ReactiveUI/ViewModelViewHost.cs

27
Avalonia.sln

@ -221,8 +221,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "samples\Sandbox\Sandbox.csproj", "{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "samples\Sandbox\Sandbox.csproj", "{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroComGenerator", "src\tools\MicroComGenerator\MicroComGenerator.csproj", "{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.MicroCom", "src\Avalonia.MicroCom\Avalonia.MicroCom.csproj", "{FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.MicroCom", "src\Avalonia.MicroCom\Avalonia.MicroCom.csproj", "{FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniMvvm", "samples\MiniMvvm\MiniMvvm.csproj", "{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniMvvm", "samples\MiniMvvm\MiniMvvm.csproj", "{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}"
@ -2027,30 +2025,6 @@ Global
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhone.Build.0 = Release|Any CPU {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhone.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.AppStore|iPhone.Build.0 = Debug|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Debug|iPhone.Build.0 = Debug|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Release|Any CPU.Build.0 = Release|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Release|iPhone.ActiveCfg = Release|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Release|iPhone.Build.0 = Release|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
@ -2253,7 +2227,6 @@ Global
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
{909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C}
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098} {11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{BC594FD5-4AF2-409E-A1E6-04123F54D7C5} = {9B9E3891-2366-4253-A952-D08BCEB71098} {BC594FD5-4AF2-409E-A1E6-04123F54D7C5} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{25831348-EB2A-483E-9576-E8F6528674A5} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268} {25831348-EB2A-483E-9576-E8F6528674A5} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
{C08E9894-AA92-426E-BF56-033E262CAD3E} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C08E9894-AA92-426E-BF56-033E262CAD3E} = {9B9E3891-2366-4253-A952-D08BCEB71098}

33
azure-pipelines.yml

@ -2,6 +2,33 @@ variables:
MSBuildEnableWorkloadResolver: 'false' MSBuildEnableWorkloadResolver: 'false'
jobs: jobs:
- job: GetPRNumber
pool:
vmImage: 'windows-2022'
variables:
SolutionDir: '$(Build.SourcesDirectory)'
steps:
- task: PowerShell@2
displayName: Get PR Number
inputs:
targetType: 'inline'
script: |
$prId = $env:System_PullRequest_PullRequestNumber
Write-Host "PR Number is:-" $env:System_PullRequest_PullRequestNumber
if (!([string]::IsNullOrWhiteSpace($prId)))
{
Set-Content -Path $env:Build_ArtifactStagingDirectory\prId.txt -Value $prId
}
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'PRNumber'
publishLocation: 'Container'
- job: Linux - job: Linux
pool: pool:
vmImage: 'ubuntu-20.04' vmImage: 'ubuntu-20.04'
@ -58,8 +85,10 @@ jobs:
displayName: 'Generate avalonia-native' displayName: 'Generate avalonia-native'
inputs: inputs:
script: | script: |
export PATH="`pwd`/sdk:$PATH" export COREHOST_TRACE=0
cd src/tools/MicroComGenerator; dotnet run -f net6.0 -i ../../Avalonia.Native/avn.idl --cpp ../../../native/Avalonia.Native/inc/avalonia-native.h export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
export DOTNET_CLI_TELEMETRY_OPTOUT=1
./build.sh --target GenerateCppHeaders --configuration Release
- task: Xcode@5 - task: Xcode@5
inputs: inputs:

35
build/MicroCom.targets

@ -1,35 +0,0 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Ensure that code generator is actually built -->
<ItemGroup>
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\src\tools\MicroComGenerator\MicroComGenerator.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<ExcludeAssets>all</ExcludeAssets>
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
<SetTargetFramework>TargetFramework=net6.0</SetTargetFramework>
</ProjectReference>
</ItemGroup>
<Target Name="GenerateAvaloniaNativeComInterop"
BeforeTargets="CoreCompile"
DependsOnTargets="ResolveReferences"
Inputs="@(AvnComIdl);$(MSBuildThisFileDirectory)../src/tools/MicroComGenerator/**/*.cs"
Outputs="%(AvnComIdl.OutputFile)">
<Message Importance="high" Text="Generating file %(AvnComIdl.OutputFile) from @(AvnComIdl)" />
<Exec Command="dotnet &quot;$(MSBuildThisFileDirectory)../src/tools/MicroComGenerator/bin/$(Configuration)/net6.0/MicroComGenerator.dll&quot; -i @(AvnComIdl) --cs %(AvnComIdl.OutputFile)"
LogStandardErrorAsError="true" />
<ItemGroup>
<!-- Remove and re-add generated file, this is needed for the clean build -->
<Compile Remove="%(AvnComIdl.OutputFile)"/>
<Compile Include="%(AvnComIdl.OutputFile)"/>
</ItemGroup>
</Target>
<ItemGroup>
<UpToDateCheckInput Include="@(AvnComIdl)"/>
<UpToDateCheckInput Include="$(MSBuildThisFileDirectory)/../src/tools/MicroComGenerator/**/*.cs"/>
</ItemGroup>
<PropertyGroup>
<_AvaloniaPatchComInterop>true</_AvaloniaPatchComInterop>
</PropertyGroup>
<Import Project="$(MSBuildThisFileDirectory)/BuildTargets.targets" />
</Project>

8
nukebuild/MicroComGen.cs

@ -1,14 +1,14 @@
using System.IO; using System.IO;
using MicroComGenerator; using MicroCom.CodeGenerator;
using Nuke.Common; using Nuke.Common;
partial class Build : NukeBuild partial class Build : NukeBuild
{ {
Target GenerateCppHeaders => _ => _.Executes(() => Target GenerateCppHeaders => _ => _.Executes(() =>
{ {
var text = File.ReadAllText(RootDirectory / "src" / "Avalonia.Native" / "avn.idl"); var file = MicroComCodeGenerator.Parse(
var ast = AstParser.Parse(text); File.ReadAllText(RootDirectory / "src" / "Avalonia.Native" / "avn.idl"));
File.WriteAllText(RootDirectory / "native" / "Avalonia.Native" / "inc" / "avalonia-native.h", File.WriteAllText(RootDirectory / "native" / "Avalonia.Native" / "inc" / "avalonia-native.h",
CppGen.GenerateCpp(ast)); file.GenerateCppHeader());
}); });
} }

6
nukebuild/_build.csproj

@ -15,7 +15,7 @@
<PackageReference Include="JetBrains.dotMemoryUnit" Version="3.0.20171219.105559" /> <PackageReference Include="JetBrains.dotMemoryUnit" Version="3.0.20171219.105559" />
<PackageReference Include="vswhere" Version="2.6.7" Condition=" '$(OS)' == 'Windows_NT' " /> <PackageReference Include="vswhere" Version="2.6.7" Condition=" '$(OS)' == 'Windows_NT' " />
<PackageReference Include="ILRepack.NETStandard" Version="2.0.4" /> <PackageReference Include="ILRepack.NETStandard" Version="2.0.4" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.7.0" /> <PackageReference Include="MicroCom.CodeGenerator" Version="0.10.4" />
<!-- Keep in sync with Avalonia.Build.Tasks --> <!-- Keep in sync with Avalonia.Build.Tasks -->
<PackageReference Include="Mono.Cecil" Version="0.11.2" /> <PackageReference Include="Mono.Cecil" Version="0.11.2" />
</ItemGroup> </ItemGroup>
@ -37,10 +37,6 @@
<None Include="..\GitVersion.yml" Condition="Exists('..\GitVersion.yml')" /> <None Include="..\GitVersion.yml" Condition="Exists('..\GitVersion.yml')" />
<Compile Remove="Numerge/**/*.*" /> <Compile Remove="Numerge/**/*.*" />
<Compile Include="Numerge/Numerge/**/*.cs" /> <Compile Include="Numerge/Numerge/**/*.cs" />
<Compile Include="..\src\tools\MicroComGenerator\**\*.cs" Exclude="..\src\tools\MicroComGenerator\obj\**">
<Link>MicroComGenerator\%(Filename)%(Extension)</Link>
</Compile>
<Compile Remove="..\src\tools\MicroComGenerator\Program.cs" />
</ItemGroup> </ItemGroup>
</Project> </Project>

2
packages/Avalonia/Avalonia.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net461;netcoreapp2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0;net461;netcoreapp2.0</TargetFrameworks>
<PackageId>Avalonia</PackageId> <PackageId>Avalonia</PackageId>
</PropertyGroup> </PropertyGroup>

1
samples/ControlCatalog.Web/ControlCatalog.Web.csproj

@ -2,6 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<WasmBuildNative>True</WasmBuildNative>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

40
samples/ControlCatalog/App.xaml.cs

@ -5,6 +5,7 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.Styling; using Avalonia.Markup.Xaml.Styling;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Themes.Fluent;
using ControlCatalog.ViewModels; using ControlCatalog.ViewModels;
namespace ControlCatalog namespace ControlCatalog
@ -16,33 +17,17 @@ namespace ControlCatalog
DataContext = new ApplicationViewModel(); DataContext = new ApplicationViewModel();
} }
private static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) public static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{ {
Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml") Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml")
}; };
private static readonly StyleInclude DataGridDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) public static readonly StyleInclude DataGridDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{ {
Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Default.xaml") Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Default.xaml")
}; };
public static Styles FluentDark = new Styles public static FluentTheme Fluent = new FluentTheme(new Uri("avares://ControlCatalog/Styles"));
{
new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/FluentDark.xaml")
},
DataGridFluent
};
public static Styles FluentLight = new Styles
{
new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/FluentLight.xaml")
},
DataGridFluent
};
public static Styles DefaultLight = new Styles public static Styles DefaultLight = new Styles
{ {
@ -65,8 +50,7 @@ namespace ControlCatalog
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{ {
Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml") Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml")
}, }
DataGridDefault
}; };
public static Styles DefaultDark = new Styles public static Styles DefaultDark = new Styles
@ -90,14 +74,13 @@ namespace ControlCatalog
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{ {
Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml") Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml")
}, }
DataGridDefault
}; };
public override void Initialize() public override void Initialize()
{ {
Styles.Insert(0, FluentLight); Styles.Insert(0, Fluent);
Styles.Insert(1, DataGridFluent);
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
@ -106,9 +89,16 @@ namespace ControlCatalog
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
{ {
desktopLifetime.MainWindow = new MainWindow(); desktopLifetime.MainWindow = new MainWindow();
this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()
{
StartupScreenIndex = 1,
});
} }
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
{
singleViewLifetime.MainView = new MainView(); singleViewLifetime.MainView = new MainView();
}
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
} }

1
samples/ControlCatalog/DecoratedWindow.xaml.cs

@ -11,7 +11,6 @@ namespace ControlCatalog
public DecoratedWindow() public DecoratedWindow()
{ {
this.InitializeComponent(); this.InitializeComponent();
this.AttachDevTools();
} }
void SetupSide(string name, StandardCursorType cursor, WindowEdge edge) void SetupSide(string name, StandardCursorType cursor, WindowEdge edge)

42
samples/ControlCatalog/MainView.xaml.cs

@ -3,14 +3,12 @@ using System.Collections;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Markup.Xaml.XamlIl;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Platform; using Avalonia.Platform;
using ControlCatalog.Pages; using Avalonia.Themes.Fluent;
using ControlCatalog.Models; using ControlCatalog.Models;
using ControlCatalog.Pages;
namespace ControlCatalog namespace ControlCatalog
{ {
@ -43,14 +41,36 @@ namespace ControlCatalog
{ {
if (themes.SelectedItem is CatalogTheme theme) if (themes.SelectedItem is CatalogTheme theme)
{ {
Application.Current.Styles[0] = theme switch var themeStyle = Application.Current.Styles[0];
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.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.DataGridFluent;
}
else if (theme == CatalogTheme.DefaultLight)
{
Application.Current.Styles[0] = App.DefaultLight;
Application.Current.Styles[1] = App.DataGridDefault;
}
else if (theme == CatalogTheme.DefaultDark)
{ {
CatalogTheme.FluentLight => App.FluentLight, Application.Current.Styles[0] = App.DefaultDark;
CatalogTheme.FluentDark => App.FluentDark, Application.Current.Styles[1] = App.DataGridDefault;
CatalogTheme.DefaultLight => App.DefaultLight, }
CatalogTheme.DefaultDark => App.DefaultDark,
_ => Application.Current.Styles[0]
};
} }
}; };

5
samples/ControlCatalog/MainWindow.xaml.cs

@ -17,10 +17,7 @@ namespace ControlCatalog
public MainWindow() public MainWindow()
{ {
this.InitializeComponent(); this.InitializeComponent();
this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()
{
StartupScreenIndex = 1,
});
//Renderer.DrawFps = true; //Renderer.DrawFps = true;
//Renderer.DrawDirtyRects = Renderer.DrawFps = true; //Renderer.DrawDirtyRects = Renderer.DrawFps = true;

15
samples/ControlCatalog/Models/Person.cs

@ -16,6 +16,7 @@ namespace ControlCatalog.Models
string _firstName; string _firstName;
string _lastName; string _lastName;
bool _isBanned; bool _isBanned;
private int _age;
public string FirstName public string FirstName
{ {
@ -59,6 +60,20 @@ namespace ControlCatalog.Models
} }
} }
/// <summary>
/// Gets or sets the age of the person
/// </summary>
public int Age
{
get => _age;
set
{
_age = value;
OnPropertyChanged(nameof(Age));
}
}
Dictionary<string, List<string>> _errorLookup = new Dictionary<string, List<string>>(); Dictionary<string, List<string>> _errorLookup = new Dictionary<string, List<string>>();
void SetError(string propertyName, string error) void SetError(string propertyName, string error)

5
samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml

@ -1,5 +1,7 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ControlCatalog.ViewModels"
x:DataType="vm:MainWindowViewModel"
x:Class="ControlCatalog.Pages.CalendarDatePickerPage"> x:Class="ControlCatalog.Pages.CalendarDatePickerPage">
<StackPanel Orientation="Vertical" Spacing="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h2">A control for selecting dates with a calendar drop-down</TextBlock> <TextBlock Classes="h2">A control for selecting dates with a calendar drop-down</TextBlock>
@ -39,6 +41,9 @@
<TextBlock Text="Disabled"/> <TextBlock Text="Disabled"/>
<CalendarDatePicker IsEnabled="False"/> <CalendarDatePicker IsEnabled="False"/>
<TextBlock Text="Validation Example"/>
<CalendarDatePicker SelectedDate="{CompiledBinding ValidatedDateExample, Mode=TwoWay}"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>

12
samples/ControlCatalog/Pages/DataGridPage.xaml

@ -64,6 +64,18 @@
<DataGridTextColumn Header="First Name" Binding="{Binding FirstName}" Width="2*" FontSize="{Binding #FontSizeSlider.Value, Mode=OneWay}" /> <DataGridTextColumn Header="First Name" Binding="{Binding FirstName}" Width="2*" FontSize="{Binding #FontSizeSlider.Value, Mode=OneWay}" />
<DataGridTextColumn Header="Last" Binding="{Binding LastName}" Width="2*" FontSize="{Binding #FontSizeSlider.Value, Mode=OneWay}" /> <DataGridTextColumn Header="Last" Binding="{Binding LastName}" Width="2*" FontSize="{Binding #FontSizeSlider.Value, Mode=OneWay}" />
<DataGridCheckBoxColumn Header="Is Banned" Binding="{Binding IsBanned}" Width="*" IsThreeState="{Binding #IsThreeStateCheckBox.IsChecked, Mode=OneWay}" /> <DataGridCheckBoxColumn Header="Is Banned" Binding="{Binding IsBanned}" Width="*" IsThreeState="{Binding #IsThreeStateCheckBox.IsChecked, Mode=OneWay}" />
<DataGridTemplateColumn Header="Age" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="local:Person">
<TextBlock Text="{Binding Age, StringFormat='{}{0} years'}" VerticalAlignment="Center" HorizontalAlignment="Center" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate DataType="local:Person">
<NumericUpDown Value="{Binding Age}" FormatString="N0" HorizontalAlignment="Stretch" Minimum="0" Maximum="120" TemplateApplied="NumericUpDown_OnTemplateApplied" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
<Button Grid.Row="1" Name="btnAdd" Margin="12,0,12,12" Content="Add" HorizontalAlignment="Right" /> <Button Grid.Row="1" Name="btnAdd" Margin="12,0,12,12" Content="Add" HorizontalAlignment="Right" />

22
samples/ControlCatalog/Pages/DataGridPage.xaml.cs

@ -6,7 +6,9 @@ using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using ControlCatalog.Models; using ControlCatalog.Models;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Primitives;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Threading;
namespace ControlCatalog.Pages namespace ControlCatalog.Pages
{ {
@ -48,9 +50,9 @@ namespace ControlCatalog.Pages
var items = new List<Person> var items = new List<Person>
{ {
new Person { FirstName = "John", LastName = "Doe" }, new Person { FirstName = "John", LastName = "Doe" , Age = 30},
new Person { FirstName = "Elizabeth", LastName = "Thomas", IsBanned = true }, new Person { FirstName = "Elizabeth", LastName = "Thomas", IsBanned = true , Age = 40 },
new Person { FirstName = "Zack", LastName = "Ward" } new Person { FirstName = "Zack", LastName = "Ward" , Age = 50 }
}; };
var collectionView3 = new DataGridCollectionView(items); var collectionView3 = new DataGridCollectionView(items);
@ -84,5 +86,19 @@ namespace ControlCatalog.Pages
return Comparer.Default.Compare(x, y); return Comparer.Default.Compare(x, y);
} }
} }
private void NumericUpDown_OnTemplateApplied(object sender, TemplateAppliedEventArgs e)
{
// We want to focus the TextBox of the NumericUpDown. To do so we search for this control when the template
// is applied, but we postpone the action until the control is actually loaded.
if (e.NameScope.Find<TextBox>("PART_TextBox") is {} textBox)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
textBox.Focus();
textBox.SelectAll();
}, DispatcherPriority.Loaded);
}
}
} }
} }

13
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@ -5,6 +5,7 @@ using Avalonia.Controls.Notifications;
using Avalonia.Dialogs; using Avalonia.Dialogs;
using Avalonia.Platform; using Avalonia.Platform;
using System; using System;
using System.ComponentModel.DataAnnotations;
using MiniMvvm; using MiniMvvm;
namespace ControlCatalog.ViewModels namespace ControlCatalog.ViewModels
@ -164,5 +165,17 @@ namespace ControlCatalog.ViewModels
public MiniCommand ExitCommand { get; } public MiniCommand ExitCommand { get; }
public MiniCommand ToggleMenuItemCheckedCommand { get; } public MiniCommand ToggleMenuItemCheckedCommand { get; }
private DateTime? _validatedDateExample;
/// <summary>
/// A required DateTime which should demonstrate validation for the DateTimePicker
/// </summary>
[Required]
public DateTime? ValidatedDateExample
{
get => _validatedDateExample;
set => this.RaiseAndSetIfChanged(ref _validatedDateExample, value);
}
} }
} }

12
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -33,8 +33,16 @@ namespace Avalonia.Android
{ {
public static readonly AndroidPlatform Instance = new AndroidPlatform(); public static readonly AndroidPlatform Instance = new AndroidPlatform();
public static AndroidPlatformOptions Options { get; private set; } public static AndroidPlatformOptions Options { get; private set; }
public Size DoubleClickSize => new Size(4, 4);
public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(200); /// <inheritdoc cref="IPlatformSettings.TouchDoubleClickSize"/>
public Size TouchDoubleClickSize => new Size(4, 4);
/// <inheritdoc cref="IPlatformSettings.TouchDoubleClickTime"/>
public TimeSpan TouchDoubleClickTime => TimeSpan.FromMilliseconds(200);
public Size DoubleClickSize => TouchDoubleClickSize;
public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(500);
public static void Initialize(Type appType, AndroidPlatformOptions options) public static void Initialize(Type appType, AndroidPlatformOptions options)
{ {

2
src/Avalonia.Animation/Avalonia.Animation.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" /> <Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />

3
src/Avalonia.Base/ApiCompatBaseline.txt

@ -0,0 +1,3 @@
Compat issues with assembly Avalonia.Base:
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Threading.IDispatcher.Post<T>(System.Action<T>, T, Avalonia.Threading.DispatcherPriority)' is present in the implementation but not in the contract.
Total Issues: 1

2
src/Avalonia.Base/Avalonia.Base.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<AssemblyName>Avalonia.Base</AssemblyName> <AssemblyName>Avalonia.Base</AssemblyName>
<RootNamespace>Avalonia</RootNamespace> <RootNamespace>Avalonia</RootNamespace>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>

21
src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs

@ -59,7 +59,7 @@ namespace Avalonia.Collections
} }
private class WeakCollectionChangedObservable : LightweightObservableBase<NotifyCollectionChangedEventArgs>, private class WeakCollectionChangedObservable : LightweightObservableBase<NotifyCollectionChangedEventArgs>,
IWeakSubscriber<NotifyCollectionChangedEventArgs> IWeakEventSubscriber<NotifyCollectionChangedEventArgs>
{ {
private WeakReference<INotifyCollectionChanged> _sourceReference; private WeakReference<INotifyCollectionChanged> _sourceReference;
@ -68,31 +68,22 @@ namespace Avalonia.Collections
_sourceReference = source; _sourceReference = source;
} }
public void OnEvent(object? sender, NotifyCollectionChangedEventArgs e) public void OnEvent(object? sender,
WeakEvent ev,
NotifyCollectionChangedEventArgs e)
{ {
PublishNext(e); PublishNext(e);
} }
protected override void Initialize() protected override void Initialize()
{ {
if (_sourceReference.TryGetTarget(out var instance)) if (_sourceReference.TryGetTarget(out var instance))
{ WeakEvents.CollectionChanged.Subscribe(instance, this);
WeakSubscriptionManager.Subscribe(
instance,
nameof(instance.CollectionChanged),
this);
}
} }
protected override void Deinitialize() protected override void Deinitialize()
{ {
if (_sourceReference.TryGetTarget(out var instance)) if (_sourceReference.TryGetTarget(out var instance))
{ WeakEvents.CollectionChanged.Unsubscribe(instance, this);
WeakSubscriptionManager.Unsubscribe(
instance,
nameof(instance.CollectionChanged),
this);
}
} }
} }
} }

10
src/Avalonia.Base/Data/Core/IndexerNodeBase.cs

@ -23,18 +23,16 @@ namespace Avalonia.Data.Core
if (incc != null) if (incc != null)
{ {
inputs.Add(WeakObservable.FromEventPattern<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>( inputs.Add(WeakObservable.FromEventPattern(
incc, incc, WeakEvents.CollectionChanged)
nameof(incc.CollectionChanged))
.Where(x => ShouldUpdate(x.Sender, x.EventArgs)) .Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target))); .Select(_ => GetValue(target)));
} }
if (inpc != null) if (inpc != null)
{ {
inputs.Add(WeakObservable.FromEventPattern<INotifyPropertyChanged, PropertyChangedEventArgs>( inputs.Add(WeakObservable.FromEventPattern(
inpc, inpc, WeakEvents.PropertyChanged)
nameof(inpc.PropertyChanged))
.Where(x => ShouldUpdate(x.Sender, x.EventArgs)) .Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target))); .Select(_ => GetValue(target)));
} }

20
src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs

@ -11,6 +11,12 @@ namespace Avalonia.Data.Core.Plugins
/// </summary> /// </summary>
public class IndeiValidationPlugin : IDataValidationPlugin public class IndeiValidationPlugin : IDataValidationPlugin
{ {
private static readonly WeakEvent<INotifyDataErrorInfo, DataErrorsChangedEventArgs>
ErrorsChangedWeakEvent = WeakEvent.Register<INotifyDataErrorInfo, DataErrorsChangedEventArgs>(
(s, h) => s.ErrorsChanged += h,
(s, h) => s.ErrorsChanged -= h
);
/// <inheritdoc/> /// <inheritdoc/>
public bool Match(WeakReference<object?> reference, string memberName) public bool Match(WeakReference<object?> reference, string memberName)
{ {
@ -25,7 +31,7 @@ namespace Avalonia.Data.Core.Plugins
return new Validator(reference, name, accessor); return new Validator(reference, name, accessor);
} }
private class Validator : DataValidationBase, IWeakSubscriber<DataErrorsChangedEventArgs> private class Validator : DataValidationBase, IWeakEventSubscriber<DataErrorsChangedEventArgs>
{ {
private readonly WeakReference<object?> _reference; private readonly WeakReference<object?> _reference;
private readonly string _name; private readonly string _name;
@ -37,7 +43,7 @@ namespace Avalonia.Data.Core.Plugins
_name = name; _name = name;
} }
void IWeakSubscriber<DataErrorsChangedEventArgs>.OnEvent(object? sender, DataErrorsChangedEventArgs e) void IWeakEventSubscriber<DataErrorsChangedEventArgs>.OnEvent(object? notifyDataErrorInfo, WeakEvent ev, DataErrorsChangedEventArgs e)
{ {
if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName)) if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName))
{ {
@ -51,10 +57,7 @@ namespace Avalonia.Data.Core.Plugins
if (target != null) if (target != null)
{ {
WeakSubscriptionManager.Subscribe( ErrorsChangedWeakEvent.Subscribe(target, this);
target,
nameof(target.ErrorsChanged),
this);
} }
base.SubscribeCore(); base.SubscribeCore();
@ -66,10 +69,7 @@ namespace Avalonia.Data.Core.Plugins
if (target != null) if (target != null)
{ {
WeakSubscriptionManager.Unsubscribe( ErrorsChangedWeakEvent.Unsubscribe(target, this);
target,
nameof(target.ErrorsChanged),
this);
} }
base.UnsubscribeCore(); base.UnsubscribeCore();

24
src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Reflection; using System.Reflection;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -85,7 +86,7 @@ namespace Avalonia.Data.Core.Plugins
return found; return found;
} }
private class Accessor : PropertyAccessorBase, IWeakSubscriber<PropertyChangedEventArgs> private class Accessor : PropertyAccessorBase, IWeakEventSubscriber<PropertyChangedEventArgs>
{ {
private readonly WeakReference<object?> _reference; private readonly WeakReference<object?> _reference;
private readonly PropertyInfo _property; private readonly PropertyInfo _property;
@ -129,7 +130,8 @@ namespace Avalonia.Data.Core.Plugins
return false; return false;
} }
void IWeakSubscriber<PropertyChangedEventArgs>.OnEvent(object? sender, PropertyChangedEventArgs e) void IWeakEventSubscriber<PropertyChangedEventArgs>.
OnEvent(object? notifyPropertyChanged, WeakEvent ev, PropertyChangedEventArgs e)
{ {
if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName))
{ {
@ -148,13 +150,8 @@ namespace Avalonia.Data.Core.Plugins
{ {
var inpc = GetReferenceTarget() as INotifyPropertyChanged; var inpc = GetReferenceTarget() as INotifyPropertyChanged;
if (inpc != null) if (inpc != null)
{ WeakEvents.PropertyChanged.Unsubscribe(inpc, this);
WeakSubscriptionManager.Unsubscribe(
inpc,
nameof(inpc.PropertyChanged),
this);
}
} }
private object? GetReferenceTarget() private object? GetReferenceTarget()
@ -178,13 +175,8 @@ namespace Avalonia.Data.Core.Plugins
{ {
var inpc = GetReferenceTarget() as INotifyPropertyChanged; var inpc = GetReferenceTarget() as INotifyPropertyChanged;
if (inpc != null) if (inpc != null)
{ WeakEvents.PropertyChanged.Subscribe(inpc, this);
WeakSubscriptionManager.Subscribe(
inpc,
nameof(inpc.PropertyChanged),
this);
}
} }
} }
} }

9
src/Avalonia.Base/Threading/Dispatcher.cs

@ -81,7 +81,7 @@ namespace Avalonia.Threading
_ = action ?? throw new ArgumentNullException(nameof(action)); _ = action ?? throw new ArgumentNullException(nameof(action));
return _jobRunner.InvokeAsync(action, priority); return _jobRunner.InvokeAsync(action, priority);
} }
/// <inheritdoc/> /// <inheritdoc/>
public Task<TResult> InvokeAsync<TResult>(Func<TResult> function, DispatcherPriority priority = DispatcherPriority.Normal) public Task<TResult> InvokeAsync<TResult>(Func<TResult> function, DispatcherPriority priority = DispatcherPriority.Normal)
{ {
@ -110,6 +110,13 @@ namespace Avalonia.Threading
_jobRunner.Post(action, priority); _jobRunner.Post(action, priority);
} }
/// <inheritdoc/>
public void Post<T>(Action<T> action, T arg, DispatcherPriority priority = DispatcherPriority.Normal)
{
_ = action ?? throw new ArgumentNullException(nameof(action));
_jobRunner.Post(action, arg, priority);
}
/// <summary> /// <summary>
/// This is needed for platform backends that don't have internal priority system (e. g. win32) /// This is needed for platform backends that don't have internal priority system (e. g. win32)
/// To ensure that there are no jobs with higher priority /// To ensure that there are no jobs with higher priority

9
src/Avalonia.Base/Threading/IDispatcher.cs

@ -26,6 +26,15 @@ namespace Avalonia.Threading
/// <param name="priority">The priority with which to invoke the method.</param> /// <param name="priority">The priority with which to invoke the method.</param>
void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal); void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal);
/// <summary>
/// Posts an action that will be invoked on the dispatcher thread.
/// </summary>
/// <typeparam name="T">type of argument</typeparam>
/// <param name="action">The method to call.</param>
/// <param name="arg">The argument of method to call.</param>
/// <param name="priority">The priority with which to invoke the method.</param>
void Post<T>(Action<T> action, T arg, DispatcherPriority priority = DispatcherPriority.Normal);
/// <summary> /// <summary>
/// Invokes a action on the dispatcher thread. /// Invokes a action on the dispatcher thread.
/// </summary> /// </summary>

83
src/Avalonia.Base/Threading/JobRunner.cs

@ -13,7 +13,7 @@ namespace Avalonia.Threading
{ {
private IPlatformThreadingInterface? _platform; private IPlatformThreadingInterface? _platform;
private readonly Queue<IJob>[] _queues = Enumerable.Range(0, (int) DispatcherPriority.MaxValue + 1) private readonly Queue<IJob>[] _queues = Enumerable.Range(0, (int)DispatcherPriority.MaxValue + 1)
.Select(_ => new Queue<IJob>()).ToArray(); .Select(_ => new Queue<IJob>()).ToArray();
public JobRunner(IPlatformThreadingInterface? platform) public JobRunner(IPlatformThreadingInterface? platform)
@ -59,7 +59,7 @@ namespace Avalonia.Threading
/// <returns>A task that can be used to track the method's execution.</returns> /// <returns>A task that can be used to track the method's execution.</returns>
public Task<TResult> InvokeAsync<TResult>(Func<TResult> function, DispatcherPriority priority) public Task<TResult> InvokeAsync<TResult>(Func<TResult> function, DispatcherPriority priority)
{ {
var job = new Job<TResult>(function, priority); var job = new JobWithResult<TResult>(function, priority);
AddJob(job); AddJob(job);
return job.Task; return job.Task;
} }
@ -75,6 +75,17 @@ namespace Avalonia.Threading
AddJob(new Job(action, priority, true)); AddJob(new Job(action, priority, true));
} }
/// <summary>
/// Post action that will be invoked on main thread
/// </summary>
/// <param name="action">The method to call.</param>
/// <param name="parameter">The parameter of method to call.</param>
/// <param name="priority">The priority with which to invoke the method.</param>
internal void Post<T>(Action<T> action, T parameter, DispatcherPriority priority)
{
AddJob(new Job<T>(action, parameter, priority, true));
}
/// <summary> /// <summary>
/// Allows unit tests to change the platform threading interface. /// Allows unit tests to change the platform threading interface.
/// </summary> /// </summary>
@ -86,7 +97,7 @@ namespace Avalonia.Threading
private void AddJob(IJob job) private void AddJob(IJob job)
{ {
bool needWake; bool needWake;
var queue = _queues[(int) job.Priority]; var queue = _queues[(int)job.Priority];
lock (queue) lock (queue)
{ {
needWake = queue.Count == 0; needWake = queue.Count == 0;
@ -98,7 +109,7 @@ namespace Avalonia.Threading
private IJob? GetNextJob(DispatcherPriority minimumPriority) private IJob? GetNextJob(DispatcherPriority minimumPriority)
{ {
for (int c = (int) DispatcherPriority.MaxValue; c >= (int) minimumPriority; c--) for (int c = (int)DispatcherPriority.MaxValue; c >= (int)minimumPriority; c--)
{ {
var q = _queues[c]; var q = _queues[c];
lock (q) lock (q)
@ -109,14 +120,14 @@ namespace Avalonia.Threading
} }
return null; return null;
} }
private interface IJob private interface IJob
{ {
/// <summary> /// <summary>
/// Gets the job priority. /// Gets the job priority.
/// </summary> /// </summary>
DispatcherPriority Priority { get; } DispatcherPriority Priority { get; }
/// <summary> /// <summary>
/// Runs the job. /// Runs the job.
/// </summary> /// </summary>
@ -177,11 +188,61 @@ namespace Avalonia.Threading
} }
} }
} }
/// <summary> /// <summary>
/// A job to run. /// A typed job to run.
/// </summary>
/// <typeparam name="T">Type of job parameter</typeparam>
private sealed class Job<T> : IJob
{
private readonly Action<T> _action;
private readonly T _parameter;
private readonly TaskCompletionSource<bool>? _taskCompletionSource;
/// <summary>
/// Initializes a new instance of the <see cref="Job"/> class.
/// </summary>
/// <param name="action">The method to call.</param>
/// <param name="parameter">The parameter of method to call.</param>
/// <param name="priority">The job priority.</param>
/// <param name="throwOnUiThread">Do not wrap exception in TaskCompletionSource</param>
public Job(Action<T> action, T parameter, DispatcherPriority priority, bool throwOnUiThread)
{
_action = action;
_parameter = parameter;
Priority = priority;
_taskCompletionSource = throwOnUiThread ? null : new TaskCompletionSource<bool>();
}
/// <inheritdoc/>
public DispatcherPriority Priority { get; }
/// <inheritdoc/>
void IJob.Run()
{
if (_taskCompletionSource == null)
{
_action(_parameter);
return;
}
try
{
_action(_parameter);
_taskCompletionSource.SetResult(default);
}
catch (Exception e)
{
_taskCompletionSource.SetException(e);
}
}
}
/// <summary>
/// A job to run thath return value.
/// </summary> /// </summary>
private sealed class Job<TResult> : IJob /// <typeparam name="TResult">Type of job result</typeparam>
private sealed class JobWithResult<TResult> : IJob
{ {
private readonly Func<TResult> _function; private readonly Func<TResult> _function;
private readonly TaskCompletionSource<TResult> _taskCompletionSource; private readonly TaskCompletionSource<TResult> _taskCompletionSource;
@ -191,7 +252,7 @@ namespace Avalonia.Threading
/// </summary> /// </summary>
/// <param name="function">The method to call.</param> /// <param name="function">The method to call.</param>
/// <param name="priority">The job priority.</param> /// <param name="priority">The job priority.</param>
public Job(Func<TResult> function, DispatcherPriority priority) public JobWithResult(Func<TResult> function, DispatcherPriority priority)
{ {
_function = function; _function = function;
Priority = priority; Priority = priority;
@ -200,7 +261,7 @@ namespace Avalonia.Threading
/// <inheritdoc/> /// <inheritdoc/>
public DispatcherPriority Priority { get; } public DispatcherPriority Priority { get; }
/// <summary> /// <summary>
/// The task. /// The task.
/// </summary> /// </summary>

12
src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs

@ -0,0 +1,12 @@
using System;
namespace Avalonia.Utilities;
/// <summary>
/// Defines a listener to a event subscribed vis the <see cref="WeakEvent{TTarget, TEventArgs}"/>.
/// </summary>
/// <typeparam name="TEventArgs">The type of the event arguments.</typeparam>
public interface IWeakEventSubscriber<in TEventArgs> where TEventArgs : EventArgs
{
void OnEvent(object? sender, WeakEvent ev, TEventArgs e);
}

187
src/Avalonia.Base/Utilities/WeakEvent.cs

@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Avalonia.Threading;
namespace Avalonia.Utilities;
/// <summary>
/// Manages subscriptions to events using weak listeners.
/// </summary>
public class WeakEvent<TSender, TEventArgs> : WeakEvent where TEventArgs : EventArgs where TSender : class
{
private readonly Func<TSender, EventHandler<TEventArgs>, Action> _subscribe;
readonly ConditionalWeakTable<object, Subscription> _subscriptions = new();
internal WeakEvent(
Action<TSender, EventHandler<TEventArgs>> subscribe,
Action<TSender, EventHandler<TEventArgs>> unsubscribe)
{
_subscribe = (t, s) =>
{
subscribe(t, s);
return () => unsubscribe(t, s);
};
}
internal WeakEvent(Func<TSender, EventHandler<TEventArgs>, Action> subscribe)
{
_subscribe = subscribe;
}
public void Subscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber)
{
if (!_subscriptions.TryGetValue(target, out var subscription))
_subscriptions.Add(target, subscription = new Subscription(this, target));
subscription.Add(new WeakReference<IWeakEventSubscriber<TEventArgs>>(subscriber));
}
public void Unsubscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber)
{
if (_subscriptions.TryGetValue(target, out var subscription))
subscription.Remove(subscriber);
}
private class Subscription
{
private readonly WeakEvent<TSender, TEventArgs> _ev;
private readonly TSender _target;
private readonly Action _compact;
private WeakReference<IWeakEventSubscriber<TEventArgs>>?[] _data =
new WeakReference<IWeakEventSubscriber<TEventArgs>>[16];
private int _count;
private readonly Action _unsubscribe;
private bool _compactScheduled;
public Subscription(WeakEvent<TSender, TEventArgs> ev, TSender target)
{
_ev = ev;
_target = target;
_compact = Compact;
_unsubscribe = ev._subscribe(target, OnEvent);
}
void Destroy()
{
_unsubscribe();
_ev._subscriptions.Remove(_target);
}
public void Add(WeakReference<IWeakEventSubscriber<TEventArgs>> s)
{
if (_count == _data.Length)
{
//Extend capacity
var extendedData = new WeakReference<IWeakEventSubscriber<TEventArgs>>?[_data.Length * 2];
Array.Copy(_data, extendedData, _data.Length);
_data = extendedData;
}
_data[_count] = s;
_count++;
}
public void Remove(IWeakEventSubscriber<TEventArgs> s)
{
var removed = false;
for (int c = 0; c < _count; ++c)
{
var reference = _data[c];
if (reference != null && reference.TryGetTarget(out var instance) && instance == s)
{
_data[c] = null;
removed = true;
}
}
if (removed)
{
ScheduleCompact();
}
}
void ScheduleCompact()
{
if(_compactScheduled)
return;
_compactScheduled = true;
Dispatcher.UIThread.Post(_compact, DispatcherPriority.Background);
}
void Compact()
{
_compactScheduled = false;
int empty = -1;
for (var c = 0; c < _count; c++)
{
var r = _data[c];
//Mark current index as first empty
if (r == null && empty == -1)
empty = c;
//If current element isn't null and we have an empty one
if (r != null && empty != -1)
{
_data[c] = null;
_data[empty] = r;
empty++;
}
}
if (empty != -1)
_count = empty;
if (_count == 0)
Destroy();
}
void OnEvent(object? sender, TEventArgs eventArgs)
{
var needCompact = false;
for (var c = 0; c < _count; c++)
{
var r = _data[c];
if (r?.TryGetTarget(out var sub) == true)
sub!.OnEvent(_target, _ev, eventArgs);
else
needCompact = true;
}
if (needCompact)
ScheduleCompact();
}
}
}
public class WeakEvent
{
public static WeakEvent<TSender, TEventArgs> Register<TSender, TEventArgs>(
Action<TSender, EventHandler<TEventArgs>> subscribe,
Action<TSender, EventHandler<TEventArgs>> unsubscribe) where TSender : class where TEventArgs : EventArgs
{
return new WeakEvent<TSender, TEventArgs>(subscribe, unsubscribe);
}
public static WeakEvent<TSender, TEventArgs> Register<TSender, TEventArgs>(
Func<TSender, EventHandler<TEventArgs>, Action> subscribe) where TSender : class where TEventArgs : EventArgs
{
return new WeakEvent<TSender, TEventArgs>(subscribe);
}
public static WeakEvent<TSender, EventArgs> Register<TSender>(
Action<TSender, EventHandler> subscribe,
Action<TSender, EventHandler> unsubscribe) where TSender : class
{
return Register<TSender, EventArgs>((s, h) =>
{
EventHandler handler = (_, e) => h(s, e);
subscribe(s, handler);
return () => unsubscribe(s, handler);
});
}
}

40
src/Avalonia.Base/Utilities/WeakEvents.cs

@ -0,0 +1,40 @@
using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows.Input;
namespace Avalonia.Utilities;
public class WeakEvents
{
/// <summary>
/// Represents CollectionChanged event from <see cref="INotifyCollectionChanged"/>
/// </summary>
public static readonly WeakEvent<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>
CollectionChanged = WeakEvent.Register<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>(
(c, s) =>
{
NotifyCollectionChangedEventHandler handler = (_, e) => s(c, e);
c.CollectionChanged += handler;
return () => c.CollectionChanged -= handler;
});
/// <summary>
/// Represents PropertyChanged event from <see cref="INotifyPropertyChanged"/>
/// </summary>
public static readonly WeakEvent<INotifyPropertyChanged, PropertyChangedEventArgs>
PropertyChanged = WeakEvent.Register<INotifyPropertyChanged, PropertyChangedEventArgs>(
(s, h) =>
{
PropertyChangedEventHandler handler = (_, e) => h(s, e);
s.PropertyChanged += handler;
return () => s.PropertyChanged -= handler;
});
/// <summary>
/// Represents CanExecuteChanged event from <see cref="ICommand"/>
/// </summary>
public static readonly WeakEvent<ICommand, EventArgs> CommandCanExecuteChanged =
WeakEvent.Register<ICommand>((s, h) => s.CanExecuteChanged += h,
(s, h) => s.CanExecuteChanged -= h);
}

35
src/Avalonia.Base/Utilities/WeakObservable.cs

@ -18,6 +18,7 @@ namespace Avalonia.Utilities
/// <param name="target">Object instance that exposes the event to convert.</param> /// <param name="target">Object instance that exposes the event to convert.</param>
/// <param name="eventName">Name of the event to convert.</param> /// <param name="eventName">Name of the event to convert.</param>
/// <returns></returns> /// <returns></returns>
[Obsolete("Use WeakEvent-based overload")]
public static IObservable<EventPattern<object, TEventArgs>> FromEventPattern<TTarget, TEventArgs>( public static IObservable<EventPattern<object, TEventArgs>> FromEventPattern<TTarget, TEventArgs>(
TTarget target, TTarget target,
string eventName) string eventName)
@ -34,7 +35,9 @@ namespace Avalonia.Utilities
}).Publish().RefCount(); }).Publish().RefCount();
} }
private class Handler<TEventArgs> : IWeakSubscriber<TEventArgs> where TEventArgs : EventArgs private class Handler<TEventArgs>
: IWeakSubscriber<TEventArgs>,
IWeakEventSubscriber<TEventArgs> where TEventArgs : EventArgs
{ {
private IObserver<EventPattern<object, TEventArgs>> _observer; private IObserver<EventPattern<object, TEventArgs>> _observer;
@ -47,6 +50,36 @@ namespace Avalonia.Utilities
{ {
_observer.OnNext(new EventPattern<object, TEventArgs>(sender, e)); _observer.OnNext(new EventPattern<object, TEventArgs>(sender, e));
} }
public void OnEvent(object? sender, WeakEvent ev, TEventArgs e)
{
_observer.OnNext(new EventPattern<object, TEventArgs>(sender, e));
}
} }
/// <summary>
/// Converts a WeakEvent conforming to the standard .NET event pattern into an observable
/// sequence, subscribing weakly.
/// </summary>
/// <typeparam name="TTarget">The type of target.</typeparam>
/// <typeparam name="TEventArgs">The type of the event args.</typeparam>
/// <param name="target">Object instance that exposes the event to convert.</param>
/// <param name="ev">The weak event to convert.</param>
/// <returns></returns>
public static IObservable<EventPattern<object, TEventArgs>> FromEventPattern<TTarget, TEventArgs>(
TTarget target, WeakEvent<TTarget, TEventArgs> ev)
where TEventArgs : EventArgs where TTarget : class
{
_ = target ?? throw new ArgumentNullException(nameof(target));
_ = ev ?? throw new ArgumentNullException(nameof(ev));
return Observable.Create<EventPattern<object, TEventArgs>>(observer =>
{
var handler = new Handler<TEventArgs>(observer);
ev.Subscribe(target, handler);
return () => ev.Unsubscribe(target, handler);
}).Publish().RefCount();
}
} }
} }

3
src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs

@ -19,6 +19,7 @@ namespace Avalonia.Utilities
/// <param name="target">The event source.</param> /// <param name="target">The event source.</param>
/// <param name="eventName">The name of the event.</param> /// <param name="eventName">The name of the event.</param>
/// <param name="subscriber">The subscriber.</param> /// <param name="subscriber">The subscriber.</param>
[Obsolete("Use WeakEvent")]
public static void Subscribe<TTarget, TEventArgs>(TTarget target, string eventName, IWeakSubscriber<TEventArgs> subscriber) public static void Subscribe<TTarget, TEventArgs>(TTarget target, string eventName, IWeakSubscriber<TEventArgs> subscriber)
where TEventArgs : EventArgs where TEventArgs : EventArgs
{ {
@ -180,7 +181,7 @@ namespace Avalonia.Utilities
{ {
var r = _data[c]; var r = _data[c];
if (r?.TryGetTarget(out var sub) == true) if (r?.TryGetTarget(out var sub) == true)
sub.OnEvent(sender, eventArgs); sub!.OnEvent(sender, eventArgs);
else else
needCompact = true; needCompact = true;
} }

2
src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<PackageId>Avalonia.Controls.DataGrid</PackageId> <PackageId>Avalonia.Controls.DataGrid</PackageId>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

1
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -5751,6 +5751,7 @@ namespace Avalonia.Controls
return true; return true;
} }
// Unselect everything except the row that was clicked on // Unselect everything except the row that was clicked on
_noSelectionChangeCount++;
try try
{ {
UpdateSelectionAndCurrency(columnIndex, slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false); UpdateSelectionAndCurrency(columnIndex, slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false);

2
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@ -448,7 +448,7 @@ namespace Avalonia.Controls
internal set; internal set;
} }
public bool IsReadOnly public virtual bool IsReadOnly
{ {
get get
{ {

72
src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs

@ -1,4 +1,4 @@
// (c) Copyright Microsoft Corporation. // (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL). // This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved. // All other rights reserved.
@ -15,7 +15,7 @@ namespace Avalonia.Controls
{ {
public class DataGridTemplateColumn : DataGridColumn public class DataGridTemplateColumn : DataGridColumn
{ {
IDataTemplate _cellTemplate; private IDataTemplate _cellTemplate;
public static readonly DirectProperty<DataGridTemplateColumn, IDataTemplate> CellTemplateProperty = public static readonly DirectProperty<DataGridTemplateColumn, IDataTemplate> CellTemplateProperty =
AvaloniaProperty.RegisterDirect<DataGridTemplateColumn, IDataTemplate>( AvaloniaProperty.RegisterDirect<DataGridTemplateColumn, IDataTemplate>(
@ -30,17 +30,38 @@ namespace Avalonia.Controls
set { SetAndRaise(CellTemplateProperty, ref _cellTemplate, value); } set { SetAndRaise(CellTemplateProperty, ref _cellTemplate, value); }
} }
private IDataTemplate _cellEditingCellTemplate;
/// <summary>
/// Defines the <see cref="CellEditingTemplate"/> property.
/// </summary>
public static readonly DirectProperty<DataGridTemplateColumn, IDataTemplate> CellEditingTemplateProperty =
AvaloniaProperty.RegisterDirect<DataGridTemplateColumn, IDataTemplate>(
nameof(CellEditingTemplate),
o => o.CellEditingTemplate,
(o, v) => o.CellEditingTemplate = v);
/// <summary>
/// Gets or sets the <see cref="IDataTemplate"/> which is used for the editing mode of the current <see cref="DataGridCell"/>
/// </summary>
/// <value>
/// An <see cref="IDataTemplate"/> for the editing mode of the current <see cref="DataGridCell"/>
/// </value>
/// <remarks>
/// If this property is <see langword="null"/> the column is read-only.
/// </remarks>
public IDataTemplate CellEditingTemplate
{
get => _cellEditingCellTemplate;
set => SetAndRaise(CellEditingTemplateProperty, ref _cellEditingCellTemplate, value);
}
private void OnCellTemplateChanged(AvaloniaPropertyChangedEventArgs e) private void OnCellTemplateChanged(AvaloniaPropertyChangedEventArgs e)
{ {
var oldValue = (IDataTemplate)e.OldValue; var oldValue = (IDataTemplate)e.OldValue;
var value = (IDataTemplate)e.NewValue; var value = (IDataTemplate)e.NewValue;
} }
public DataGridTemplateColumn()
{
IsReadOnly = true;
}
protected override IControl GenerateElement(DataGridCell cell, object dataItem) protected override IControl GenerateElement(DataGridCell cell, object dataItem)
{ {
if(CellTemplate != null) if(CellTemplate != null)
@ -60,7 +81,22 @@ namespace Avalonia.Controls
protected override IControl GenerateEditingElement(DataGridCell cell, object dataItem, out ICellEditBinding binding) protected override IControl GenerateEditingElement(DataGridCell cell, object dataItem, out ICellEditBinding binding)
{ {
binding = null; binding = null;
return GenerateElement(cell, dataItem); if(CellEditingTemplate != null)
{
return CellEditingTemplate.Build(dataItem);
}
else if (CellTemplate != null)
{
return CellTemplate.Build(dataItem);
}
if (Design.IsDesignMode)
{
return null;
}
else
{
throw DataGridError.DataGridTemplateColumn.MissingTemplateForType(typeof(DataGridTemplateColumn));
}
} }
protected override object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs) protected override object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs)
@ -70,12 +106,30 @@ namespace Avalonia.Controls
protected internal override void RefreshCellContent(IControl element, string propertyName) protected internal override void RefreshCellContent(IControl element, string propertyName)
{ {
if(propertyName == nameof(CellTemplate) && element.Parent is DataGridCell cell) var cell = element.Parent as DataGridCell;
if(propertyName == nameof(CellTemplate) && cell is not null)
{ {
cell.Content = GenerateElement(cell, cell.DataContext); cell.Content = GenerateElement(cell, cell.DataContext);
} }
base.RefreshCellContent(element, propertyName); base.RefreshCellContent(element, propertyName);
} }
public override bool IsReadOnly
{
get
{
if (CellEditingTemplate is null)
{
return true;
}
return base.IsReadOnly;
}
set
{
base.IsReadOnly = value;
}
}
} }
} }

7
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -29,15 +29,20 @@ MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDownValueChang
MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.NewValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.NewValue.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.StyledProperty<System.Boolean> Avalonia.StyledProperty<System.Boolean> Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.StyledProperty<System.Boolean> Avalonia.StyledProperty<System.Boolean> Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.TopLevel' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Window' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.WindowBase' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs> Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs> Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Embedding.EmbeddableControlRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
MembersMustExist : Member 'public System.Action<Avalonia.Size> Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public System.Action<Avalonia.Size> Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action<Avalonia.Size>)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action<Avalonia.Size>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.Platform.ITopLevelNativeMenuExporter.SetNativeMenu(Avalonia.Controls.NativeMenu)' is present in the contract but not in the implementation. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.Platform.ITopLevelNativeMenuExporter.SetNativeMenu(Avalonia.Controls.NativeMenu)' is present in the contract but not in the implementation.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Primitives.PopupRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract.
@ -57,4 +62,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor
MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract.
Total Issues: 58 Total Issues: 63

2
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" /> <Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />

7
src/Avalonia.Controls/ButtonSpinner.cs

@ -1,4 +1,4 @@
using System; using System;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Data; using Avalonia.Data;
@ -196,13 +196,14 @@ namespace Avalonia.Controls
protected override void OnPointerWheelChanged(PointerWheelEventArgs e) protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{ {
base.OnPointerWheelChanged(e); base.OnPointerWheelChanged(e);
if (!e.Handled && AllowSpin)
if (AllowSpin && IsKeyboardFocusWithin)
{ {
if (e.Delta.Y != 0) if (e.Delta.Y != 0)
{ {
var spinnerEventArgs = new SpinEventArgs(SpinEvent, (e.Delta.Y < 0) ? SpinDirection.Decrease : SpinDirection.Increase, true); var spinnerEventArgs = new SpinEventArgs(SpinEvent, (e.Delta.Y < 0) ? SpinDirection.Decrease : SpinDirection.Increase, true);
OnSpin(spinnerEventArgs); OnSpin(spinnerEventArgs);
e.Handled = spinnerEventArgs.Handled; e.Handled = true;
} }
} }
} }

11
src/Avalonia.Controls/Calendar/CalendarDatePicker.cs

@ -185,7 +185,8 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect<CalendarDatePicker, DateTime?>( AvaloniaProperty.RegisterDirect<CalendarDatePicker, DateTime?>(
nameof(SelectedDate), nameof(SelectedDate),
o => o.SelectedDate, o => o.SelectedDate,
(o, v) => o.SelectedDate = v); (o, v) => o.SelectedDate = v,
enableDataValidation: true);
public static readonly StyledProperty<CalendarDatePickerFormat> SelectedDateFormatProperty = public static readonly StyledProperty<CalendarDatePickerFormat> SelectedDateFormatProperty =
AvaloniaProperty.Register<CalendarDatePicker, CalendarDatePickerFormat>( AvaloniaProperty.Register<CalendarDatePicker, CalendarDatePickerFormat>(
@ -533,13 +534,11 @@ namespace Avalonia.Controls
} }
} }
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{ {
base.OnPropertyChanged(change); if (property == SelectedDateProperty)
if (change.Property == SelectedDateProperty)
{ {
DataValidationErrors.SetError(this, change.NewValue.Error); DataValidationErrors.SetError(this, value.Error);
} }
} }

10
src/Avalonia.Controls/NativeMenuItem.cs

@ -33,7 +33,7 @@ namespace Avalonia.Controls
} }
class CanExecuteChangedSubscriber : IWeakSubscriber<EventArgs> class CanExecuteChangedSubscriber : IWeakEventSubscriber<EventArgs>
{ {
private readonly NativeMenuItem _parent; private readonly NativeMenuItem _parent;
@ -42,7 +42,7 @@ namespace Avalonia.Controls
_parent = parent; _parent = parent;
} }
public void OnEvent(object sender, EventArgs e) public void OnEvent(object? sender, WeakEvent ev, EventArgs e)
{ {
_parent.CanExecuteChanged(); _parent.CanExecuteChanged();
} }
@ -160,14 +160,12 @@ namespace Avalonia.Controls
set set
{ {
if (_command != null) if (_command != null)
WeakSubscriptionManager.Unsubscribe(_command, WeakEvents.CommandCanExecuteChanged.Unsubscribe(_command, _canExecuteChangedSubscriber);
nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber);
SetAndRaise(CommandProperty, ref _command, value); SetAndRaise(CommandProperty, ref _command, value);
if (_command != null) if (_command != null)
WeakSubscriptionManager.Subscribe(_command, WeakEvents.CommandCanExecuteChanged.Subscribe(_command, _canExecuteChangedSubscriber);
nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber);
CanExecuteChanged(); CanExecuteChanged();
} }

32
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -20,7 +20,7 @@ namespace Avalonia.Controls
/// Represents a data-driven collection control that incorporates a flexible layout system, /// Represents a data-driven collection control that incorporates a flexible layout system,
/// custom views, and virtualization. /// custom views, and virtualization.
/// </summary> /// </summary>
public class ItemsRepeater : Panel, IChildIndexProvider public class ItemsRepeater : Panel, IChildIndexProvider, IWeakEventSubscriber<EventArgs>
{ {
/// <summary> /// <summary>
/// Defines the <see cref="HorizontalCacheLength"/> property. /// Defines the <see cref="HorizontalCacheLength"/> property.
@ -723,14 +723,8 @@ namespace Avalonia.Controls
{ {
oldValue.UninitializeForContext(LayoutContext); oldValue.UninitializeForContext(LayoutContext);
WeakEventHandlerManager.Unsubscribe<EventArgs, ItemsRepeater>( AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, this);
oldValue, AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, this);
nameof(AttachedLayout.MeasureInvalidated),
InvalidateMeasureForLayout);
WeakEventHandlerManager.Unsubscribe<EventArgs, ItemsRepeater>(
oldValue,
nameof(AttachedLayout.ArrangeInvalidated),
InvalidateArrangeForLayout);
// Walk through all the elements and make sure they are cleared // Walk through all the elements and make sure they are cleared
foreach (var element in Children) foreach (var element in Children)
@ -748,14 +742,8 @@ namespace Avalonia.Controls
{ {
newValue.InitializeForContext(LayoutContext); newValue.InitializeForContext(LayoutContext);
WeakEventHandlerManager.Subscribe<AttachedLayout, EventArgs, ItemsRepeater>( AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, this);
newValue, AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, this);
nameof(AttachedLayout.MeasureInvalidated),
InvalidateMeasureForLayout);
WeakEventHandlerManager.Subscribe<AttachedLayout, EventArgs, ItemsRepeater>(
newValue,
nameof(AttachedLayout.ArrangeInvalidated),
InvalidateArrangeForLayout);
} }
bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout;
@ -806,9 +794,13 @@ namespace Avalonia.Controls
_viewportManager.OnBringIntoViewRequested(e); _viewportManager.OnBringIntoViewRequested(e);
} }
private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateMeasure(); void IWeakEventSubscriber<EventArgs>.OnEvent(object? sender, WeakEvent ev, EventArgs e)
{
private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateArrange(); if(ev == AttachedLayout.ArrangeInvalidatedWeakEvent)
InvalidateArrange();
else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent)
InvalidateMeasure();
}
private VirtualizingLayoutContext GetLayoutContext() private VirtualizingLayoutContext GetLayoutContext()
{ {

19
src/Avalonia.Controls/TextBox.cs

@ -1006,13 +1006,28 @@ namespace Avalonia.Controls
if (text != null && clickInfo.Properties.IsLeftButtonPressed && !(clickInfo.Pointer?.Captured is Border)) if (text != null && clickInfo.Properties.IsLeftButtonPressed && !(clickInfo.Pointer?.Captured is Border))
{ {
var point = e.GetPosition(_presenter); var point = e.GetPosition(_presenter);
var index = CaretIndex = _presenter.GetCaretIndex(point); var index = _presenter.GetCaretIndex(point);
var clickToSelect = index != CaretIndex && e.KeyModifiers.HasFlag(KeyModifiers.Shift);
if (!clickToSelect)
{
CaretIndex = index;
}
#pragma warning disable CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete
switch (e.ClickCount) switch (e.ClickCount)
#pragma warning restore CS0618 // Type or member is obsolete #pragma warning restore CS0618 // Type or member is obsolete
{ {
case 1: case 1:
SelectionStart = SelectionEnd = index; if (clickToSelect)
{
SelectionStart = Math.Min(index, CaretIndex);
SelectionEnd = Math.Max(index, CaretIndex);
}
else
{
SelectionStart = SelectionEnd = index;
}
break; break;
case 2: case 2:
if (!StringUtils.IsStartOfWord(text, index)) if (!StringUtils.IsStartOfWord(text, index))

15
src/Avalonia.Controls/TopLevel.cs

@ -34,7 +34,7 @@ namespace Avalonia.Controls
IStyleHost, IStyleHost,
ILogicalRoot, ILogicalRoot,
ITextInputMethodRoot, ITextInputMethodRoot,
IWeakSubscriber<ResourcesChangedEventArgs> IWeakEventSubscriber<ResourcesChangedEventArgs>
{ {
/// <summary> /// <summary>
/// Defines the <see cref="ClientSize"/> property. /// Defines the <see cref="ClientSize"/> property.
@ -74,6 +74,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IBrush> TransparencyBackgroundFallbackProperty = public static readonly StyledProperty<IBrush> TransparencyBackgroundFallbackProperty =
AvaloniaProperty.Register<TopLevel, IBrush>(nameof(TransparencyBackgroundFallback), Brushes.White); AvaloniaProperty.Register<TopLevel, IBrush>(nameof(TransparencyBackgroundFallback), Brushes.White);
private static readonly WeakEvent<IResourceHost, ResourcesChangedEventArgs>
ResourcesChangedWeakEvent = WeakEvent.Register<IResourceHost, ResourcesChangedEventArgs>(
(s, h) => s.ResourcesChanged += h,
(s, h) => s.ResourcesChanged -= h
);
private readonly IInputManager _inputManager; private readonly IInputManager _inputManager;
private readonly IAccessKeyHandler _accessKeyHandler; private readonly IAccessKeyHandler _accessKeyHandler;
private readonly IKeyboardNavigationHandler _keyboardNavigationHandler; private readonly IKeyboardNavigationHandler _keyboardNavigationHandler;
@ -178,10 +184,7 @@ namespace Avalonia.Controls
if (((IStyleHost)this).StylingParent is IResourceHost applicationResources) if (((IStyleHost)this).StylingParent is IResourceHost applicationResources)
{ {
WeakSubscriptionManager.Subscribe( ResourcesChangedWeakEvent.Subscribe(applicationResources, this);
applicationResources,
nameof(IResourceHost.ResourcesChanged),
this);
} }
impl.LostFocus += PlatformImpl_LostFocus; impl.LostFocus += PlatformImpl_LostFocus;
@ -286,7 +289,7 @@ namespace Avalonia.Controls
/// <inheritdoc/> /// <inheritdoc/>
IMouseDevice IInputRoot.MouseDevice => PlatformImpl?.MouseDevice; IMouseDevice IInputRoot.MouseDevice => PlatformImpl?.MouseDevice;
void IWeakSubscriber<ResourcesChangedEventArgs>.OnEvent(object sender, ResourcesChangedEventArgs e) void IWeakEventSubscriber<ResourcesChangedEventArgs>.OnEvent(object sender, WeakEvent ev, ResourcesChangedEventArgs e)
{ {
((ILogical)this).NotifyResourcesChanged(e); ((ILogical)this).NotifyResourcesChanged(e);
} }

15
src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs

@ -83,7 +83,7 @@ namespace Avalonia.Controls.Utils
"Collection listener not registered for this collection/listener combination."); "Collection listener not registered for this collection/listener combination.");
} }
private class Entry : IWeakSubscriber<NotifyCollectionChangedEventArgs>, IDisposable private class Entry : IWeakEventSubscriber<NotifyCollectionChangedEventArgs>, IDisposable
{ {
private INotifyCollectionChanged _collection; private INotifyCollectionChanged _collection;
@ -91,23 +91,18 @@ namespace Avalonia.Controls.Utils
{ {
_collection = collection; _collection = collection;
Listeners = new List<WeakReference<ICollectionChangedListener>>(); Listeners = new List<WeakReference<ICollectionChangedListener>>();
WeakSubscriptionManager.Subscribe( WeakEvents.CollectionChanged.Subscribe(_collection, this);
_collection,
nameof(INotifyCollectionChanged.CollectionChanged),
this);
} }
public List<WeakReference<ICollectionChangedListener>> Listeners { get; } public List<WeakReference<ICollectionChangedListener>> Listeners { get; }
public void Dispose() public void Dispose()
{ {
WeakSubscriptionManager.Unsubscribe( WeakEvents.CollectionChanged.Unsubscribe(_collection, this);
_collection,
nameof(INotifyCollectionChanged.CollectionChanged),
this);
} }
void IWeakSubscriber<NotifyCollectionChangedEventArgs>.OnEvent(object? sender, NotifyCollectionChangedEventArgs e) void IWeakEventSubscriber<NotifyCollectionChangedEventArgs>.
OnEvent(object? notifyCollectionChanged, WeakEvent ev, NotifyCollectionChangedEventArgs e)
{ {
static void Notify( static void Notify(
INotifyCollectionChanged incc, INotifyCollectionChanged incc,

2
src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<!-- WARNING! The designer support version number needs to be frozen <!-- WARNING! The designer support version number needs to be frozen
To allow projects that implement designer functionality to still To allow projects that implement designer functionality to still
work with newer versions of Avalonia. This version number only work with newer versions of Avalonia. This version number only

4
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@ -64,5 +64,9 @@ namespace Avalonia.DesignerSupport.Remote
public Size DoubleClickSize { get; } = new Size(2, 2); public Size DoubleClickSize { get; } = new Size(2, 2);
public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500); public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500);
public Size TouchDoubleClickSize => new Size(16, 16);
public TimeSpan TouchDoubleClickTime => DoubleClickTime;
} }
} }

2
src/Avalonia.Desktop/Avalonia.Desktop.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<PackageId>Avalonia.Desktop</PackageId> <PackageId>Avalonia.Desktop</PackageId>
</PropertyGroup> </PropertyGroup>

2
src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net461;netcoreapp2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;net461;netcoreapp2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

2
src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<RootNamespace>Avalonia</RootNamespace> <RootNamespace>Avalonia</RootNamespace>
<PackageId>Avalonia.Diagnostics</PackageId> <PackageId>Avalonia.Diagnostics</PackageId>
</PropertyGroup> </PropertyGroup>

44
src/Avalonia.Diagnostics/DevToolsExtensions.cs

@ -37,5 +37,49 @@ namespace Avalonia
{ {
DevTools.Attach(root, options); DevTools.Attach(root, options);
} }
/// <summary>
/// Attaches DevTools to a Application, to be opened with the specified options.
/// </summary>
/// <param name="application">The Application to attach DevTools to.</param>
public static void AttachDevTools(this Application application)
{
DevTools.Attach(application, new DevToolsOptions());
}
/// <summary>
/// Attaches DevTools to a Application, to be opened with the specified options.
/// </summary>
/// <param name="application">The Application to attach DevTools to.</param>
/// <param name="options">Additional settings of DevTools.</param>
/// <remarks>
/// Attach DevTools should only be called after application initialization is complete. A good point is <see cref="Application.OnFrameworkInitializationCompleted"/>
/// </remarks>
/// <example>
/// <code>
/// public class App : Application
/// {
/// public override void OnFrameworkInitializationCompleted()
/// {
/// if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
/// {
/// desktopLifetime.MainWindow = new MainWindow();
/// }
/// else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
/// singleViewLifetime.MainView = new MainView();
///
/// base.OnFrameworkInitializationCompleted();
/// this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()
/// {
/// StartupScreenIndex = 1,
/// });
/// }
/// }
/// </code>
/// </example>
public static void AttachDevTools(this Application application, DevToolsOptions options)
{
DevTools.Attach(application, options);
}
} }
} }

47
src/Avalonia.Diagnostics/Diagnostics/Behaviors/ColumnDefinition.cs

@ -0,0 +1,47 @@
namespace Avalonia.Diagnostics.Behaviors
{
/// <summary>
/// See discussion https://github.com/AvaloniaUI/Avalonia/discussions/6773
/// </summary>
static class ColumnDefinition
{
private readonly static Avalonia.Controls.GridLength ZeroWidth =
new Avalonia.Controls.GridLength(0, Avalonia.Controls.GridUnitType.Pixel);
private readonly static AttachedProperty<Avalonia.Controls.GridLength?> LastWidthProperty =
AvaloniaProperty.RegisterAttached<Avalonia.Controls.ColumnDefinition, Avalonia.Controls.GridLength?>("LastWidth"
, typeof(ColumnDefinition)
, default);
public readonly static AttachedProperty<bool> IsVisibleProperty =
AvaloniaProperty.RegisterAttached<Avalonia.Controls.ColumnDefinition, bool>("IsVisible"
, typeof(ColumnDefinition)
, true
, coerce: (element, visibility) =>
{
var lastWidth = element.GetValue(LastWidthProperty);
if (visibility == true && lastWidth is { })
{
element.SetValue(Avalonia.Controls.ColumnDefinition.WidthProperty, lastWidth);
}
else if (visibility == false)
{
element.SetValue(LastWidthProperty, element.GetValue(Avalonia.Controls.ColumnDefinition.WidthProperty));
element.SetValue(Avalonia.Controls.ColumnDefinition.WidthProperty, ZeroWidth);
}
return visibility;
}
);
public static bool GetIsVisible(Avalonia.Controls.ColumnDefinition columnDefinition)
{
return columnDefinition.GetValue(IsVisibleProperty);
}
public static void SetIsVisible(Avalonia.Controls.ColumnDefinition columnDefinition, bool visibility)
{
columnDefinition.SetValue(IsVisibleProperty, visibility);
}
}
}

119
src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs

@ -0,0 +1,119 @@
using System;
using Avalonia.Controls;
using Lifetimes = Avalonia.Controls.ApplicationLifetimes;
using App = Avalonia.Application;
namespace Avalonia.Diagnostics.Controls
{
class Application : AvaloniaObject
, Input.ICloseable
{
private readonly App _application;
private static readonly Version s_version = typeof(IAvaloniaObject).Assembly?.GetName()?.Version
?? Version.Parse("0.0.00");
public event EventHandler? Closed;
public Application(App application)
{
_application = application;
if (_application.ApplicationLifetime is Lifetimes.IControlledApplicationLifetime controller)
{
EventHandler<Lifetimes.ControlledApplicationLifetimeExitEventArgs> eh = default!;
eh = (s, e) =>
{
controller.Exit -= eh;
Closed?.Invoke(s, e);
};
controller.Exit += eh;
}
RendererRoot = application.ApplicationLifetime switch
{
Lifetimes.IClassicDesktopStyleApplicationLifetime classic => classic.MainWindow.Renderer,
Lifetimes.ISingleViewApplicationLifetime single => (single.MainView as VisualTree.IVisual)?.VisualRoot?.Renderer,
_ => null
};
}
internal App Instance => _application;
/// <summary>
/// Defines the <see cref="DataContext"/> property.
/// </summary>
public object? DataContext =>
_application.DataContext;
/// <summary>
/// Gets or sets the application's global data templates.
/// </summary>
/// <value>
/// The application's global data templates.
/// </value>
public Avalonia.Controls.Templates.DataTemplates DataTemplates =>
_application.DataTemplates;
/// <summary>
/// Gets the application's focus manager.
/// </summary>
/// <value>
/// The application's focus manager.
/// </value>
public Input.IFocusManager? FocusManager =>
_application.FocusManager;
/// <summary>
/// Gets the application's input manager.
/// </summary>
/// <value>
/// The application's input manager.
/// </value>
public Input.InputManager? InputManager =>
_application.InputManager;
/// <summary>
/// Gets the application clipboard.
/// </summary>
public Input.Platform.IClipboard? Clipboard =>
_application.Clipboard;
/// <summary>
/// Gets the application's global resource dictionary.
/// </summary>
public IResourceDictionary Resources =>
_application.Resources;
/// <summary>
/// Gets the application's global styles.
/// </summary>
/// <value>
/// The application's global styles.
/// </value>
/// <remarks>
/// Global styles apply to all windows in the application.
/// </remarks>
public Styling.Styles Styles =>
_application.Styles;
/// <summary>
/// Application lifetime, use it for things like setting the main window and exiting the app from code
/// Currently supported lifetimes are:
/// - <see cref="Lifetimes.IClassicDesktopStyleApplicationLifetime"/>
/// - <see cref="Lifetimes.ISingleViewApplicationLifetime"/>
/// - <see cref="Lifetimes.IControlledApplicationLifetime"/>
/// </summary>
public Lifetimes.IApplicationLifetime? ApplicationLifetime =>
_application.ApplicationLifetime;
/// <summary>
/// Application name to be used for various platform-specific purposes
/// </summary>
public string? Name =>
_application.Name;
/// <summary>
/// Gets the root of the visual tree, if the control is attached to a visual tree.
/// </summary>
internal Rendering.IRenderer? RendererRoot { get; }
}
}

24
src/Avalonia.Diagnostics/Diagnostics/Converters/GetTypeNameConverter.cs

@ -0,0 +1,24 @@
using System;
using System.Globalization;
using Avalonia.Data;
using Avalonia.Data.Converters;
namespace Avalonia.Diagnostics.Converters
{
internal class GetTypeNameConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is Type type)
{
return type.GetTypeName();
}
return BindingOperations.DoNothing;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return BindingOperations.DoNothing;
}
}
}

17
src/Avalonia.Diagnostics/Diagnostics/Convetions.cs

@ -0,0 +1,17 @@
using System;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics
{
static class Convetions
{
public static string DefaultScreenshotsRoot =>
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures, Environment.SpecialFolderOption.Create),
"Screenshots");
public static IScreenshotHandler DefaultScreenshotHandler { get; } =
new Screenshots.FilePickerHandler();
}
}

95
src/Avalonia.Diagnostics/Diagnostics/DevTools.cs

@ -1,17 +1,21 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Diagnostics.Views; using Avalonia.Diagnostics.Views;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity; using Avalonia.Interactivity;
namespace Avalonia.Diagnostics namespace Avalonia.Diagnostics
{ {
public static class DevTools public static class DevTools
{ {
private static readonly Dictionary<TopLevel, MainWindow> s_open = private static readonly Dictionary<AvaloniaObject, MainWindow> s_open =
new Dictionary<TopLevel, MainWindow>(); new Dictionary<AvaloniaObject, MainWindow>();
private static bool s_attachedToApplication;
public static IDisposable Attach(TopLevel root, KeyGesture gesture) public static IDisposable Attach(TopLevel root, KeyGesture gesture)
{ {
@ -23,6 +27,11 @@ namespace Avalonia.Diagnostics
public static IDisposable Attach(TopLevel root, DevToolsOptions options) public static IDisposable Attach(TopLevel root, DevToolsOptions options)
{ {
if (s_attachedToApplication == true)
{
throw new ArgumentException("DevTools already attached to application", nameof(root));
}
void PreviewKeyDown(object? sender, KeyEventArgs e) void PreviewKeyDown(object? sender, KeyEventArgs e)
{ {
if (options.Gesture.Matches(e)) if (options.Gesture.Matches(e))
@ -37,45 +46,95 @@ namespace Avalonia.Diagnostics
RoutingStrategies.Tunnel); RoutingStrategies.Tunnel);
} }
public static IDisposable Open(TopLevel root) => Open(root, new DevToolsOptions()); public static IDisposable Open(TopLevel root) =>
Open(Application.Current,new DevToolsOptions(),root as Window);
public static IDisposable Open(TopLevel root, DevToolsOptions options) =>
Open(Application.Current, options, root as Window);
public static IDisposable Open(TopLevel root, DevToolsOptions options) private static void DevToolsClosed(object? sender, EventArgs e)
{ {
if (s_open.TryGetValue(root, out var window)) var window = (MainWindow)sender!;
window.Closed -= DevToolsClosed;
if (window.Root is Controls.Application host)
{
s_open.Remove(host.Instance);
}
else
{ {
s_open.Remove(window.Root!);
}
}
internal static IDisposable Attach(Application? application, DevToolsOptions options, Window? owner = null)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
var result = Disposable.Empty;
// Skip if call on Design Mode
if (!Avalonia.Controls.Design.IsDesignMode
&& !s_attachedToApplication)
{
var lifeTime = application.ApplicationLifetime
as Avalonia.Controls.ApplicationLifetimes.IControlledApplicationLifetime;
if (lifeTime is null)
{
throw new ArgumentNullException(nameof(Application.ApplicationLifetime));
}
if (application.InputManager is { })
{
s_attachedToApplication = true;
application.InputManager.PreProcess.OfType<RawKeyEventArgs>().Subscribe(e =>
{
if (options.Gesture.Matches(e))
{
result = Open(application, options, owner);
}
});
}
}
return result;
}
private static IDisposable Open(Application? application, DevToolsOptions options, Window? owner = default)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
if (s_open.TryGetValue(application, out var window))
{
window.Activate(); window.Activate();
} }
else else
{ {
window = new MainWindow window = new MainWindow
{ {
Root = root, Root = new Controls.Application(application),
Width = options.Size.Width, Width = options.Size.Width,
Height = options.Size.Height, Height = options.Size.Height,
}; };
window.SetOptions(options); window.SetOptions(options);
window.Closed += DevToolsClosed; window.Closed += DevToolsClosed;
s_open.Add(root, window); s_open.Add(application, window);
if (options.ShowAsChildWindow && owner is { })
if (options.ShowAsChildWindow && root is Window inspectedWindow)
{ {
window.Show(inspectedWindow); window.Show(owner);
} }
else else
{ {
window.Show(); window.Show();
} }
} }
return Disposable.Create(() => window?.Close()); return Disposable.Create(() => window?.Close());
} }
private static void DevToolsClosed(object? sender, EventArgs e)
{
var window = (MainWindow)sender!;
s_open.Remove(window.Root!);
window.Closed -= DevToolsClosed;
}
} }
} }

16
src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs

@ -1,4 +1,5 @@
using Avalonia.Input; using System;
using Avalonia.Input;
namespace Avalonia.Diagnostics namespace Avalonia.Diagnostics
{ {
@ -16,6 +17,7 @@ namespace Avalonia.Diagnostics
/// Gets or sets a value indicating whether DevTools should be displayed as a child window /// Gets or sets a value indicating whether DevTools should be displayed as a child window
/// of the window being inspected. The default value is true. /// of the window being inspected. The default value is true.
/// </summary> /// </summary>
/// <remarks>This setting is ignored if DevTools is attached to <see cref="Application"/></remarks>
public bool ShowAsChildWindow { get; set; } = true; public bool ShowAsChildWindow { get; set; } = true;
/// <summary> /// <summary>
@ -27,5 +29,17 @@ namespace Avalonia.Diagnostics
/// Get or set the startup screen index where the DevTools window will be displayed. /// Get or set the startup screen index where the DevTools window will be displayed.
/// </summary> /// </summary>
public int? StartupScreenIndex { get; set; } public int? StartupScreenIndex { get; set; }
/// <summary>
/// Gets or sets a value indicating whether DevTools should be displayed implemented interfaces on Control details. The default value is true.
/// </summary>
public bool ShowImplementedInterfaces { get; set; } = true;
/// <summary>
/// Allow to customizze SreenshotHandler
/// </summary>
/// <remarks>Default handler is <see cref="Screenshots.FilePickerHandler"/></remarks>
public IScreenshotHandler ScreenshotHandler { get; set; }
= Convetions.DefaultScreenshotHandler;
} }
} }

17
src/Avalonia.Diagnostics/Diagnostics/IScreenshotHandler.cs

@ -0,0 +1,17 @@
using System.Threading.Tasks;
using Avalonia.Controls;
namespace Avalonia.Diagnostics
{
/// <summary>
/// Allowed to define custom handler for Shreeshot
/// </summary>
public interface IScreenshotHandler
{
/// <summary>
/// Handle the Screenshot
/// </summary>
/// <returns></returns>
Task Take(IControl control);
}
}

28
src/Avalonia.Diagnostics/Diagnostics/KeyGestureExtesions.cs

@ -0,0 +1,28 @@
using Avalonia.Input;
using Avalonia.Input.Raw;
namespace Avalonia.Diagnostics
{
static class KeyGestureExtesions
{
public static bool Matches(this KeyGesture gesture, RawKeyEventArgs keyEvent) =>
keyEvent != null &&
(KeyModifiers)(keyEvent.Modifiers & RawInputModifiers.KeyboardMask) == gesture.KeyModifiers &&
ResolveNumPadOperationKey(keyEvent.Key) == ResolveNumPadOperationKey(gesture.Key);
private static Key ResolveNumPadOperationKey(Key key)
{
switch (key)
{
case Key.Add:
return Key.OemPlus;
case Key.Subtract:
return Key.OemMinus;
case Key.Decimal:
return Key.OemPeriod;
default:
return key;
}
}
}
}

29
src/Avalonia.Diagnostics/Diagnostics/Screenshots/BaseRenderToStreamHandler.cs

@ -0,0 +1,29 @@
using System.Threading.Tasks;
using Avalonia.Controls;
namespace Avalonia.Diagnostics.Screenshots
{
/// <summary>
/// Base class for render Screenshto to stream
/// </summary>
public abstract class BaseRenderToStreamHandler : IScreenshotHandler
{
/// <summary>
/// Get stream
/// </summary>
/// <param name="control"></param>
/// <returns>stream to render the control</returns>
protected abstract Task<System.IO.Stream?> GetStream(IControl control);
public async Task Take(IControl control)
{
using var output = await GetStream(control);
if (output is { })
{
control.RenderTo(output);
await output.FlushAsync();
}
}
}
}

85
src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs

@ -0,0 +1,85 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Lifetimes = Avalonia.Controls.ApplicationLifetimes;
namespace Avalonia.Diagnostics.Screenshots
{
/// <summary>
/// Show a FileSavePicker to select where save screenshot
/// </summary>
public sealed class FilePickerHandler : BaseRenderToStreamHandler
{
/// <summary>
/// Instance FilePickerHandler
/// </summary>
public FilePickerHandler()
{
}
/// <summary>
/// Instance FilePickerHandler with specificated parameter
/// </summary>
/// <param name="title">SaveFilePicker Title</param>
/// <param name="screenshotRoot"></param>
public FilePickerHandler(string? title
, string? screenshotRoot = default
)
{
if (title is { })
Title = title;
if (screenshotRoot is { })
ScreenshotsRoot = screenshotRoot;
}
/// <summary>
/// Get the root folder where screeshots well be stored.
/// The default root folder is [Environment.SpecialFolder.MyPictures]/Screenshots.
/// </summary>
public string ScreenshotsRoot { get; }
= Convetions.DefaultScreenshotsRoot;
/// <summary>
/// SaveFilePicker Title
/// </summary>
public string Title { get; } = "Save Screenshot to ...";
Window GetWindow(IControl control)
{
var window = control.VisualRoot as Window;
var app = Application.Current;
if (app?.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime desktop)
{
window = desktop.Windows.FirstOrDefault(w => w is Views.MainWindow);
}
return window!;
}
protected async override Task<Stream?> GetStream(IControl control)
{
Stream? output = default;
var result = await new SaveFileDialog()
{
Title = Title,
Filters = new() { new FileDialogFilter() { Name = "PNG", Extensions = new() { "png" } } },
Directory = ScreenshotsRoot,
}.ShowAsync(GetWindow(control));
if (!string.IsNullOrWhiteSpace(result))
{
var foldler = Path.GetDirectoryName(result);
// Directory information for path, or null if path denotes a root directory or is
// null. Returns System.String.Empty if path does not contain directory information.
if (!string.IsNullOrWhiteSpace(foldler))
{
if (!Directory.Exists(foldler))
{
Directory.CreateDirectory(foldler);
}
output = new FileStream(result, FileMode.Create);
}
}
return output;
}
}
}

33
src/Avalonia.Diagnostics/Diagnostics/TypeExtesnions.cs

@ -0,0 +1,33 @@
using System;
using System.Runtime.CompilerServices;
using System.Linq;
namespace Avalonia.Diagnostics
{
internal static class TypeExtesnions
{
private static readonly ConditionalWeakTable<Type, string> s_getTypeNameCache =
new ConditionalWeakTable<Type, string>();
public static string GetTypeName(this Type type)
{
if (!s_getTypeNameCache.TryGetValue(type, out var name))
{
name = type.Name;
if (Nullable.GetUnderlyingType(type) is Type nullable)
{
name = nullable.Name + "?";
}
else if (type.IsGenericType)
{
var definition = type.GetGenericTypeDefinition();
var arguments = type.GetGenericArguments();
name = definition.Name.Substring(0, definition.Name.IndexOf('`'));
name = $"{name}<{string.Join(",", arguments.Select(GetTypeName))}>";
}
s_getTypeNameCache.Add(type, name);
}
return name;
}
}
}

13
src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs

@ -3,10 +3,11 @@ namespace Avalonia.Diagnostics.ViewModels
internal class AvaloniaPropertyViewModel : PropertyViewModel internal class AvaloniaPropertyViewModel : PropertyViewModel
{ {
private readonly AvaloniaObject _target; private readonly AvaloniaObject _target;
private System.Type _type; private System.Type _assignedType;
private object? _value; private object? _value;
private string _priority; private string _priority;
private string _group; private string _group;
private readonly System.Type _propertyType;
#nullable disable #nullable disable
// Remove "nullable disable" after MemberNotNull will work on our CI. // Remove "nullable disable" after MemberNotNull will work on our CI.
@ -20,6 +21,7 @@ namespace Avalonia.Diagnostics.ViewModels
$"[{property.OwnerType.Name}.{property.Name}]" : $"[{property.OwnerType.Name}.{property.Name}]" :
property.Name; property.Name;
DeclaringType = property.OwnerType; DeclaringType = property.OwnerType;
_propertyType = property.PropertyType;
Update(); Update();
} }
@ -32,7 +34,7 @@ namespace Avalonia.Diagnostics.ViewModels
public override string Priority => public override string Priority =>
_priority; _priority;
public override System.Type Type => _type; public override System.Type AssignedType => _assignedType;
public override string? Value public override string? Value
{ {
@ -43,6 +45,7 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
var convertedValue = ConvertFromString(value, Property.PropertyType); var convertedValue = ConvertFromString(value, Property.PropertyType);
_target.SetValue(Property, convertedValue); _target.SetValue(Property, convertedValue);
Update();
} }
catch { } catch { }
} }
@ -51,6 +54,7 @@ namespace Avalonia.Diagnostics.ViewModels
public override string Group => _group; public override string Group => _group;
public override System.Type? DeclaringType { get; } public override System.Type? DeclaringType { get; }
public override System.Type PropertyType => _propertyType;
// [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))] // [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))]
public override void Update() public override void Update()
@ -58,7 +62,7 @@ namespace Avalonia.Diagnostics.ViewModels
if (Property.IsDirect) if (Property.IsDirect)
{ {
RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value)); RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value));
RaiseAndSetIfChanged(ref _type, _value?.GetType() ?? Property.PropertyType, nameof(Type)); RaiseAndSetIfChanged(ref _assignedType,_value?.GetType() ?? Property.PropertyType, nameof(AssignedType));
RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority)); RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority));
_group = "Properties"; _group = "Properties";
@ -68,7 +72,7 @@ namespace Avalonia.Diagnostics.ViewModels
var val = _target.GetDiagnostic(Property); var val = _target.GetDiagnostic(Property);
RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value)); RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value));
RaiseAndSetIfChanged(ref _type, _value?.GetType() ?? Property.PropertyType, nameof(Type)); RaiseAndSetIfChanged(ref _assignedType, _value?.GetType() ?? Property.PropertyType, nameof(AssignedType));
if (val != null) if (val != null)
{ {
@ -81,6 +85,7 @@ namespace Avalonia.Diagnostics.ViewModels
RaiseAndSetIfChanged(ref _group, "Unset", nameof(Group)); RaiseAndSetIfChanged(ref _group, "Unset", nameof(Group));
} }
} }
RaisePropertyChanged(nameof(Type));
} }
} }
} }

12
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs

@ -5,8 +5,9 @@ namespace Avalonia.Diagnostics.ViewModels
internal class ClrPropertyViewModel : PropertyViewModel internal class ClrPropertyViewModel : PropertyViewModel
{ {
private readonly object _target; private readonly object _target;
private System.Type _type; private System.Type _assignedType;
private object? _value; private object? _value;
private readonly System.Type _propertyType;
#nullable disable #nullable disable
// Remove "nullable disable" after MemberNotNull will work on our CI. // Remove "nullable disable" after MemberNotNull will work on our CI.
@ -25,6 +26,8 @@ namespace Avalonia.Diagnostics.ViewModels
Name = property.DeclaringType.Name + '.' + property.Name; Name = property.DeclaringType.Name + '.' + property.Name;
} }
DeclaringType = property.DeclaringType; DeclaringType = property.DeclaringType;
_propertyType = property.PropertyType;
Update(); Update();
} }
@ -33,7 +36,8 @@ namespace Avalonia.Diagnostics.ViewModels
public override string Name { get; } public override string Name { get; }
public override string Group => "CLR Properties"; public override string Group => "CLR Properties";
public override System.Type Type => _type; public override System.Type AssignedType => _assignedType;
public override System.Type PropertyType => _propertyType;
public override string? Value public override string? Value
{ {
@ -44,6 +48,7 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
var convertedValue = ConvertFromString(value, Property.PropertyType); var convertedValue = ConvertFromString(value, Property.PropertyType);
Property.SetValue(_target, convertedValue); Property.SetValue(_target, convertedValue);
Update();
} }
catch { } catch { }
} }
@ -62,7 +67,8 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
var val = Property.GetValue(_target); var val = Property.GetValue(_target);
RaiseAndSetIfChanged(ref _value, val, nameof(Value)); RaiseAndSetIfChanged(ref _value, val, nameof(Value));
RaiseAndSetIfChanged(ref _type, _value?.GetType() ?? Property.PropertyType, nameof(Type)); RaiseAndSetIfChanged(ref _assignedType, _value?.GetType() ?? Property.PropertyType, nameof(AssignedType));
RaisePropertyChanged(nameof(Type));
} }
} }
} }

54
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@ -16,7 +16,7 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
internal class ControlDetailsViewModel : ViewModelBase, IDisposable internal class ControlDetailsViewModel : ViewModelBase, IDisposable
{ {
private readonly IVisual _control; private readonly IAvaloniaObject _avaloniaObject;
private IDictionary<object, List<PropertyViewModel>>? _propertyIndex; private IDictionary<object, List<PropertyViewModel>>? _propertyIndex;
private PropertyViewModel? _selectedProperty; private PropertyViewModel? _selectedProperty;
private DataGridCollectionView? _propertiesView; private DataGridCollectionView? _propertiesView;
@ -27,21 +27,23 @@ namespace Avalonia.Diagnostics.ViewModels
private readonly Stack<(string Name,object Entry)> _selectedEntitiesStack = new(); private readonly Stack<(string Name,object Entry)> _selectedEntitiesStack = new();
private string? _selectedEntityName; private string? _selectedEntityName;
private string? _selectedEntityType; private string? _selectedEntityType;
private bool _showImplementedInterfaces;
public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control) public ControlDetailsViewModel(TreePageViewModel treePage, IAvaloniaObject avaloniaObject)
{ {
_control = control; _avaloniaObject = avaloniaObject;
TreePage = treePage; TreePage = treePage;
Layout = avaloniaObject is IVisual
Layout = new ControlLayoutViewModel(control); ? new ControlLayoutViewModel((IVisual)avaloniaObject)
: default;
NavigateToProperty(control, (control as IControl)?.Name ?? control.ToString()); NavigateToProperty(_avaloniaObject, (_avaloniaObject as IControl)?.Name ?? _avaloniaObject.ToString());
AppliedStyles = new ObservableCollection<StyleViewModel>(); AppliedStyles = new ObservableCollection<StyleViewModel>();
PseudoClasses = new ObservableCollection<PseudoClassViewModel>(); PseudoClasses = new ObservableCollection<PseudoClassViewModel>();
if (control is StyledElement styledElement) if (avaloniaObject is StyledElement styledElement)
{ {
styledElement.Classes.CollectionChanged += OnClassesChanged; styledElement.Classes.CollectionChanged += OnClassesChanged;
@ -181,7 +183,7 @@ namespace Avalonia.Diagnostics.ViewModels
set => RaiseAndSetIfChanged(ref _styleStatus, value); set => RaiseAndSetIfChanged(ref _styleStatus, value);
} }
public ControlLayoutViewModel Layout { get; } public ControlLayoutViewModel? Layout { get; }
protected override void OnPropertyChanged(PropertyChangedEventArgs e) protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{ {
@ -215,17 +217,17 @@ namespace Avalonia.Diagnostics.ViewModels
public void Dispose() public void Dispose()
{ {
if (_control is INotifyPropertyChanged inpc) if (_avaloniaObject is INotifyPropertyChanged inpc)
{ {
inpc.PropertyChanged -= ControlPropertyChanged; inpc.PropertyChanged -= ControlPropertyChanged;
} }
if (_control is AvaloniaObject ao) if (_avaloniaObject is AvaloniaObject ao)
{ {
ao.PropertyChanged -= ControlPropertyChanged; ao.PropertyChanged -= ControlPropertyChanged;
} }
if (_control is StyledElement se) if (_avaloniaObject is StyledElement se)
{ {
se.Classes.CollectionChanged -= OnClassesChanged; se.Classes.CollectionChanged -= OnClassesChanged;
} }
@ -245,18 +247,21 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
private IEnumerable<PropertyViewModel> GetClrProperties(object o) private IEnumerable<PropertyViewModel> GetClrProperties(object o, bool showImplementedInterfaces)
{ {
foreach (var p in GetClrProperties(o, o.GetType())) foreach (var p in GetClrProperties(o, o.GetType()))
{ {
yield return p; yield return p;
} }
foreach (var i in o.GetType().GetInterfaces()) if (showImplementedInterfaces)
{ {
foreach (var p in GetClrProperties(o, i)) foreach (var i in o.GetType().GetInterfaces())
{ {
yield return p; foreach (var p in GetClrProperties(o, i))
{
yield return p;
}
} }
} }
} }
@ -278,7 +283,7 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
Layout.ControlPropertyChanged(sender, e); Layout?.ControlPropertyChanged(sender, e);
} }
private void ControlPropertyChanged(object? sender, PropertyChangedEventArgs e) private void ControlPropertyChanged(object? sender, PropertyChangedEventArgs e)
@ -405,8 +410,8 @@ namespace Avalonia.Diagnostics.ViewModels
var selectedEntityName = SelectedEntityName; var selectedEntityName = SelectedEntityName;
if (selectedEntity == null if (selectedEntity == null
|| selectedProperty == null || selectedProperty == null
|| selectedProperty.Type == typeof(string) || selectedProperty.PropertyType == typeof(string)
|| selectedProperty.Type.IsValueType || selectedProperty.PropertyType.IsValueType
) )
return; return;
@ -420,7 +425,7 @@ namespace Avalonia.Diagnostics.ViewModels
property = selectedEntity.GetType().GetProperties() property = selectedEntity.GetType().GetProperties()
.FirstOrDefault(pi => pi.Name == selectedProperty.Name .FirstOrDefault(pi => pi.Name == selectedProperty.Name
&& pi.DeclaringType == selectedProperty.DeclaringType && pi.DeclaringType == selectedProperty.DeclaringType
&& pi.PropertyType.Name == selectedProperty.Type.Name) && pi.PropertyType.Name == selectedProperty.PropertyType.Name)
?.GetValue(selectedEntity); ?.GetValue(selectedEntity);
} }
if (property == null) return; if (property == null) return;
@ -453,7 +458,7 @@ namespace Avalonia.Diagnostics.ViewModels
SelectedEntityName = entityName; SelectedEntityName = entityName;
SelectedEntityType = o.ToString(); SelectedEntityType = o.ToString();
var properties = GetAvaloniaProperties(o) var properties = GetAvaloniaProperties(o)
.Concat(GetClrProperties(o)) .Concat(GetClrProperties(o, _showImplementedInterfaces))
.OrderBy(x => x, PropertyComparer.Instance) .OrderBy(x => x, PropertyComparer.Instance)
.ThenBy(x => x.Name) .ThenBy(x => x.Name)
.ToList(); .ToList();
@ -474,5 +479,12 @@ namespace Avalonia.Diagnostics.ViewModels
inpc2.PropertyChanged += ControlPropertyChanged; inpc2.PropertyChanged += ControlPropertyChanged;
} }
} }
internal void UpdatePropertiesView(bool showImplementedInterfaces)
{
_showImplementedInterfaces = showImplementedInterfaces;
SelectedProperty = null;
NavigateToProperty(_avaloniaObject, (_avaloniaObject as IControl)?.Name ?? _avaloniaObject.ToString());
}
} }
} }

90
src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs

@ -1,23 +1,31 @@
using System; using System;
using System.Reactive.Disposables;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Lifetimes = Avalonia.Controls.ApplicationLifetimes;
using System.Linq;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
internal class LogicalTreeNode : TreeNode internal class LogicalTreeNode : TreeNode
{ {
public LogicalTreeNode(ILogical logical, TreeNode? parent) public LogicalTreeNode(IAvaloniaObject avaloniaObject, TreeNode? parent)
: base((Control)logical, parent) : base(avaloniaObject, parent)
{ {
Children = new LogicalTreeNodeCollection(this, logical); Children = avaloniaObject switch
{
ILogical logical => new LogicalTreeNodeCollection(this, logical),
Controls.Application host => new ApplicationHostLogical(this, host),
_ => TreeNodeCollection.Empty
};
} }
public override TreeNodeCollection Children { get; } public override TreeNodeCollection Children { get; }
public static LogicalTreeNode[] Create(object control) public static LogicalTreeNode[] Create(object control)
{ {
var logical = control as ILogical; var logical = control as IAvaloniaObject;
return logical != null ? new[] { new LogicalTreeNode(logical, null) } : Array.Empty<LogicalTreeNode>(); return logical != null ? new[] { new LogicalTreeNode(logical, null) } : Array.Empty<LogicalTreeNode>();
} }
@ -41,10 +49,82 @@ namespace Avalonia.Diagnostics.ViewModels
protected override void Initialize(AvaloniaList<TreeNode> nodes) protected override void Initialize(AvaloniaList<TreeNode> nodes)
{ {
_subscription = _control.LogicalChildren.ForEachItem( _subscription = _control.LogicalChildren.ForEachItem(
(i, item) => nodes.Insert(i, new LogicalTreeNode(item, Owner)), (i, item) => nodes.Insert(i, new LogicalTreeNode((IAvaloniaObject)item, Owner)),
(i, item) => nodes.RemoveAt(i), (i, item) => nodes.RemoveAt(i),
() => nodes.Clear()); () => nodes.Clear());
} }
} }
internal class ApplicationHostLogical : TreeNodeCollection
{
readonly Controls.Application _application;
CompositeDisposable _subscriptions = new CompositeDisposable(2);
public ApplicationHostLogical(TreeNode owner, Controls.Application host) :
base(owner)
{
_application = host;
}
protected override void Initialize(AvaloniaList<TreeNode> nodes)
{
if (_application.ApplicationLifetime is Lifetimes.ISingleViewApplicationLifetime single)
{
nodes.Add(new LogicalTreeNode(single.MainView, Owner));
}
if (_application.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime classic)
{
for (int i = 0; i < classic.Windows.Count; i++)
{
var window = classic.Windows[i];
if (window is Views.MainWindow)
{
continue;
}
nodes.Add(new LogicalTreeNode(window, Owner));
}
_subscriptions = new System.Reactive.Disposables.CompositeDisposable()
{
Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (s,e)=>
{
if (s is Views.MainWindow)
{
return;
}
nodes.Add(new LogicalTreeNode((IAvaloniaObject)s!,Owner));
}),
Window.WindowClosedEvent.AddClassHandler(typeof(Window), (s,e)=>
{
if (s is Views.MainWindow)
{
return;
}
var item = nodes.FirstOrDefault(node=>object.ReferenceEquals(node.Visual,s));
if(!(item is null))
{
nodes.Remove(item);
}
if(nodes.Count == 0)
{
if (Avalonia.Application.Current?.ApplicationLifetime is Lifetimes.IControlledApplicationLifetime controller)
{
controller.Shutdown();
}
else
{
Environment.Exit(0);
}
}
}),
};
}
}
public override void Dispose()
{
_subscriptions?.Dispose();
base.Dispose();
}
}
} }
} }

157
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@ -1,15 +1,20 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Diagnostics.Models; using Avalonia.Diagnostics.Models;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Metadata;
using Avalonia.Threading; using Avalonia.Threading;
using System.Reactive.Linq;
using System.Linq;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
internal class MainViewModel : ViewModelBase, IDisposable internal class MainViewModel : ViewModelBase, IDisposable
{ {
private readonly TopLevel _root; private readonly AvaloniaObject _root;
private readonly TreePageViewModel _logicalTree; private readonly TreePageViewModel _logicalTree;
private readonly TreePageViewModel _visualTree; private readonly TreePageViewModel _visualTree;
private readonly EventsPageViewModel _events; private readonly EventsPageViewModel _events;
@ -17,13 +22,18 @@ namespace Avalonia.Diagnostics.ViewModels
private ViewModelBase? _content; private ViewModelBase? _content;
private int _selectedTab; private int _selectedTab;
private string? _focusedControl; private string? _focusedControl;
private string? _pointerOverElement; private IInputElement? _pointerOverElement;
private bool _shouldVisualizeMarginPadding = true; private bool _shouldVisualizeMarginPadding = true;
private bool _shouldVisualizeDirtyRects; private bool _shouldVisualizeDirtyRects;
private bool _showFpsOverlay; private bool _showFpsOverlay;
private bool _freezePopups; private bool _freezePopups;
private string? _pointerOverElementName;
public MainViewModel(TopLevel root) private IInputRoot? _pointerOverRoot;
private IScreenshotHandler? _screenshotHandler;
private bool _showPropertyType;
private bool _showImplementedInterfaces;
public MainViewModel(AvaloniaObject root)
{ {
_root = root; _root = root;
_logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root)); _logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root));
@ -35,8 +45,24 @@ namespace Avalonia.Diagnostics.ViewModels
if (KeyboardDevice.Instance is not null) if (KeyboardDevice.Instance is not null)
KeyboardDevice.Instance.PropertyChanged += KeyboardPropertyChanged; KeyboardDevice.Instance.PropertyChanged += KeyboardPropertyChanged;
SelectedTab = 0; SelectedTab = 0;
_pointerOverSubscription = root.GetObservable(TopLevel.PointerOverElementProperty) if (root is TopLevel topLevel)
.Subscribe(x => PointerOverElement = x?.GetType().Name); {
_pointerOverSubscription = topLevel.GetObservable(TopLevel.PointerOverElementProperty)
.Subscribe(x => PointerOverElement = x);
}
else
{
#nullable disable
_pointerOverSubscription = InputManager.Instance.PreProcess
.OfType<Input.Raw.RawPointerEventArgs>()
.Subscribe(e =>
{
PointerOverRoot = e.Root;
PointerOverElement = e.Root.GetInputElementsAt(e.Position).FirstOrDefault();
});
#nullable restore
}
Console = new ConsoleViewModel(UpdateConsoleContext); Console = new ConsoleViewModel(UpdateConsoleContext);
} }
@ -51,13 +77,26 @@ namespace Avalonia.Diagnostics.ViewModels
get => _shouldVisualizeMarginPadding; get => _shouldVisualizeMarginPadding;
set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value); set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value);
} }
public bool ShouldVisualizeDirtyRects public bool ShouldVisualizeDirtyRects
{ {
get => _shouldVisualizeDirtyRects; get => _shouldVisualizeDirtyRects;
set set
{ {
_root.Renderer.DrawDirtyRects = value; var changed = true;
if (_root is TopLevel topLevel && topLevel.Renderer is { })
{
topLevel.Renderer.DrawDirtyRects = value;
}
else if (_root is Controls.Application app && app.RendererRoot is { })
{
app.RendererRoot.DrawDirtyRects = value;
}
else
{
changed = false;
}
if (changed)
RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value); RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value);
} }
} }
@ -77,8 +116,21 @@ namespace Avalonia.Diagnostics.ViewModels
get => _showFpsOverlay; get => _showFpsOverlay;
set set
{ {
_root.Renderer.DrawFps = value; var changed = true;
RaiseAndSetIfChanged(ref _showFpsOverlay, value); if (_root is TopLevel topLevel && topLevel.Renderer is { })
{
topLevel.Renderer.DrawFps = value;
}
else if (_root is Controls.Application app && app.RendererRoot is { })
{
app.RendererRoot.DrawFps = value;
}
else
{
changed = false;
}
if(changed)
RaiseAndSetIfChanged(ref _showFpsOverlay, value);
} }
} }
@ -150,12 +202,28 @@ namespace Avalonia.Diagnostics.ViewModels
private set { RaiseAndSetIfChanged(ref _focusedControl, value); } private set { RaiseAndSetIfChanged(ref _focusedControl, value); }
} }
public string? PointerOverElement public IInputRoot? PointerOverRoot
{
get => _pointerOverRoot;
private set => RaiseAndSetIfChanged( ref _pointerOverRoot , value);
}
public IInputElement? PointerOverElement
{ {
get { return _pointerOverElement; } get { return _pointerOverElement; }
private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); } private set
{
RaiseAndSetIfChanged(ref _pointerOverElement, value);
PointerOverElementName = value?.GetType()?.Name;
}
} }
public string? PointerOverElementName
{
get => _pointerOverElementName;
private set => RaiseAndSetIfChanged(ref _pointerOverElementName, value);
}
private void UpdateConsoleContext(ConsoleContext context) private void UpdateConsoleContext(ConsoleContext context)
{ {
context.root = _root; context.root = _root;
@ -188,8 +256,11 @@ namespace Avalonia.Diagnostics.ViewModels
_pointerOverSubscription.Dispose(); _pointerOverSubscription.Dispose();
_logicalTree.Dispose(); _logicalTree.Dispose();
_visualTree.Dispose(); _visualTree.Dispose();
_root.Renderer.DrawDirtyRects = false; if (_root is TopLevel top)
_root.Renderer.DrawFps = false; {
top.Renderer.DrawDirtyRects = false;
top.Renderer.DrawFps = false;
}
} }
private void UpdateFocusedControl() private void UpdateFocusedControl()
@ -220,10 +291,66 @@ namespace Avalonia.Diagnostics.ViewModels
} }
public int? StartupScreenIndex { get; private set; } = default; public int? StartupScreenIndex { get; private set; } = default;
[DependsOn(nameof(TreePageViewModel.SelectedNode))]
[DependsOn(nameof(Content))]
bool CanShot(object? parameter)
{
return Content is TreePageViewModel tree
&& tree.SelectedNode != null
&& tree.SelectedNode.Visual is VisualTree.IVisual visual
&& visual.VisualRoot != null;
}
async void Shot(object? parameter)
{
if ((Content as TreePageViewModel)?.SelectedNode?.Visual is IControl control
&& _screenshotHandler is { }
)
{
try
{
await _screenshotHandler.Take(control);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.Message);
//TODO: Notify error
}
}
}
public void SetOptions(DevToolsOptions options) public void SetOptions(DevToolsOptions options)
{ {
_screenshotHandler = options.ScreenshotHandler;
StartupScreenIndex = options.StartupScreenIndex; StartupScreenIndex = options.StartupScreenIndex;
ShowImplementedInterfaces = options.ShowImplementedInterfaces;
}
public bool ShowImplementedInterfaces
{
get => _showImplementedInterfaces;
private set => RaiseAndSetIfChanged(ref _showImplementedInterfaces , value);
}
public void ToggleShowImplementedInterfaces(object parametr)
{
ShowImplementedInterfaces = !ShowImplementedInterfaces;
if (Content is TreePageViewModel viewModel)
{
viewModel.UpdatePropertiesView();
}
}
public bool ShowDettailsPropertyType
{
get => _showPropertyType;
private set => RaiseAndSetIfChanged(ref _showPropertyType , value);
}
public void ToggleShowDettailsPropertyType(object paramter)
{
ShowDettailsPropertyType = !ShowDettailsPropertyType;
} }
} }
} }

13
src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs

@ -14,12 +14,17 @@ namespace Avalonia.Diagnostics.ViewModels
public abstract object Key { get; } public abstract object Key { get; }
public abstract string Name { get; } public abstract string Name { get; }
public abstract string Group { get; } public abstract string Group { get; }
public abstract Type Type { get; } public abstract Type AssignedType { get; }
public abstract Type? DeclaringType { get; } public abstract Type? DeclaringType { get; }
public abstract string? Value { get; set; } public abstract string? Value { get; set; }
public abstract string Priority { get; } public abstract string Priority { get; }
public abstract bool? IsAttached { get; } public abstract bool? IsAttached { get; }
public abstract void Update(); public abstract void Update();
public abstract Type PropertyType { get; }
public string Type => PropertyType == AssignedType
? PropertyType.GetTypeName()
: $"{PropertyType.GetTypeName()} {{{AssignedType.GetTypeName()}}}";
protected static string? ConvertToString(object? value) protected static string? ConvertToString(object? value)
{ {
@ -31,7 +36,7 @@ namespace Avalonia.Diagnostics.ViewModels
var converter = TypeDescriptor.GetConverter(value); var converter = TypeDescriptor.GetConverter(value);
//CollectionConverter does not deliver any important information. It just displays "(Collection)". //CollectionConverter does not deliver any important information. It just displays "(Collection)".
if (!converter.CanConvertTo(typeof(string)) || if (!converter.CanConvertTo(typeof(string)) ||
converter.GetType() == typeof(CollectionConverter)) converter.GetType() == typeof(CollectionConverter))
{ {
return value.ToString() ?? "(null)"; return value.ToString() ?? "(null)";

9
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs

@ -16,12 +16,13 @@ namespace Avalonia.Diagnostics.ViewModels
private string _classes; private string _classes;
private bool _isExpanded; private bool _isExpanded;
protected TreeNode(IVisual visual, TreeNode? parent, string? customName = null) protected TreeNode(IAvaloniaObject avaloniaObject, TreeNode? parent, string? customName = null)
{ {
_classes = string.Empty; _classes = string.Empty;
Parent = parent; Parent = parent;
Type = customName ?? visual.GetType().Name; var visual = avaloniaObject ;
Visual = visual; Type = customName ?? avaloniaObject.GetType().Name;
Visual = visual!;
FontWeight = IsRoot ? FontWeight.Bold : FontWeight.Normal; FontWeight = IsRoot ? FontWeight.Bold : FontWeight.Normal;
if (visual is IControl control) if (visual is IControl control)
@ -76,7 +77,7 @@ namespace Avalonia.Diagnostics.ViewModels
get; get;
} }
public IVisual Visual public IAvaloniaObject Visual
{ {
get; get;
} }

14
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs

@ -10,6 +10,20 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList<TreeNode>, IDisposable internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList<TreeNode>, IDisposable
{ {
private class EmptyTreeNodeCollection : TreeNodeCollection
{
public EmptyTreeNodeCollection():base(default!)
{
}
protected override void Initialize(AvaloniaList<TreeNode> nodes)
{
}
}
static readonly internal TreeNodeCollection Empty = new EmptyTreeNodeCollection();
private AvaloniaList<TreeNode>? _inner; private AvaloniaList<TreeNode>? _inner;
public TreeNodeCollection(TreeNode owner) => Owner = owner; public TreeNodeCollection(TreeNode owner) => Owner = owner;

6
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs

@ -39,6 +39,7 @@ namespace Avalonia.Diagnostics.ViewModels
Details = value != null ? Details = value != null ?
new ControlDetailsViewModel(this, value.Visual) : new ControlDetailsViewModel(this, value.Visual) :
null; null;
Details?.UpdatePropertiesView(MainView.ShowImplementedInterfaces);
Details?.UpdateStyleFilters(); Details?.UpdateStyleFilters();
} }
} }
@ -135,5 +136,10 @@ namespace Avalonia.Diagnostics.ViewModels
return null; return null;
} }
internal void UpdatePropertiesView()
{
Details?.UpdatePropertiesView(MainView?.ShowImplementedInterfaces ?? true);
}
} }
} }

80
src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs

@ -7,15 +7,22 @@ using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Lifetimes = Avalonia.Controls.ApplicationLifetimes;
using System.Linq;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
internal class VisualTreeNode : TreeNode internal class VisualTreeNode : TreeNode
{ {
public VisualTreeNode(IVisual visual, TreeNode? parent, string? customName = null) public VisualTreeNode(IAvaloniaObject avaloniaObject, TreeNode? parent, string? customName = null)
: base(visual, parent, customName) : base(avaloniaObject, parent, customName)
{ {
Children = new VisualTreeNodeCollection(this, visual); Children = avaloniaObject switch
{
IVisual visual => new VisualTreeNodeCollection(this, visual),
Controls.Application host => new ApplicationHostVisuals(this, host),
_ => TreeNodeCollection.Empty
};
if (Visual is IStyleable styleable) if (Visual is IStyleable styleable)
IsInTemplate = styleable.TemplatedParent != null; IsInTemplate = styleable.TemplatedParent != null;
@ -27,7 +34,7 @@ namespace Avalonia.Diagnostics.ViewModels
public static VisualTreeNode[] Create(object control) public static VisualTreeNode[] Create(object control)
{ {
return control is IVisual visual ? return control is IAvaloniaObject visual ?
new[] { new VisualTreeNode(visual, null) } : new[] { new VisualTreeNode(visual, null) } :
Array.Empty<VisualTreeNode>(); Array.Empty<VisualTreeNode>();
} }
@ -130,7 +137,7 @@ namespace Avalonia.Diagnostics.ViewModels
_subscriptions.Add( _subscriptions.Add(
_control.VisualChildren.ForEachItem( _control.VisualChildren.ForEachItem(
(i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)), (i, item) => nodes.Insert(i, new VisualTreeNode((IAvaloniaObject)item, Owner)),
(i, item) => nodes.RemoveAt(i), (i, item) => nodes.RemoveAt(i),
() => nodes.Clear())); () => nodes.Clear()));
} }
@ -147,5 +154,68 @@ namespace Avalonia.Diagnostics.ViewModels
public string? CustomName { get; } public string? CustomName { get; }
} }
} }
internal class ApplicationHostVisuals : TreeNodeCollection
{
readonly Controls.Application _application;
CompositeDisposable _subscriptions = new CompositeDisposable(2);
public ApplicationHostVisuals(TreeNode owner, Controls.Application host) :
base(owner)
{
_application = host;
}
protected override void Initialize(AvaloniaList<TreeNode> nodes)
{
if (_application.ApplicationLifetime is Lifetimes.ISingleViewApplicationLifetime single)
{
nodes.Add(new VisualTreeNode(single.MainView, Owner));
}
if (_application.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime classic)
{
for (int i = 0; i < classic.Windows.Count; i++)
{
var window = classic.Windows[i];
if (window is Views.MainWindow)
{
continue;
}
nodes.Add(new VisualTreeNode(window, Owner));
}
_subscriptions = new System.Reactive.Disposables.CompositeDisposable()
{
Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (s,e)=>
{
if (s is Views.MainWindow)
{
return;
}
nodes.Add(new VisualTreeNode((IAvaloniaObject)s!,Owner));
}),
Window.WindowClosedEvent.AddClassHandler(typeof(Window), (s,e)=>
{
if (s is Views.MainWindow)
{
return;
}
var item = nodes.FirstOrDefault(node=>object.ReferenceEquals(node.Visual,s));
if(!(item is null))
{
nodes.Remove(item);
}
}),
};
}
}
public override void Dispose()
{
_subscriptions?.Dispose();
base.Dispose();
}
}
} }
} }

36
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@ -4,15 +4,29 @@
xmlns:local="clr-namespace:Avalonia.Diagnostics.Views" xmlns:local="clr-namespace:Avalonia.Diagnostics.Views"
xmlns:controls="clr-namespace:Avalonia.Diagnostics.Controls" xmlns:controls="clr-namespace:Avalonia.Diagnostics.Controls"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
xmlns:lb="using:Avalonia.Diagnostics.Behaviors"
x:Class="Avalonia.Diagnostics.Views.ControlDetailsView" x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"
x:Name="Main"> x:Name="Main">
<UserControl.Resources> <UserControl.Resources>
<conv:BoolToOpacityConverter x:Key="BoolToOpacity" Opacity="0.6"/> <conv:BoolToOpacityConverter x:Key="BoolToOpacity" Opacity="0.6"/>
<conv:GetTypeNameConverter x:Key="GetTypeName"/>
</UserControl.Resources> </UserControl.Resources>
<Grid ColumnDefinitions="*,Auto,320"> <Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<!--
When selecting the Application node, we need this trick to hide Layout Visualizer
because when using the GridSplitter it sets the Witdth property of ColumnDefinition
(see https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Controls/GridSplitter.cs#L528)
and if we hide the contents of the column, the space is not reclaimed
(see discussion https://github.com/AvaloniaUI/Avalonia/discussions/6773).
-->
<ColumnDefinition Width="320" lb:ColumnDefinition.IsVisible="{Binding Layout, Converter={x:Static ObjectConverters.IsNotNull}}" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" RowDefinitions="Auto,Auto,*"> <Grid Grid.Column="0" RowDefinitions="Auto,Auto,*">
<Grid ColumnDefinitions="Auto, *" RowDefinitions="Auto, Auto"> <Grid ColumnDefinitions="Auto, *" RowDefinitions="Auto, Auto">
@ -40,7 +54,18 @@
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True" /> <DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True" />
<DataGridTextColumn Header="Value" Binding="{Binding Value}" /> <DataGridTextColumn Header="Value" Binding="{Binding Value}" />
<DataGridTextColumn Header="Type" Binding="{Binding Type.Name}" /> <DataGridTextColumn Header="Type" Binding="{Binding Type}"
IsReadOnly="True"
IsVisible="{Binding !$parent[UserControl;2].DataContext.ShowDettailsPropertyType}"
/>
<DataGridTextColumn Header="Assinged Type" Binding="{Binding AssignedType, Converter={StaticResource GetTypeName}}"
IsReadOnly="True"
IsVisible="{Binding $parent[UserControl;2].DataContext.ShowDettailsPropertyType}"
/>
<DataGridTextColumn Header="Property Type" Binding="{Binding PropertyType, Converter={StaticResource GetTypeName}}"
IsReadOnly="True"
IsVisible="{Binding $parent[UserControl;2].DataContext.ShowDettailsPropertyType}"
/>
<DataGridTextColumn Header="Priority" Binding="{Binding Priority}" IsReadOnly="True" /> <DataGridTextColumn Header="Priority" Binding="{Binding Priority}" IsReadOnly="True" />
</DataGrid.Columns> </DataGrid.Columns>
@ -53,9 +78,10 @@
</Grid> </Grid>
<GridSplitter Grid.Column="1" /> <GridSplitter Grid.Column="1"/>
<Grid Grid.Column="2" RowDefinitions="*,Auto,*" > <Grid Grid.Column="2" RowDefinitions="*,Auto,*"
IsVisible="{Binding Layout, Converter={x:Static ObjectConverters.IsNotNull}}">
<Grid RowDefinitions="Auto,*" Grid.Row="0"> <Grid RowDefinitions="Auto,*" Grid.Row="0">
<TextBlock FontWeight="Bold" Grid.Row="0" Text="Layout Visualizer" Margin="4" /> <TextBlock FontWeight="Bold" Grid.Row="0" Text="Layout Visualizer" Margin="4" />

49
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml

@ -6,6 +6,36 @@
<Menu> <Menu>
<MenuItem Header="_File"> <MenuItem Header="_File">
<MenuItem Header="E_xit" Command="{Binding $parent[Window].Close}" /> <MenuItem Header="E_xit" Command="{Binding $parent[Window].Close}" />
<MenuItem Header="Screenshot" Command="{Binding Shot}" HotKey="F8">
<MenuItem.Icon>
<Image>
<DrawingImage>
<DrawingGroup>
<GeometryDrawing Geometry="F1 M 13.4533,17.56C 13.4533,19.8827 15.344,21.772 17.6653,21.772L 17.6653,21.772C 19.988,21.772 21.8773,19.8827 21.8773,17.56L 21.8773,17.56C 21.8773,15.2373 19.988,13.348 17.6653,13.348L 17.6653,13.348C 15.344,13.348 13.4533,15.2373 13.4533,17.56 Z ">
<GeometryDrawing.Brush>
<RadialGradientBrush Center="0.245696,0.288009" GradientOrigin="0.245696,0.288009" Radius="0.499952">
<RadialGradientBrush.GradientStops>
<GradientStop Color="#FF878A8C" Offset="0" />
<GradientStop Color="#FF544A4C" Offset="0.991379" />
</RadialGradientBrush.GradientStops>
</RadialGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing Geometry="F1 M 13.332,6.22803L 10.2227,9.72668L 8.49866,9.72668L 8.49866,7.56136L 5.33333,7.56136L 5.33333,9.72668L 3.33333,9.72668L 3.33333,24.3947L 13.1213,24.3947C 14.424,25.264 15.9853,25.772 17.6653,25.772L 17.6653,25.772C 19.344,25.772 20.9067,25.264 22.2094,24.3947L 28.6667,24.3947L 28.6667,9.72668L 24.944,9.72668L 21.8333,6.22803M 12.12,17.56C 12.12,14.5013 14.608,12.0147 17.6653,12.0147L 17.6653,12.0147C 20.7227,12.0147 23.2107,14.5013 23.2107,17.56L 23.2107,17.56C 23.2107,20.6174 20.7227,23.104 17.6653,23.104L 17.6653,23.104C 14.608,23.104 12.12,20.6174 12.12,17.56 Z ">
<GeometryDrawing.Brush>
<RadialGradientBrush Center="0.196943,0.216757" GradientOrigin="0.196943,0.216757" Radius="0.44654">
<RadialGradientBrush.GradientStops>
<GradientStop Color="#FF87898C" Offset="0" />
<GradientStop Color="#FF544A4C" Offset="1" />
</RadialGradientBrush.GradientStops>
</RadialGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage>
</Image>
</MenuItem.Icon>
</MenuItem>
</MenuItem> </MenuItem>
<MenuItem Header="_View"> <MenuItem Header="_View">
<MenuItem Header="_Console" Command="{Binding $parent[UserControl].ToggleConsole}"> <MenuItem Header="_Console" Command="{Binding $parent[UserControl].ToggleConsole}">
@ -15,6 +45,23 @@
IsEnabled="False" /> IsEnabled="False" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<MenuItem Header="Control _Details">
<MenuItem Header="Show Implemented Interfaces" Command="{Binding ToggleShowImplementedInterfaces}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding ShowImplementedInterfaces}"
IsEnabled="False" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Split Property Type" Command="{Binding ToggleShowDettailsPropertyType}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding ShowDettailsPropertyType}"
IsEnabled="False"/>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
</MenuItem> </MenuItem>
<MenuItem Header="_Options"> <MenuItem Header="_Options">
<MenuItem Header="Visualize margin/padding" Command="{Binding ToggleVisualizeMarginPadding}"> <MenuItem Header="Visualize margin/padding" Command="{Binding ToggleVisualizeMarginPadding}">
@ -73,7 +120,7 @@
<TextBlock Text="{Binding FocusedControl}" /> <TextBlock Text="{Binding FocusedControl}" />
<Separator Width="8" /> <Separator Width="8" />
<TextBlock>Pointer Over:</TextBlock> <TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}" /> <TextBlock Text="{Binding PointerOverElementName}" />
</StackPanel> </StackPanel>
<TextBlock Grid.Column="1" <TextBlock Grid.Column="1"

4
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml

@ -18,6 +18,8 @@
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml" /> <StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml" />
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml" /> <StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml" />
</Window.Styles> </Window.Styles>
<Window.KeyBindings>
<KeyBinding Gesture="F8" Command="{Binding Shot}"/>
</Window.KeyBindings>
<views:MainView/> <views:MainView/>
</Window> </Window>

21
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs

@ -19,7 +19,7 @@ namespace Avalonia.Diagnostics.Views
{ {
private readonly IDisposable? _keySubscription; private readonly IDisposable? _keySubscription;
private readonly Dictionary<Popup, IDisposable> _frozenPopupStates; private readonly Dictionary<Popup, IDisposable> _frozenPopupStates;
private TopLevel? _root; private AvaloniaObject? _root;
public MainWindow() public MainWindow()
{ {
@ -50,23 +50,23 @@ namespace Avalonia.Diagnostics.Views
this.Opened += lh; this.Opened += lh;
} }
public TopLevel? Root public AvaloniaObject? Root
{ {
get => _root; get => _root;
set set
{ {
if (_root != value) if (_root != value)
{ {
if (_root != null) if (_root is ICloseable oldClosable)
{ {
_root.Closed -= RootClosed; oldClosable.Closed -= RootClosed;
} }
_root = value; _root = value;
if (_root != null) if (_root is ICloseable newClosable)
{ {
_root.Closed += RootClosed; newClosable.Closed += RootClosed;
DataContext = new MainViewModel(_root); DataContext = new MainViewModel(_root);
} }
else else
@ -91,9 +91,9 @@ namespace Avalonia.Diagnostics.Views
_frozenPopupStates.Clear(); _frozenPopupStates.Clear();
if (_root != null) if (_root is ICloseable cloneable)
{ {
_root.Closed -= RootClosed; cloneable.Closed -= RootClosed;
_root = null; _root = null;
} }
@ -123,7 +123,7 @@ namespace Avalonia.Diagnostics.Views
.FirstOrDefault(); .FirstOrDefault();
} }
private static List<PopupRoot> GetPopupRoots(IVisual root) private static List<PopupRoot> GetPopupRoots(TopLevel root)
{ {
var popupRoots = new List<PopupRoot>(); var popupRoots = new List<PopupRoot>();
@ -160,7 +160,8 @@ namespace Avalonia.Diagnostics.Views
return; return;
} }
var root = Root; var root = Root as TopLevel
?? vm.PointerOverRoot as TopLevel;
if (root is null) if (root is null)
{ {
return; return;

7
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs

@ -47,7 +47,12 @@ namespace Avalonia.Diagnostics.Views
return; return;
} }
var visual = (Visual)node.Visual; var visual = node.Visual as Visual;
if (visual is null)
{
return;
}
_currentLayer = AdornerLayer.GetAdornerLayer(visual); _currentLayer = AdornerLayer.GetAdornerLayer(visual);

71
src/Avalonia.Diagnostics/Diagnostics/VisualExtensions.cs

@ -0,0 +1,71 @@
using System;
using System.IO;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Media.Imaging;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics
{
internal static class VisualExtensions
{
/// <summary>
/// Render control to the destination stream.
/// </summary>
/// <param name="source">Control to be rendered.</param>
/// <param name="destination">Destination stream.</param>
/// <param name="dpi">Dpi quality.</param>
public static void RenderTo(this IControl source, Stream destination, double dpi = 96)
{
if (source.TransformedBounds == null)
{
return;
}
var rect = source.TransformedBounds.Value.Clip;
var top = rect.TopLeft;
var pixelSize = new PixelSize((int)rect.Width, (int)rect.Height);
var dpiVector = new Vector(dpi, dpi);
// get Visual root
var root = (source.VisualRoot
?? source.GetVisualRoot())
as IControl ?? source;
IDisposable? clipSetter = default;
IDisposable? clipToBoundsSetter = default;
IDisposable? renderTransformOriginSetter = default;
IDisposable? renderTransformSetter = default;
try
{
// Set clip region
var clipRegion = new Media.RectangleGeometry(rect);
clipToBoundsSetter = root.SetValue(Visual.ClipToBoundsProperty, true, BindingPriority.Animation);
clipSetter = root.SetValue(Visual.ClipProperty, clipRegion, BindingPriority.Animation);
// Translate origin
renderTransformOriginSetter = root.SetValue(Visual.RenderTransformOriginProperty,
new RelativePoint(top, RelativeUnit.Absolute),
BindingPriority.Animation);
renderTransformSetter = root.SetValue(Visual.RenderTransformProperty,
new Media.TranslateTransform(-top.X, -top.Y),
BindingPriority.Animation);
using (var bitmap = new RenderTargetBitmap(pixelSize, dpiVector))
{
bitmap.Render(root);
bitmap.Save(destination);
}
}
finally
{
// Restore values before trasformation
renderTransformSetter?.Dispose();
renderTransformOriginSetter?.Dispose();
clipSetter?.Dispose();
clipToBoundsSetter?.Dispose();
source?.InvalidateVisual();
}
}
}
}

3
src/Avalonia.Dialogs/ApiCompatBaseline.txt

@ -0,0 +1,3 @@
Compat issues with assembly Avalonia.Dialogs:
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Dialogs.AboutAvaloniaDialog' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
Total Issues: 1

2
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

2
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

2
src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

2
src/Avalonia.Headless/Avalonia.Headless.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

4
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -67,6 +67,10 @@ namespace Avalonia.Headless
{ {
public Size DoubleClickSize { get; } = new Size(2, 2); public Size DoubleClickSize { get; } = new Size(2, 2);
public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500); public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500);
public Size TouchDoubleClickSize => new Size(16,16);
public TimeSpan TouchDoubleClickTime => DoubleClickTime;
} }
class HeadlessSystemDialogsStub : ISystemDialogImpl class HeadlessSystemDialogsStub : ISystemDialogImpl

2
src/Avalonia.Input/Avalonia.Input.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" /> <Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />

2
src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs

@ -71,7 +71,7 @@ namespace Avalonia.Input.GestureRecognizers
EndGesture(); EndGesture();
_tracking = e.Pointer; _tracking = e.Pointer;
_gestureId = ScrollGestureEventArgs.GetNextFreeId();; _gestureId = ScrollGestureEventArgs.GetNextFreeId();;
_trackedRootPoint = e.GetPosition(null); _trackedRootPoint = e.GetPosition(_target);
} }
} }

11
src/Avalonia.Input/TouchDevice.cs

@ -58,20 +58,17 @@ namespace Avalonia.Input
} }
else else
{ {
var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>(); var settings = AvaloniaLocator.Current.GetRequiredService<IPlatformSettings>();
if (settings == null)
{
throw new Exception("IPlatformSettings can not be null");
}
if (!_lastClickRect.Contains(args.Position) if (!_lastClickRect.Contains(args.Position)
|| ev.Timestamp - _lastClickTime > settings.DoubleClickTime.TotalMilliseconds) || ev.Timestamp - _lastClickTime > settings.TouchDoubleClickTime.TotalMilliseconds)
{ {
_clickCount = 0; _clickCount = 0;
} }
++_clickCount; ++_clickCount;
_lastClickTime = ev.Timestamp; _lastClickTime = ev.Timestamp;
_lastClickRect = new Rect(args.Position, new Size()) _lastClickRect = new Rect(args.Position, new Size())
.Inflate(new Thickness(16, 16)); .Inflate(new Thickness(settings.TouchDoubleClickSize.Width / 2, settings.TouchDoubleClickSize.Height / 2));
} }
target.RaiseEvent(new PointerPressedEventArgs(target, pointer, target.RaiseEvent(new PointerPressedEventArgs(target, pointer,

2
src/Avalonia.Interactivity/Avalonia.Interactivity.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" /> <ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />

17
src/Avalonia.Layout/AttachedLayout.cs

@ -4,6 +4,7 @@
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System; using System;
using Avalonia.Utilities;
namespace Avalonia.Layout namespace Avalonia.Layout
{ {
@ -19,10 +20,26 @@ namespace Avalonia.Layout
/// </summary> /// </summary>
public event EventHandler? MeasureInvalidated; public event EventHandler? MeasureInvalidated;
/// <summary>
/// Occurs when the measurement state (layout) has been invalidated.
/// </summary>
public static readonly WeakEvent<AttachedLayout, EventArgs> MeasureInvalidatedWeakEvent =
WeakEvent.Register<AttachedLayout>(
(s, h) => s.MeasureInvalidated += h,
(s, h) => s.MeasureInvalidated -= h);
/// <summary> /// <summary>
/// Occurs when the arrange state (layout) has been invalidated. /// Occurs when the arrange state (layout) has been invalidated.
/// </summary> /// </summary>
public event EventHandler? ArrangeInvalidated; public event EventHandler? ArrangeInvalidated;
/// <summary>
/// Occurs when the arrange state (layout) has been invalidated.
/// </summary>
public static readonly WeakEvent<AttachedLayout, EventArgs> ArrangeInvalidatedWeakEvent =
WeakEvent.Register<AttachedLayout>(
(s, h) => s.ArrangeInvalidated += h,
(s, h) => s.ArrangeInvalidated -= h);
/// <summary> /// <summary>
/// Initializes any per-container state the layout requires when it is attached to an /// Initializes any per-container state the layout requires when it is attached to an

2
src/Avalonia.Layout/Avalonia.Layout.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" /> <ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />

6
src/Avalonia.MicroCom/MicroComRuntime.cs

@ -36,7 +36,13 @@ namespace Avalonia.MicroCom
public static T CreateProxyFor<T>(void* pObject, bool ownsHandle) => (T)CreateProxyFor(typeof(T), new IntPtr(pObject), ownsHandle); public static T CreateProxyFor<T>(void* pObject, bool ownsHandle) => (T)CreateProxyFor(typeof(T), new IntPtr(pObject), ownsHandle);
public static T CreateProxyFor<T>(IntPtr pObject, bool ownsHandle) => (T)CreateProxyFor(typeof(T), pObject, ownsHandle); public static T CreateProxyFor<T>(IntPtr pObject, bool ownsHandle) => (T)CreateProxyFor(typeof(T), pObject, ownsHandle);
public static T CreateProxyOrNullFor<T>(void* pObject, bool ownsHandle) where T : class =>
pObject == null ? null : (T)CreateProxyFor(typeof(T), new IntPtr(pObject), ownsHandle);
public static T CreateProxyOrNullFor<T>(IntPtr pObject, bool ownsHandle) where T : class =>
pObject == IntPtr.Zero ? null : (T)CreateProxyFor(typeof(T), pObject, ownsHandle);
public static object CreateProxyFor(Type type, IntPtr pObject, bool ownsHandle) => _factories[type](pObject, ownsHandle); public static object CreateProxyFor(Type type, IntPtr pObject, bool ownsHandle) => _factories[type](pObject, ownsHandle);
public static IntPtr GetNativeIntPtr<T>(this T obj, bool owned = false) where T : IUnknown public static IntPtr GetNativeIntPtr<T>(this T obj, bool owned = false) where T : IUnknown

5
src/Avalonia.MicroCom/MicroComVtblBase.cs

@ -21,6 +21,11 @@ namespace Avalonia.MicroCom
AddMethod((AddRefDelegate)Release); AddMethod((AddRefDelegate)Release);
} }
protected void AddMethod(void* f)
{
_methods.Add(new IntPtr(f));
}
protected void AddMethod(Delegate d) protected void AddMethod(Delegate d)
{ {
GCHandle.Alloc(d); GCHandle.Alloc(d);

7
src/Avalonia.Native/Avalonia.Native.csproj

@ -4,8 +4,9 @@
<PackAvaloniaNative Condition="'$(PackAvaloniaNative)' == ''">$([MSBuild]::IsOSPlatform(OSX))</PackAvaloniaNative> <PackAvaloniaNative Condition="'$(PackAvaloniaNative)' == ''">$([MSBuild]::IsOSPlatform(OSX))</PackAvaloniaNative>
<IsPackable>$(PackAvaloniaNative)</IsPackable> <IsPackable>$(PackAvaloniaNative)</IsPackable>
<IsPackable Condition="'$([MSBuild]::IsOSPlatform(OSX))' == 'True'">true</IsPackable> <IsPackable Condition="'$([MSBuild]::IsOSPlatform(OSX))' == 'True'">true</IsPackable>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<MicroComGeneratorRuntimeNamespace>Avalonia.MicroCom</MicroComGeneratorRuntimeNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition="'$(PackAvaloniaNative)' == 'true'"> <ItemGroup Condition="'$(PackAvaloniaNative)' == 'true'">
@ -20,7 +21,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" /> <ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\Avalonia.Dialogs\Avalonia.Dialogs.csproj" /> <ProjectReference Include="..\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
<AvnComIdl Include="avn.idl" OutputFile="Interop.Generated.cs" /> <PackageReference Include="MicroCom.CodeGenerator.MSBuild" Version="0.10.4" PrivateAssets="all" />
<MicroComIdl Include="avn.idl" CSharpInteropPath="Interop.Generated.cs" />
</ItemGroup> </ItemGroup>
<Import Project="../../build/MicroCom.targets" />
</Project> </Project>

10
src/Avalonia.Native/AvaloniaNativeMenuExporter.cs

@ -80,8 +80,8 @@ namespace Avalonia.Native
}; };
result.Add(aboutItem); result.Add(aboutItem);
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>(); var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>() ?? new MacOSPlatformOptions();
if (macOpts == null || !macOpts.DisableDefaultApplicationMenuItems) if (!macOpts.DisableDefaultApplicationMenuItems)
{ {
result.Add(new NativeMenuItemSeparator()); result.Add(new NativeMenuItemSeparator());
@ -131,7 +131,7 @@ namespace Avalonia.Native
}; };
quitItem.Click += (sender, args) => quitItem.Click += (sender, args) =>
{ {
_applicationCommands.ShowAll(); (Application.Current.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown();
}; };
result.Add(quitItem); result.Add(quitItem);
} }
@ -142,9 +142,9 @@ namespace Avalonia.Native
private void DoLayoutReset(bool forceUpdate = false) private void DoLayoutReset(bool forceUpdate = false)
{ {
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>(); var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>() ?? new MacOSPlatformOptions();
if (macOpts != null && macOpts.DisableNativeMenus) if (macOpts.DisableNativeMenus)
{ {
return; return;
} }

32
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -26,9 +26,15 @@ namespace Avalonia.Native
public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(500); //TODO public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(500); //TODO
/// <inheritdoc cref="IPlatformSettings.TouchDoubleClickSize"/>
public Size TouchDoubleClickSize => new Size(16, 16);
/// <inheritdoc cref="IPlatformSettings.TouchDoubleClickTime"/>
public TimeSpan TouchDoubleClickTime => DoubleClickTime;
public static AvaloniaNativePlatform Initialize(IntPtr factory, AvaloniaNativePlatformOptions options) public static AvaloniaNativePlatform Initialize(IntPtr factory, AvaloniaNativePlatformOptions options)
{ {
var result = new AvaloniaNativePlatform(MicroComRuntime.CreateProxyFor<IAvaloniaNativeFactory>(factory, true)); var result = new AvaloniaNativePlatform(MicroComRuntime.CreateProxyFor<IAvaloniaNativeFactory>(factory, true));
result.DoInitialize(options); result.DoInitialize(options);
return result; return result;
@ -55,14 +61,14 @@ namespace Avalonia.Native
return Initialize(CreateAvaloniaNative(), options); return Initialize(CreateAvaloniaNative(), options);
} }
public void SetupApplicationMenuExporter () public void SetupApplicationMenuExporter()
{ {
var exporter = new AvaloniaNativeMenuExporter(_factory); var exporter = new AvaloniaNativeMenuExporter(_factory);
} }
public void SetupApplicationName () public void SetupApplicationName()
{ {
if(!string.IsNullOrWhiteSpace(Application.Current.Name)) if (!string.IsNullOrWhiteSpace(Application.Current.Name))
{ {
_factory.MacOptions.SetApplicationTitle(Application.Current.Name); _factory.MacOptions.SetApplicationTitle(Application.Current.Name);
} }
@ -80,19 +86,19 @@ namespace Avalonia.Native
GCHandle.FromIntPtr(handle).Free(); GCHandle.FromIntPtr(handle).Free();
} }
} }
void DoInitialize(AvaloniaNativePlatformOptions options) void DoInitialize(AvaloniaNativePlatformOptions options)
{ {
_options = options; _options = options;
var applicationPlatform = new AvaloniaNativeApplicationPlatform(); var applicationPlatform = new AvaloniaNativeApplicationPlatform();
_factory.Initialize(new GCHandleDeallocator(), applicationPlatform); _factory.Initialize(new GCHandleDeallocator(), applicationPlatform);
if (_factory.MacOptions != null) if (_factory.MacOptions != null)
{ {
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>(); var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>() ?? new MacOSPlatformOptions();
_factory.MacOptions.SetShowInDock(macOpts?.ShowInDock != false ? 1 : 0); _factory.MacOptions.SetShowInDock(macOpts.ShowInDock ? 1 : 0);
} }
AvaloniaLocator.CurrentMutable AvaloniaLocator.CurrentMutable
@ -118,7 +124,7 @@ namespace Avalonia.Native
hotkeys.MoveCursorToTheStartOfLineWithSelection.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); hotkeys.MoveCursorToTheStartOfLineWithSelection.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers | hotkeys.SelectionModifiers));
hotkeys.MoveCursorToTheEndOfLine.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers)); hotkeys.MoveCursorToTheEndOfLine.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers));
hotkeys.MoveCursorToTheEndOfLineWithSelection.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); hotkeys.MoveCursorToTheEndOfLineWithSelection.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers | hotkeys.SelectionModifiers));
if (_options.UseGpu) if (_options.UseGpu)
{ {
try try
@ -133,7 +139,7 @@ namespace Avalonia.Native
} }
} }
public ITrayIconImpl CreateTrayIcon () public ITrayIconImpl CreateTrayIcon()
{ {
return new TrayIconImpl(_factory); return new TrayIconImpl(_factory);
} }
@ -159,8 +165,8 @@ namespace Avalonia.Native
ShowInDock = true; ShowInDock = true;
} }
public bool ShowInDock public bool ShowInDock
{ {
get => _showInDock; get => _showInDock;
set set
{ {

28
src/Avalonia.Native/WindowImplBase.cs

@ -28,18 +28,18 @@ namespace Avalonia.Native
public string HandleDescriptor => "NSWindow"; public string HandleDescriptor => "NSWindow";
public IntPtr NSView => _native.ObtainNSViewHandle(); public IntPtr NSView => _native?.ObtainNSViewHandle() ?? IntPtr.Zero;
public IntPtr NSWindow => _native.ObtainNSWindowHandle(); public IntPtr NSWindow => _native?.ObtainNSWindowHandle() ?? IntPtr.Zero;
public IntPtr GetNSViewRetained() public IntPtr GetNSViewRetained()
{ {
return _native.ObtainNSViewHandleRetained(); return _native?.ObtainNSViewHandleRetained() ?? IntPtr.Zero;
} }
public IntPtr GetNSWindowRetained() public IntPtr GetNSWindowRetained()
{ {
return _native.ObtainNSWindowHandleRetained(); return _native?.ObtainNSWindowHandleRetained() ?? IntPtr.Zero;
} }
} }
@ -260,7 +260,7 @@ namespace Avalonia.Native
public void Activate() public void Activate()
{ {
_native.Activate(); _native?.Activate();
} }
public bool RawTextInputEvent(uint timeStamp, string text) public bool RawTextInputEvent(uint timeStamp, string text)
@ -322,7 +322,7 @@ namespace Avalonia.Native
public void Resize(Size clientSize, PlatformResizeReason reason) public void Resize(Size clientSize, PlatformResizeReason reason)
{ {
_native.Resize(clientSize.Width, clientSize.Height, (AvnPlatformResizeReason)reason); _native?.Resize(clientSize.Width, clientSize.Height, (AvnPlatformResizeReason)reason);
} }
public IRenderer CreateRenderer(IRenderRoot root) public IRenderer CreateRenderer(IRenderRoot root)
@ -367,14 +367,14 @@ namespace Avalonia.Native
public virtual void Show(bool activate, bool isDialog) public virtual void Show(bool activate, bool isDialog)
{ {
_native.Show(activate.AsComBool(), isDialog.AsComBool()); _native?.Show(activate.AsComBool(), isDialog.AsComBool());
} }
public PixelPoint Position public PixelPoint Position
{ {
get => _native.Position.ToAvaloniaPixelPoint(); get => _native?.Position.ToAvaloniaPixelPoint() ?? default;
set => _native.SetPosition(value.ToAvnPoint()); set => _native?.SetPosition(value.ToAvnPoint());
} }
public Point PointToClient(PixelPoint point) public Point PointToClient(PixelPoint point)
@ -389,12 +389,12 @@ namespace Avalonia.Native
public void Hide() public void Hide()
{ {
_native.Hide(); _native?.Hide();
} }
public void BeginMoveDrag(PointerPressedEventArgs e) public void BeginMoveDrag(PointerPressedEventArgs e)
{ {
_native.BeginMoveDrag(); _native?.BeginMoveDrag();
} }
public Size MaxAutoSizeHint => Screen.AllScreens.Select(s => s.Bounds.Size.ToSize(1)) public Size MaxAutoSizeHint => Screen.AllScreens.Select(s => s.Bounds.Size.ToSize(1))
@ -402,7 +402,7 @@ namespace Avalonia.Native
public void SetTopmost(bool value) public void SetTopmost(bool value)
{ {
_native.SetTopMost(value.AsComBool()); _native?.SetTopMost(value.AsComBool());
} }
public double RenderScaling => _native?.Scaling ?? 1; public double RenderScaling => _native?.Scaling ?? 1;
@ -438,7 +438,7 @@ namespace Avalonia.Native
public void SetMinMaxSize(Size minSize, Size maxSize) public void SetMinMaxSize(Size minSize, Size maxSize)
{ {
_native.SetMinMaxSize(minSize.ToAvnSize(), maxSize.ToAvnSize()); _native?.SetMinMaxSize(minSize.ToAvnSize(), maxSize.ToAvnSize());
} }
public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e)
@ -449,7 +449,7 @@ namespace Avalonia.Native
internal void BeginDraggingSession(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard clipboard, internal void BeginDraggingSession(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard clipboard,
IAvnDndResultCallback callback, IntPtr sourceHandle) IAvnDndResultCallback callback, IntPtr sourceHandle)
{ {
_native.BeginDragAndDropOperation(effects, point, clipboard, callback, sourceHandle); _native?.BeginDragAndDropOperation(effects, point, clipboard, callback, sourceHandle);
} }
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)

2
src/Avalonia.OpenGL/Avalonia.OpenGL.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>

2
src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<PackageId>Avalonia.ReactiveUI</PackageId> <PackageId>Avalonia.ReactiveUI</PackageId>
<SignAssembly>false</SignAssembly> <SignAssembly>false</SignAssembly>
</PropertyGroup> </PropertyGroup>

57
src/Avalonia.ReactiveUI/RoutedViewHost.cs

@ -57,7 +57,13 @@ namespace Avalonia.ReactiveUI
/// </summary> /// </summary>
public static readonly StyledProperty<RoutingState?> RouterProperty = public static readonly StyledProperty<RoutingState?> RouterProperty =
AvaloniaProperty.Register<RoutedViewHost, RoutingState?>(nameof(Router)); AvaloniaProperty.Register<RoutedViewHost, RoutingState?>(nameof(Router));
/// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="ViewContract"/> property.
/// </summary>
public static readonly StyledProperty<string?> ViewContractProperty =
AvaloniaProperty.Register<ViewModelViewHost, string?>(nameof(ViewContract));
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RoutedViewHost"/> class. /// Initializes a new instance of the <see cref="RoutedViewHost"/> class.
/// </summary> /// </summary>
@ -70,15 +76,18 @@ namespace Avalonia.ReactiveUI
.Where(router => router == null)! .Where(router => router == null)!
.Cast<object?>(); .Cast<object?>();
var viewContract = this.WhenAnyValue(x => x.ViewContract);
this.WhenAnyValue(x => x.Router) this.WhenAnyValue(x => x.Router)
.Where(router => router != null) .Where(router => router != null)
.SelectMany(router => router!.CurrentViewModel) .SelectMany(router => router!.CurrentViewModel)
.Merge(routerRemoved) .Merge(routerRemoved)
.Subscribe(NavigateToViewModel) .CombineLatest(viewContract)
.Subscribe(tuple => NavigateToViewModel(tuple.First, tuple.Second))
.DisposeWith(disposables); .DisposeWith(disposables);
}); });
} }
/// <summary> /// <summary>
/// Gets or sets the <see cref="RoutingState"/> of the view model stack. /// Gets or sets the <see cref="RoutingState"/> of the view model stack.
/// </summary> /// </summary>
@ -87,17 +96,27 @@ namespace Avalonia.ReactiveUI
get => GetValue(RouterProperty); get => GetValue(RouterProperty);
set => SetValue(RouterProperty, value); set => SetValue(RouterProperty, value);
} }
/// <summary>
/// Gets or sets the view contract.
/// </summary>
public string? ViewContract
{
get => GetValue(ViewContractProperty);
set => SetValue(ViewContractProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets the ReactiveUI view locator used by this router. /// Gets or sets the ReactiveUI view locator used by this router.
/// </summary> /// </summary>
public IViewLocator? ViewLocator { get; set; } public IViewLocator? ViewLocator { get; set; }
/// <summary> /// <summary>
/// Invoked when ReactiveUI router navigates to a view model. /// Invoked when ReactiveUI router navigates to a view model.
/// </summary> /// </summary>
/// <param name="viewModel">ViewModel to which the user navigates.</param> /// <param name="viewModel">ViewModel to which the user navigates.</param>
private void NavigateToViewModel(object? viewModel) /// <param name="contract">The contract for view resolution.</param>
private void NavigateToViewModel(object? viewModel, string? contract)
{ {
if (Router == null) if (Router == null)
{ {
@ -112,17 +131,33 @@ namespace Avalonia.ReactiveUI
Content = DefaultContent; Content = DefaultContent;
return; return;
} }
var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current;
var viewInstance = viewLocator.ResolveView(viewModel); var viewInstance = viewLocator.ResolveView(viewModel, contract);
if (viewInstance == null) if (viewInstance == null)
{ {
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); if (contract == null)
{
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content.");
}
else
{
this.Log().Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content.");
}
Content = DefaultContent; Content = DefaultContent;
return; return;
} }
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); if (contract == null)
{
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}.");
}
else
{
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel} and contract '{contract}'.");
}
viewInstance.ViewModel = viewModel; viewInstance.ViewModel = viewModel;
if (viewInstance is IDataContextProvider provider) if (viewInstance is IDataContextProvider provider)
provider.DataContext = viewModel; provider.DataContext = viewModel;

52
src/Avalonia.ReactiveUI/ViewModelViewHost.cs

@ -3,7 +3,7 @@ using System.Reactive.Disposables;
using ReactiveUI; using ReactiveUI;
using Splat; using Splat;
namespace Avalonia.ReactiveUI namespace Avalonia.ReactiveUI
{ {
/// <summary> /// <summary>
/// This content control will automatically load the View associated with /// This content control will automatically load the View associated with
@ -18,6 +18,12 @@ namespace Avalonia.ReactiveUI
public static readonly AvaloniaProperty<object?> ViewModelProperty = public static readonly AvaloniaProperty<object?> ViewModelProperty =
AvaloniaProperty.Register<ViewModelViewHost, object?>(nameof(ViewModel)); AvaloniaProperty.Register<ViewModelViewHost, object?>(nameof(ViewModel));
/// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="ViewContract"/> property.
/// </summary>
public static readonly StyledProperty<string?> ViewContractProperty =
AvaloniaProperty.Register<ViewModelViewHost, string?>(nameof(ViewContract));
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ViewModelViewHost"/> class. /// Initializes a new instance of the <see cref="ViewModelViewHost"/> class.
/// </summary> /// </summary>
@ -25,8 +31,8 @@ namespace Avalonia.ReactiveUI
{ {
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
this.WhenAnyValue(x => x.ViewModel) this.WhenAnyValue(x => x.ViewModel, x => x.ViewContract)
.Subscribe(NavigateToViewModel) .Subscribe(tuple => NavigateToViewModel(tuple.Item1, tuple.Item2))
.DisposeWith(disposables); .DisposeWith(disposables);
}); });
} }
@ -39,7 +45,16 @@ namespace Avalonia.ReactiveUI
get => GetValue(ViewModelProperty); get => GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value); set => SetValue(ViewModelProperty, value);
} }
/// <summary>
/// Gets or sets the view contract.
/// </summary>
public string? ViewContract
{
get => GetValue(ViewContractProperty);
set => SetValue(ViewContractProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets the view locator. /// Gets or sets the view locator.
/// </summary> /// </summary>
@ -49,7 +64,8 @@ namespace Avalonia.ReactiveUI
/// Invoked when ReactiveUI router navigates to a view model. /// Invoked when ReactiveUI router navigates to a view model.
/// </summary> /// </summary>
/// <param name="viewModel">ViewModel to which the user navigates.</param> /// <param name="viewModel">ViewModel to which the user navigates.</param>
private void NavigateToViewModel(object? viewModel) /// <param name="contract">The contract for view resolution.</param>
private void NavigateToViewModel(object? viewModel, string? contract)
{ {
if (viewModel == null) if (viewModel == null)
{ {
@ -57,17 +73,33 @@ namespace Avalonia.ReactiveUI
Content = DefaultContent; Content = DefaultContent;
return; return;
} }
var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current;
var viewInstance = viewLocator.ResolveView(viewModel); var viewInstance = viewLocator.ResolveView(viewModel, contract);
if (viewInstance == null) if (viewInstance == null)
{ {
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); if (contract == null)
{
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content.");
}
else
{
this.Log().Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content.");
}
Content = DefaultContent; Content = DefaultContent;
return; return;
} }
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); if (contract == null)
{
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}.");
}
else
{
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel} and contract '{contract}'.");
}
viewInstance.ViewModel = viewModel; viewInstance.ViewModel = viewModel;
if (viewInstance is IStyledElement styled) if (viewInstance is IStyledElement styled)
styled.DataContext = viewModel; styled.DataContext = viewModel;

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

Loading…
Cancel
Save