Browse Source

Merge branch 'master' into patch-1

pull/10553/head
Dan Walmsley 3 years ago
committed by GitHub
parent
commit
9e7c05d2fa
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .nuke/build.schema.json
  2. 21
      Avalonia.sln
  3. 17
      build/SourceGenerators.props
  4. 12
      nukebuild/Build.cs
  5. 5
      nukebuild/numerge.config
  6. 6
      packages/Avalonia/Avalonia.csproj
  7. 14
      samples/ControlCatalog/ControlCatalog.csproj
  8. 8
      samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs
  9. 11
      samples/ControlCatalog/Pages/LabelsPage.axaml.cs
  10. 14
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
  11. 13
      samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs
  12. 20
      samples/ControlCatalog/Pages/ThemePage.axaml.cs
  13. 7
      samples/Generators.Sandbox/App.xaml
  14. 20
      samples/Generators.Sandbox/App.xaml.cs
  15. 10
      samples/Generators.Sandbox/Controls/CustomTextBox.cs
  16. 45
      samples/Generators.Sandbox/Controls/SignUpView.xaml
  17. 54
      samples/Generators.Sandbox/Controls/SignUpView.xaml.cs
  18. 28
      samples/Generators.Sandbox/Generators.Sandbox.csproj
  19. 15
      samples/Generators.Sandbox/Program.cs
  20. 70
      samples/Generators.Sandbox/ViewModels/SignUpViewModel.cs
  21. 9
      samples/Generators.Sandbox/Views/SignUpView.xaml
  22. 28
      samples/Generators.Sandbox/Views/SignUpView.xaml.cs
  23. 32
      src/tools/Avalonia.Generators/Avalonia.Generators.csproj
  24. 22
      src/tools/Avalonia.Generators/Avalonia.Generators.props
  25. 9
      src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs
  26. 6
      src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs
  27. 19
      src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs
  28. 11
      src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs
  29. 18
      src/tools/Avalonia.Generators/Common/GlobPattern.cs
  30. 17
      src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs
  31. 25
      src/tools/Avalonia.Generators/Common/ResolverExtensions.cs
  32. 92
      src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs
  33. 100
      src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs
  34. 17
      src/tools/Avalonia.Generators/Compiler/DataTemplateTransformer.cs
  35. 50
      src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs
  36. 28
      src/tools/Avalonia.Generators/Compiler/NameDirectiveTransformer.cs
  37. 276
      src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs
  38. 36
      src/tools/Avalonia.Generators/GeneratorContextExtensions.cs
  39. 71
      src/tools/Avalonia.Generators/GeneratorOptions.cs
  40. 63
      src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs
  41. 60
      src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs
  42. 11
      src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs
  43. 83
      src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs
  44. 31
      src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs
  45. 21
      src/tools/Avalonia.Generators/NameGenerator/Options.cs
  46. 8
      src/tools/Avalonia.Generators/Properties/launchSettings.json
  47. 209
      src/tools/Avalonia.Generators/README.md
  48. 26
      tests/Avalonia.Generators.Tests/Avalonia.Generators.Tests.csproj
  49. 31
      tests/Avalonia.Generators.Tests/GlobPatternTests.cs
  50. 28
      tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/AttachedProps.txt
  51. 36
      tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/AttachedPropsWithDevTools.txt
  52. 28
      tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/ControlWithoutWindow.txt
  53. 32
      tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/CustomControls.txt
  54. 30
      tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/DataTemplates.txt
  55. 38
      tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/FieldModifier.txt
  56. 35
      tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/InitializeComponentCode.cs
  57. 28
      tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NamedControl.txt
  58. 32
      tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NamedControls.txt
  59. 28
      tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NoNamedControls.txt
  60. 46
      tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/SignUpView.txt
  61. 28
      tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/xNamedControl.txt
  62. 32
      tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/xNamedControls.txt
  63. 63
      tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs
  64. 59
      tests/Avalonia.Generators.Tests/MiniCompilerTests.cs
  65. 11
      tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/AttachedProps.txt
  66. 11
      tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/ControlWithoutWindow.txt
  67. 13
      tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/CustomControls.txt
  68. 12
      tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/DataTemplates.txt
  69. 16
      tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/FieldModifier.txt
  70. 11
      tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NamedControl.txt
  71. 13
      tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NamedControls.txt
  72. 11
      tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NoNamedControls.txt
  73. 33
      tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/OnlyPropertiesCode.cs
  74. 20
      tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/SignUpView.txt
  75. 11
      tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/xNamedControl.txt
  76. 13
      tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/xNamedControls.txt
  77. 52
      tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs
  78. 10
      tests/Avalonia.Generators.Tests/Views/AttachedProps.xml
  79. 10
      tests/Avalonia.Generators.Tests/Views/ControlWithoutWindow.xml
  80. 11
      tests/Avalonia.Generators.Tests/Views/CustomControls.xml
  81. 18
      tests/Avalonia.Generators.Tests/Views/DataTemplates.xml
  82. 28
      tests/Avalonia.Generators.Tests/Views/FieldModifier.xml
  83. 7
      tests/Avalonia.Generators.Tests/Views/NamedControl.xml
  84. 14
      tests/Avalonia.Generators.Tests/Views/NamedControls.xml
  85. 6
      tests/Avalonia.Generators.Tests/Views/NoNamedControls.xml
  86. 52
      tests/Avalonia.Generators.Tests/Views/SignUpView.xml
  87. 76
      tests/Avalonia.Generators.Tests/Views/View.cs
  88. 13
      tests/Avalonia.Generators.Tests/Views/ViewWithGenericBaseView.xml
  89. 7
      tests/Avalonia.Generators.Tests/Views/xNamedControl.xml
  90. 14
      tests/Avalonia.Generators.Tests/Views/xNamedControls.xml
  91. 40
      tests/Avalonia.Generators.Tests/XamlXClassResolverTests.cs
  92. 141
      tests/Avalonia.Generators.Tests/XamlXNameResolverTests.cs
  93. 1
      tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs

4
.nuke/build.schema.json

@ -84,11 +84,11 @@
"GenerateCppHeaders",
"Package",
"RunCoreLibsTests",
"RunDesignerTests",
"RunHtmlPreviewerTests",
"RunLeakTests",
"RunRenderTests",
"RunTests",
"RunToolsTests",
"ZipFiles"
]
}
@ -123,11 +123,11 @@
"GenerateCppHeaders",
"Package",
"RunCoreLibsTests",
"RunDesignerTests",
"RunHtmlPreviewerTests",
"RunLeakTests",
"RunRenderTests",
"RunTests",
"RunToolsTests",
"ZipFiles"
]
}

21
Avalonia.sln

@ -244,8 +244,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepe
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater.UnitTests", "tests\Avalonia.Controls.ItemsRepeater.UnitTests\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "{F4E36AA8-814E-4704-BC07-291F70F45193}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -573,10 +579,22 @@ Global
{F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU
{DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.Build.0 = Release|Any CPU
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.Build.0 = Release|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.Build.0 = Release|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -643,7 +661,10 @@ Global
{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

17
build/SourceGenerators.props

@ -1,5 +1,10 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PropertyGroup>
<IncludeDevGenerators Condition="'$(IncludeDevGenerators)' == ''">true</IncludeDevGenerators>
<IncludeAvaloniaGenerators Condition="'$(IncludeAvaloniaGenerators)' == ''">false</IncludeAvaloniaGenerators>
</PropertyGroup>
<ItemGroup Condition="'$(IncludeDevGenerators)' == 'true'">
<ProjectReference
Include="$(MSBuildThisFileDirectory)/../src/tools/DevGenerators/DevGenerators.csproj"
OutputItemType="Analyzer"
@ -7,4 +12,14 @@
PrivateAssets="all" />
<Compile Include="$(MSBuildThisFileDirectory)/../src/Shared/SourceGeneratorAttributes.cs" />
</ItemGroup>
<ItemGroup Condition="'$(IncludeAvaloniaGenerators)' == 'true'">
<ProjectReference
Include="../../src/tools/Avalonia.Generators/Avalonia.Generators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"
PrivateAssets="all" />
</ItemGroup>
<Import Project="$(MSBuildThisFileDirectory)/../src/tools/Avalonia.Generators/Avalonia.Generators.props"
Condition="'$(IncludeDevGenerators)' == 'true'" />
</Project>

12
nukebuild/Build.cs

@ -220,16 +220,18 @@ partial class Build : NukeBuild
.Executes(() =>
{
RunCoreTest("Avalonia.Skia.RenderTests");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (Parameters.IsRunningOnWindows)
RunCoreTest("Avalonia.Direct2D1.RenderTests");
});
Target RunDesignerTests => _ => _
.OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows)
Target RunToolsTests => _ => _
.OnlyWhenStatic(() => !Parameters.SkipTests)
.DependsOn(Compile)
.Executes(() =>
{
RunCoreTest("Avalonia.DesignerSupport.Tests");
RunCoreTest("Avalonia.Generators.Tests");
if (Parameters.IsRunningOnWindows)
RunCoreTest("Avalonia.DesignerSupport.Tests");
});
Target RunLeakTests => _ => _
@ -276,7 +278,7 @@ partial class Build : NukeBuild
Target RunTests => _ => _
.DependsOn(RunCoreLibsTests)
.DependsOn(RunRenderTests)
.DependsOn(RunDesignerTests)
.DependsOn(RunToolsTests)
.DependsOn(RunHtmlPreviewerTests)
.DependsOn(RunLeakTests);

5
nukebuild/numerge.config

@ -11,6 +11,11 @@
"Id": "Avalonia.Build.Tasks",
"IgnoreMissingFrameworkBinaries": true,
"DoNotMergeDependencies": true
},
{
"Id": "Avalonia.Generators",
"IgnoreMissingFrameworkBinaries": true,
"DoNotMergeDependencies": true
}
]
}

6
packages/Avalonia/Avalonia.csproj

@ -6,11 +6,15 @@
<ItemGroup>
<ProjectReference Include="../../src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj" />
<ProjectReference Include="../../src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj" >
<ProjectReference Include="../../src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj">
<PrivateAssets>all</PrivateAssets>
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
<SetTargetFramework>TargetFramework=netstandard2.0</SetTargetFramework>
</ProjectReference>
<ProjectReference Include="..\..\src\tools\Avalonia.Generators\Avalonia.Generators.csproj"
ReferenceOutputAssembly="false"
PrivateAssets="all"
OutputItemType="Analyzer" />
</ItemGroup>
<PropertyGroup>

14
samples/ControlCatalog/ControlCatalog.csproj

@ -2,7 +2,8 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<Nullable>enable</Nullable>
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
</PropertyGroup>
<ItemGroup>
<Compile Update="**\*.xaml.cs">
@ -35,14 +36,5 @@
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />
<ItemGroup>
<None Remove="Pages\CustomDrawing.xaml" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Update="Pages\CustomDrawing.xaml">
<Generator></Generator>
</AvaloniaResource>
</ItemGroup>
<Import Project="..\..\build\SourceGenerators.props" />
</Project>

8
samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs

@ -1,11 +1,10 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Markup.Xaml;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public class FlyoutsPage : UserControl
public partial class FlyoutsPage : UserControl
{
public FlyoutsPage()
{
@ -28,11 +27,6 @@ namespace ControlCatalog.Pages
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void SetXamlTexts()
{
var bfxt = this.Get<TextBlock>("ButtonFlyoutXamlText");

11
samples/ControlCatalog/Pages/LabelsPage.axaml.cs

@ -1,11 +1,9 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Controls;
using ControlCatalog.Models;
namespace ControlCatalog.Pages
{
public class LabelsPage : UserControl
public partial class LabelsPage : UserControl
{
private Person? _person;
@ -25,11 +23,6 @@ namespace ControlCatalog.Pages
};
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public void DoSave()
{

14
samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs

@ -1,18 +1,15 @@
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Controls;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
public class RefreshContainerPage : UserControl
public partial class RefreshContainerPage : UserControl
{
private RefreshContainerViewModel _viewModel;
public RefreshContainerPage()
{
this.InitializeComponent();
InitializeComponent();
_viewModel = new RefreshContainerViewModel();
@ -27,10 +24,5 @@ namespace ControlCatalog.Pages
deferral.Complete();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

13
samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs

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

20
samples/ControlCatalog/Pages/ThemePage.axaml.cs

@ -1,35 +1,31 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
namespace ControlCatalog.Pages
{
public class ThemePage : UserControl
public partial class ThemePage : UserControl
{
public static ThemeVariant Pink { get; } = new("Pink", ThemeVariant.Light);
public ThemePage()
{
AvaloniaXamlLoader.Load(this);
InitializeComponent();
var selector = this.FindControl<ComboBox>("Selector")!;
var themeVariantScope = this.FindControl<ThemeVariantScope>("ThemeVariantScope")!;
selector.Items = new[]
Selector.Items = new[]
{
ThemeVariant.Default,
ThemeVariant.Dark,
ThemeVariant.Light,
Pink
};
selector.SelectedIndex = 0;
Selector.SelectedIndex = 0;
selector.SelectionChanged += (_, _) =>
Selector.SelectionChanged += (_, _) =>
{
if (selector.SelectedItem is ThemeVariant theme)
if (Selector.SelectedItem is ThemeVariant theme)
{
themeVariantScope.RequestedThemeVariant = theme;
ThemeVariantScope.RequestedThemeVariant = theme;
}
};
}

7
samples/Generators.Sandbox/App.xaml

@ -0,0 +1,7 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Generators.Sandbox.App">
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

20
samples/Generators.Sandbox/App.xaml.cs

@ -0,0 +1,20 @@
using Avalonia;
using Avalonia.Markup.Xaml;
using Generators.Sandbox.ViewModels;
namespace Generators.Sandbox;
public class App : Application
{
public override void Initialize() => AvaloniaXamlLoader.Load(this);
public override void OnFrameworkInitializationCompleted()
{
var view = new Views.SignUpView
{
ViewModel = new SignUpViewModel()
};
view.Show();
base.OnFrameworkInitializationCompleted();
}
}

10
samples/Generators.Sandbox/Controls/CustomTextBox.cs

@ -0,0 +1,10 @@
using System;
using Avalonia.Controls;
using Avalonia.Styling;
namespace Generators.Sandbox.Controls;
public class CustomTextBox : TextBox, IStyleable
{
Type IStyleable.StyleKey => typeof(TextBox);
}

45
samples/Generators.Sandbox/Controls/SignUpView.xaml

@ -0,0 +1,45 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Generators.Sandbox.Controls"
x:Class="Generators.Sandbox.Controls.SignUpView">
<StackPanel>
<controls:CustomTextBox Margin="0 10 0 0"
x:Name="UserNameTextBox"
Watermark="Please, enter user name..."
UseFloatingWatermark="True" />
<TextBlock x:Name="UserNameValidation"
Foreground="Red"
FontSize="12" />
<TextBox Margin="0 10 0 0"
x:Name="PasswordTextBox"
Watermark="Please, enter your password..."
UseFloatingWatermark="True"
PasswordChar="*" />
<TextBlock x:Name="PasswordValidation"
Foreground="Red"
FontSize="12" />
<TextBox Margin="0 10 0 0"
x:Name="ConfirmPasswordTextBox"
Watermark="Please, confirm the password..."
UseFloatingWatermark="True"
PasswordChar="*" />
<TextBlock x:Name="ConfirmPasswordValidation"
TextWrapping="Wrap"
Foreground="Red"
FontSize="12" />
<TextBlock>
<TextBlock.Inlines>
<InlineCollection>
<Run x:Name="SignUpButtonDescription" />
</InlineCollection>
</TextBlock.Inlines>
</TextBlock>
<Button Margin="0 10 0 5"
Content="Sign up"
x:Name="SignUpButton" />
<TextBlock x:Name="CompoundValidation"
TextWrapping="Wrap"
Foreground="Red"
FontSize="12" />
</StackPanel>
</UserControl>

54
samples/Generators.Sandbox/Controls/SignUpView.xaml.cs

@ -0,0 +1,54 @@
using System;
using System.Reactive.Disposables;
using Avalonia.ReactiveUI;
using Generators.Sandbox.ViewModels;
using ReactiveUI;
using ReactiveUI.Validation.Extensions;
using ReactiveUI.Validation.Formatters;
namespace Generators.Sandbox.Controls;
/// <summary>
/// This is a sample view class with typed x:Name references generated using
/// .NET 5 source generators. The class has to be partial because x:Name
/// references are living in a separate partial class file. See also:
/// https://devblogs.microsoft.com/dotnet/new-c-source-generator-samples/
/// </summary>
public partial class SignUpView : ReactiveUserControl<SignUpViewModel>
{
public SignUpView()
{
// The InitializeComponent method is also generated automatically
// and lives in the autogenerated part of the partial class.
InitializeComponent();
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, x => x.UserName, x => x.UserNameTextBox.Text)
.DisposeWith(disposables);
this.Bind(ViewModel, x => x.Password, x => x.PasswordTextBox.Text)
.DisposeWith(disposables);
this.Bind(ViewModel, x => x.ConfirmPassword, x => x.ConfirmPasswordTextBox.Text)
.DisposeWith(disposables);
this.BindCommand(ViewModel, x => x.SignUp, x => x.SignUpButton)
.DisposeWith(disposables);
this.BindValidation(ViewModel, x => x.UserName, x => x.UserNameValidation.Text)
.DisposeWith(disposables);
this.BindValidation(ViewModel, x => x.Password, x => x.PasswordValidation.Text)
.DisposeWith(disposables);
this.BindValidation(ViewModel, x => x.ConfirmPassword, x => x.ConfirmPasswordValidation.Text)
.DisposeWith(disposables);
var newLineFormatter = new SingleLineFormatter(Environment.NewLine);
this.BindValidation(ViewModel, x => x.CompoundValidation.Text, newLineFormatter)
.DisposeWith(disposables);
// The references to text boxes below are also auto generated.
// Use Ctrl+Click in order to view the generated sources.
UserNameTextBox.Text = "Joseph!";
PasswordTextBox.Text = "1234";
ConfirmPasswordTextBox.Text = "1234";
SignUpButtonDescription.Text = "Press the button below to sign up.";
});
}
}

28
samples/Generators.Sandbox/Generators.Sandbox.csproj

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="**\*.xaml"/>
<!-- Note this AdditionalFiles directive. -->
<AdditionalFiles Include="**\*.xaml"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="ReactiveUI.Validation" Version="3.0.22"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj"/>
<ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj"/>
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj"/>
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj"/>
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj"/>
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets"/>
<Import Project="..\..\build\SourceGenerators.props"/>
</Project>

15
samples/Generators.Sandbox/Program.cs

@ -0,0 +1,15 @@
using Avalonia;
using Avalonia.ReactiveUI;
namespace Generators.Sandbox;
internal static class Program
{
public static void Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
private static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UseReactiveUI()
.UsePlatformDetect()
.LogToTrace();
}

70
samples/Generators.Sandbox/ViewModels/SignUpViewModel.cs

@ -0,0 +1,70 @@
using System.Reactive;
using ReactiveUI;
using ReactiveUI.Validation.Extensions;
using ReactiveUI.Validation.Helpers;
namespace Generators.Sandbox.ViewModels;
public class SignUpViewModel : ReactiveValidationObject
{
private string _userName = string.Empty;
private string _password = string.Empty;
private string _confirmPassword = string.Empty;
public SignUpViewModel()
{
this.ValidationRule(
vm => vm.UserName,
name => !string.IsNullOrWhiteSpace(name),
"UserName is required.");
this.ValidationRule(
vm => vm.Password,
password => !string.IsNullOrWhiteSpace(password),
"Password is required.");
this.ValidationRule(
vm => vm.Password,
password => password?.Length > 2,
password => $"Password should be longer, current length: {password.Length}");
this.ValidationRule(
vm => vm.ConfirmPassword,
confirmation => !string.IsNullOrWhiteSpace(confirmation),
"Confirm password field is required.");
var passwordsObservable =
this.WhenAnyValue(
x => x.Password,
x => x.ConfirmPassword,
(password, confirmation) =>
password == confirmation);
this.ValidationRule(
vm => vm.ConfirmPassword,
passwordsObservable,
"Passwords must match.");
SignUp = ReactiveCommand.Create(() => {}, this.IsValid());
}
public ReactiveCommand<Unit, Unit> SignUp { get; }
public string UserName
{
get => _userName;
set => this.RaiseAndSetIfChanged(ref _userName, value);
}
public string Password
{
get => _password;
set => this.RaiseAndSetIfChanged(ref _password, value);
}
public string ConfirmPassword
{
get => _confirmPassword;
set => this.RaiseAndSetIfChanged(ref _confirmPassword, value);
}
}

9
samples/Generators.Sandbox/Views/SignUpView.xaml

@ -0,0 +1,9 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Generators.Sandbox.Controls"
x:Class="Generators.Sandbox.Views.SignUpView">
<StackPanel Margin="10">
<TextBlock Text="Sign Up" />
<controls:SignUpView x:Name="SignUpControl" />
</StackPanel>
</Window>

28
samples/Generators.Sandbox/Views/SignUpView.xaml.cs

@ -0,0 +1,28 @@
using System.Reactive.Disposables;
using Avalonia.ReactiveUI;
using Generators.Sandbox.ViewModels;
using ReactiveUI;
namespace Generators.Sandbox.Views;
/// <summary>
/// This is a sample view class with typed x:Name references generated using
/// .NET 5 source generators. The class has to be partial because x:Name
/// references are living in a separate partial class file. See also:
/// https://devblogs.microsoft.com/dotnet/new-c-source-generator-samples/
/// </summary>
public partial class SignUpView : ReactiveWindow<SignUpViewModel>
{
public SignUpView()
{
// The InitializeComponent method is also generated automatically
// and lives in the autogenerated part of the partial class.
InitializeComponent();
this.WhenActivated(disposables =>
{
this.WhenAnyValue(view => view.ViewModel)
.BindTo(this, view => view.SignUpControl.ViewModel)
.DisposeWith(disposables);
});
}
}

32
src/tools/Avalonia.Generators/Avalonia.Generators.csproj

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IncludeBuildOutput>false</IncludeBuildOutput>
<PackageId>Avalonia.Generators</PackageId>
<DefineConstants>$(DefineConstants);XAMLX_INTERNAL</DefineConstants>
<IsPackable>true</IsPackable>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<Compile Link="Compiler\XamlX\filename" Include="../../Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/src/XamlX/**/*.cs" />
<Compile Remove="../../Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/src/XamlX/**/SreTypeSystem.cs" />
<Compile Include="..\..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" />
</ItemGroup>
<ItemGroup>
<None Include="Avalonia.Generators.props" Pack="true" PackagePath="buildTransitive/$(PackageId).props" />
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
<ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.Generators.Tests, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup>
<Import Project="..\..\..\build\TrimmingEnable.props" />
</Project>

22
src/tools/Avalonia.Generators/Avalonia.Generators.props

@ -0,0 +1,22 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<AvaloniaNameGeneratorIsEnabled Condition="'$(AvaloniaNameGeneratorIsEnabled)' == ''">true</AvaloniaNameGeneratorIsEnabled>
<AvaloniaNameGeneratorBehavior Condition="'$(AvaloniaNameGeneratorBehavior)' == ''">InitializeComponent</AvaloniaNameGeneratorBehavior>
<AvaloniaNameGeneratorDefaultFieldModifier Condition="'$(AvaloniaNameGeneratorDefaultFieldModifier)' == ''">internal</AvaloniaNameGeneratorDefaultFieldModifier>
<AvaloniaNameGeneratorFilterByPath Condition="'$(AvaloniaNameGeneratorFilterByPath)' == ''">*</AvaloniaNameGeneratorFilterByPath>
<AvaloniaNameGeneratorFilterByNamespace Condition="'$(AvaloniaNameGeneratorFilterByNamespace)' == ''">*</AvaloniaNameGeneratorFilterByNamespace>
</PropertyGroup>
<ItemGroup>
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="SourceItemGroup"/>
<CompilerVisibleProperty Include="AvaloniaNameGeneratorIsEnabled" />
<CompilerVisibleProperty Include="AvaloniaNameGeneratorBehavior" />
<CompilerVisibleProperty Include="AvaloniaNameGeneratorDefaultFieldModifier" />
<CompilerVisibleProperty Include="AvaloniaNameGeneratorFilterByPath" />
<CompilerVisibleProperty Include="AvaloniaNameGeneratorFilterByNamespace" />
</ItemGroup>
<Target Name="_InjectAdditionalFiles" BeforeTargets="GenerateMSBuildEditorConfigFileShouldRun">
<ItemGroup>
<AdditionalFiles Include="@(AvaloniaXaml)" SourceItemGroup="AvaloniaXaml" />
</ItemGroup>
</Target>
</Project>

9
src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs

@ -0,0 +1,9 @@
using System.Collections.Generic;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common.Domain;
internal interface ICodeGenerator
{
string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names);
}

6
src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs

@ -0,0 +1,6 @@
namespace Avalonia.Generators.Common.Domain;
internal interface IGlobPattern
{
bool Matches(string str);
}

19
src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs

@ -0,0 +1,19 @@
using System.Collections.Generic;
using XamlX.Ast;
namespace Avalonia.Generators.Common.Domain;
internal enum NamedFieldModifier
{
Public = 0,
Private = 1,
Internal = 2,
Protected = 3,
}
internal interface INameResolver
{
IReadOnlyList<ResolvedName> ResolveNames(XamlDocument xaml);
}
internal record ResolvedName(string TypeName, string Name, string FieldModifier);

11
src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs

@ -0,0 +1,11 @@
using XamlX.Ast;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common.Domain;
internal interface IViewResolver
{
ResolvedView ResolveView(string xaml);
}
internal record ResolvedView(string ClassName, IXamlType XamlType, string Namespace, XamlDocument Xaml);

18
src/tools/Avalonia.Generators/Common/GlobPattern.cs

@ -0,0 +1,18 @@
using System.Text.RegularExpressions;
using Avalonia.Generators.Common.Domain;
namespace Avalonia.Generators.Common;
internal class GlobPattern : IGlobPattern
{
private const RegexOptions GlobOptions = RegexOptions.IgnoreCase | RegexOptions.Singleline;
private readonly Regex _regex;
public GlobPattern(string pattern)
{
var expression = "^" + Regex.Escape(pattern).Replace(@"\*", ".*").Replace(@"\?", ".") + "$";
_regex = new Regex(expression, GlobOptions);
}
public bool Matches(string str) => _regex.IsMatch(str);
}

17
src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs

@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Generators.Common.Domain;
namespace Avalonia.Generators.Common;
internal class GlobPatternGroup : IGlobPattern
{
private readonly GlobPattern[] _patterns;
public GlobPatternGroup(IEnumerable<string> patterns) =>
_patterns = patterns
.Select(pattern => new GlobPattern(pattern))
.ToArray();
public bool Matches(string str) => _patterns.Any(pattern => pattern.Matches(str));
}

25
src/tools/Avalonia.Generators/Common/ResolverExtensions.cs

@ -0,0 +1,25 @@
using System.Linq;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common;
internal static class ResolverExtensions
{
public static bool IsAvaloniaStyledElement(this IXamlType clrType) =>
clrType.HasStyledElementBaseType() ||
clrType.HasIStyledElementInterface();
private static bool HasStyledElementBaseType(this IXamlType clrType)
{
// Check for the base type since IStyledElement interface is removed.
// https://github.com/AvaloniaUI/Avalonia/pull/9553
if (clrType.FullName == "Avalonia.StyledElement")
return true;
return clrType.BaseType != null && IsAvaloniaStyledElement(clrType.BaseType);
}
private static bool HasIStyledElementInterface(this IXamlType clrType) =>
clrType.Interfaces.Any(abstraction =>
abstraction.IsInterface &&
abstraction.FullName == "Avalonia.IStyledElement");
}

92
src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs

@ -0,0 +1,92 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Avalonia.Generators.Common.Domain;
using XamlX;
using XamlX.Ast;
namespace Avalonia.Generators.Common;
internal class XamlXNameResolver : INameResolver, IXamlAstVisitor
{
private readonly List<ResolvedName> _items = new();
private readonly string _defaultFieldModifier;
public XamlXNameResolver(NamedFieldModifier namedFieldModifier = NamedFieldModifier.Internal)
{
_defaultFieldModifier = namedFieldModifier.ToString().ToLowerInvariant();
}
public IReadOnlyList<ResolvedName> ResolveNames(XamlDocument xaml)
{
_items.Clear();
xaml.Root.Visit(this);
xaml.Root.VisitChildren(this);
return _items;
}
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node)
{
if (node is not XamlAstObjectNode objectNode)
return node;
var clrType = objectNode.Type.GetClrType();
if (!clrType.IsAvaloniaStyledElement())
return node;
foreach (var child in objectNode.Children)
{
if (child is XamlAstXamlPropertyValueNode propertyValueNode &&
propertyValueNode.Property is XamlAstNamePropertyReference namedProperty &&
namedProperty.Name == "Name" &&
propertyValueNode.Values.Count > 0 &&
propertyValueNode.Values[0] is XamlAstTextNode text)
{
var fieldModifier = TryGetFieldModifier(objectNode);
var typeName = $@"{clrType.Namespace}.{clrType.Name}";
var typeAgs = clrType.GenericArguments.Select(arg => arg.FullName).ToImmutableList();
var genericTypeName = typeAgs.Count == 0
? $"global::{typeName}"
: $@"global::{typeName}<{string.Join(", ", typeAgs.Select(arg => $"global::{arg}"))}>";
var resolvedName = new ResolvedName(genericTypeName, text.Text, fieldModifier);
if (_items.Contains(resolvedName))
continue;
_items.Add(resolvedName);
}
}
return node;
}
void IXamlAstVisitor.Push(IXamlAstNode node) { }
void IXamlAstVisitor.Pop() { }
private string TryGetFieldModifier(XamlAstObjectNode objectNode)
{
// We follow Xamarin.Forms API behavior in terms of x:FieldModifier here:
// https://docs.microsoft.com/en-us/xamarin/xamarin-forms/xaml/field-modifiers
// However, by default we use 'internal' field modifier here for generated
// x:Name references for historical purposes and WPF compatibility.
//
var fieldModifierType = objectNode
.Children
.OfType<XamlAstXmlDirective>()
.Where(dir => dir.Name == "FieldModifier" && dir.Namespace == XamlNamespaces.Xaml2006)
.Select(dir => dir.Values[0])
.OfType<XamlAstTextNode>()
.Select(txt => txt.Text)
.FirstOrDefault();
return fieldModifierType?.ToLowerInvariant() switch
{
"private" => "private",
"public" => "public",
"protected" => "protected",
"internal" => "internal",
"notpublic" => "internal",
_ => _defaultFieldModifier
};
}
}

100
src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs

@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Generators.Common.Domain;
using Avalonia.Generators.Compiler;
using XamlX;
using XamlX.Ast;
using XamlX.Parsers;
namespace Avalonia.Generators.Common;
internal class XamlXViewResolver : IViewResolver, IXamlAstVisitor
{
private readonly RoslynTypeSystem _typeSystem;
private readonly MiniCompiler _compiler;
private readonly bool _checkTypeValidity;
private readonly Action<string> _onTypeInvalid;
private readonly Action<Exception> _onUnhandledError;
private ResolvedView _resolvedClass;
private XamlDocument _xaml;
public XamlXViewResolver(
RoslynTypeSystem typeSystem,
MiniCompiler compiler,
bool checkTypeValidity = false,
Action<string> onTypeInvalid = null,
Action<Exception> onUnhandledError = null)
{
_checkTypeValidity = checkTypeValidity;
_onTypeInvalid = onTypeInvalid;
_onUnhandledError = onUnhandledError;
_typeSystem = typeSystem;
_compiler = compiler;
}
public ResolvedView ResolveView(string xaml)
{
try
{
_resolvedClass = null;
_xaml = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string>
{
{XamlNamespaces.Blend2008, XamlNamespaces.Blend2008}
});
_compiler.Transform(_xaml);
_xaml.Root.Visit(this);
_xaml.Root.VisitChildren(this);
return _resolvedClass;
}
catch (Exception exception)
{
_onUnhandledError?.Invoke(exception);
return null;
}
}
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node)
{
if (node is not XamlAstObjectNode objectNode)
return node;
var clrType = objectNode.Type.GetClrType();
if (!clrType.IsAvaloniaStyledElement())
return node;
foreach (var child in objectNode.Children)
{
if (child is XamlAstXmlDirective directive &&
directive.Name == "Class" &&
directive.Namespace == XamlNamespaces.Xaml2006 &&
directive.Values[0] is XamlAstTextNode text)
{
if (_checkTypeValidity)
{
var existingType = _typeSystem.FindType(text.Text);
if (existingType == null)
{
_onTypeInvalid?.Invoke(text.Text);
return node;
}
}
var split = text.Text.Split('.');
var nameSpace = string.Join(".", split.Take(split.Length - 1));
var className = split.Last();
_resolvedClass = new ResolvedView(className, clrType, nameSpace, _xaml);
return node;
}
}
return node;
}
void IXamlAstVisitor.Push(IXamlAstNode node) { }
void IXamlAstVisitor.Pop() { }
}

17
src/tools/Avalonia.Generators/Compiler/DataTemplateTransformer.cs

@ -0,0 +1,17 @@
using XamlX.Ast;
using XamlX.Transform;
namespace Avalonia.Generators.Compiler;
internal class DataTemplateTransformer : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (node is XamlAstObjectNode objectNode &&
objectNode.Type is XamlAstXmlTypeReference typeReference &&
(typeReference.Name == "DataTemplate" ||
typeReference.Name == "ControlTemplate"))
objectNode.Children.Clear();
return node;
}
}

50
src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using XamlX.Compiler;
using XamlX.Emit;
using XamlX.Transform;
using XamlX.Transform.Transformers;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Compiler;
internal sealed class MiniCompiler : XamlCompiler<object, IXamlEmitResult>
{
public const string AvaloniaXmlnsDefinitionAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute";
public static MiniCompiler CreateDefault(RoslynTypeSystem typeSystem, params string[] additionalTypes)
{
var mappings = new XamlLanguageTypeMappings(typeSystem);
foreach (var additionalType in additionalTypes)
mappings.XmlnsAttributes.Add(typeSystem.GetType(additionalType));
var configuration = new TransformerConfiguration(
typeSystem,
typeSystem.Assemblies.First(),
mappings);
return new MiniCompiler(configuration);
}
private MiniCompiler(TransformerConfiguration configuration)
: base(configuration, new XamlLanguageEmitMappings<object, IXamlEmitResult>(), false)
{
Transformers.Add(new NameDirectiveTransformer());
Transformers.Add(new DataTemplateTransformer());
Transformers.Add(new KnownDirectivesTransformer());
Transformers.Add(new XamlIntrinsicsTransformer());
Transformers.Add(new XArgumentsTransformer());
Transformers.Add(new TypeReferenceResolver());
}
protected override XamlEmitContext<object, IXamlEmitResult> InitCodeGen(
IFileSource file,
Func<string, IXamlType,
IXamlTypeBuilder<object>> createSubType,
Func<string, IXamlType, IEnumerable<IXamlType>,
IXamlTypeBuilder<object>> createDelegateType,
object codeGen,
XamlRuntimeContext<object, IXamlEmitResult> context,
bool needContextLocal) =>
throw new NotSupportedException();
}

28
src/tools/Avalonia.Generators/Compiler/NameDirectiveTransformer.cs

@ -0,0 +1,28 @@
using XamlX;
using XamlX.Ast;
using XamlX.Transform;
namespace Avalonia.Generators.Compiler;
internal class NameDirectiveTransformer : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (node is not XamlAstObjectNode objectNode)
return node;
for (var index = 0; index < objectNode.Children.Count; index++)
{
var child = objectNode.Children[index];
if (child is XamlAstXmlDirective directive &&
directive.Namespace == XamlNamespaces.Xaml2006 &&
directive.Name == "Name")
objectNode.Children[index] = new XamlAstXamlPropertyValueNode(
directive,
new XamlAstNamePropertyReference(directive, objectNode.Type, "Name", objectNode.Type),
directive.Values);
}
return node;
}
}

276
src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs

@ -0,0 +1,276 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Compiler;
internal class RoslynTypeSystem : IXamlTypeSystem
{
private readonly List<IXamlAssembly> _assemblies = new();
public RoslynTypeSystem(CSharpCompilation compilation)
{
_assemblies.Add(new RoslynAssembly(compilation.Assembly));
var assemblySymbols = compilation
.References
.Select(compilation.GetAssemblyOrModuleSymbol)
.OfType<IAssemblySymbol>()
.Select(assembly => new RoslynAssembly(assembly))
.ToList();
_assemblies.AddRange(assemblySymbols);
}
public IEnumerable<IXamlAssembly> Assemblies => _assemblies;
public IXamlAssembly FindAssembly(string name) =>
Assemblies
.FirstOrDefault(a => string.Equals(a.Name, name, StringComparison.OrdinalIgnoreCase));
public IXamlType FindType(string name) =>
_assemblies
.Select(assembly => assembly.FindType(name))
.FirstOrDefault(type => type != null);
public IXamlType FindType(string name, string assembly) =>
_assemblies
.Select(assemblyInstance => assemblyInstance.FindType(name))
.FirstOrDefault(type => type != null);
}
internal class RoslynAssembly : IXamlAssembly
{
private readonly IAssemblySymbol _symbol;
public RoslynAssembly(IAssemblySymbol symbol) => _symbol = symbol;
public bool Equals(IXamlAssembly other) =>
other is RoslynAssembly roslynAssembly &&
SymbolEqualityComparer.Default.Equals(_symbol, roslynAssembly._symbol);
public string Name => _symbol.Name;
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes =>
_symbol.GetAttributes()
.Select(data => new RoslynAttribute(data, this))
.ToList();
public IXamlType FindType(string fullName)
{
var type = _symbol.GetTypeByMetadataName(fullName);
return type is null ? null : new RoslynType(type, this);
}
}
internal class RoslynAttribute : IXamlCustomAttribute
{
private readonly AttributeData _data;
private readonly RoslynAssembly _assembly;
public RoslynAttribute(AttributeData data, RoslynAssembly assembly)
{
_data = data;
_assembly = assembly;
}
public bool Equals(IXamlCustomAttribute other) =>
other is RoslynAttribute attribute &&
_data == attribute._data;
public IXamlType Type => new RoslynType(_data.AttributeClass, _assembly);
public List<object> Parameters =>
_data.ConstructorArguments
.Select(argument => argument.Value)
.ToList();
public Dictionary<string, object> Properties =>
_data.NamedArguments.ToDictionary(
pair => pair.Key,
pair => pair.Value.Value);
}
internal class RoslynType : IXamlType
{
private static readonly SymbolDisplayFormat SymbolDisplayFormat = new SymbolDisplayFormat(
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters |
SymbolDisplayGenericsOptions.IncludeTypeConstraints |
SymbolDisplayGenericsOptions.IncludeVariance);
private readonly RoslynAssembly _assembly;
private readonly INamedTypeSymbol _symbol;
public RoslynType(INamedTypeSymbol symbol, RoslynAssembly assembly)
{
_symbol = symbol;
_assembly = assembly;
}
public bool Equals(IXamlType other) =>
other is RoslynType roslynType &&
SymbolEqualityComparer.Default.Equals(_symbol, roslynType._symbol);
public object Id => _symbol;
public string Name => _symbol.Name;
public string Namespace => _symbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat);
public string FullName => $"{Namespace}.{Name}";
public IXamlAssembly Assembly => _assembly;
public IReadOnlyList<IXamlProperty> Properties =>
_symbol.GetMembers()
.Where(member => member.Kind == SymbolKind.Property)
.OfType<IPropertySymbol>()
.Select(property => new RoslynProperty(property, _assembly))
.ToList();
public IReadOnlyList<IXamlEventInfo> Events { get; } = new List<IXamlEventInfo>();
public IReadOnlyList<IXamlField> Fields { get; } = new List<IXamlField>();
public IReadOnlyList<IXamlMethod> Methods { get; } = new List<IXamlMethod>();
public IReadOnlyList<IXamlConstructor> Constructors =>
_symbol.Constructors
.Select(method => new RoslynConstructor(method, _assembly))
.ToList();
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes { get; } = new List<IXamlCustomAttribute>();
public IReadOnlyList<IXamlType> GenericArguments { get; private set; } = new List<IXamlType>();
public bool IsAssignableFrom(IXamlType type) => type == this;
public IXamlType MakeGenericType(IReadOnlyList<IXamlType> typeArguments)
{
GenericArguments = typeArguments;
return this;
}
public IXamlType GenericTypeDefinition => this;
public bool IsArray => false;
public IXamlType ArrayElementType { get; } = null;
public IXamlType MakeArrayType(int dimensions) => null;
public IXamlType BaseType => _symbol.BaseType == null ? null : new RoslynType(_symbol.BaseType, _assembly);
public bool IsValueType { get; } = false;
public bool IsEnum { get; } = false;
public IReadOnlyList<IXamlType> Interfaces =>
_symbol.AllInterfaces
.Select(abstraction => new RoslynType(abstraction, _assembly))
.ToList();
public bool IsInterface => _symbol.IsAbstract;
public IXamlType GetEnumUnderlyingType() => null;
public IReadOnlyList<IXamlType> GenericParameters { get; } = new List<IXamlType>();
}
internal class RoslynConstructor : IXamlConstructor
{
private readonly IMethodSymbol _symbol;
private readonly RoslynAssembly _assembly;
public RoslynConstructor(IMethodSymbol symbol, RoslynAssembly assembly)
{
_symbol = symbol;
_assembly = assembly;
}
public bool Equals(IXamlConstructor other) =>
other is RoslynConstructor roslynConstructor &&
SymbolEqualityComparer.Default.Equals(_symbol, roslynConstructor._symbol);
public bool IsPublic => true;
public bool IsStatic => false;
public IReadOnlyList<IXamlType> Parameters =>
_symbol.Parameters
.Select(parameter => parameter.Type)
.OfType<INamedTypeSymbol>()
.Select(type => new RoslynType(type, _assembly))
.ToList();
}
internal class RoslynProperty : IXamlProperty
{
private readonly IPropertySymbol _symbol;
private readonly RoslynAssembly _assembly;
public RoslynProperty(IPropertySymbol symbol, RoslynAssembly assembly)
{
_symbol = symbol;
_assembly = assembly;
}
public bool Equals(IXamlProperty other) =>
other is RoslynProperty roslynProperty &&
SymbolEqualityComparer.Default.Equals(_symbol, roslynProperty._symbol);
public string Name => _symbol.Name;
public IXamlType PropertyType =>
_symbol.Type is INamedTypeSymbol namedTypeSymbol
? new RoslynType(namedTypeSymbol, _assembly)
: null;
public IXamlMethod Getter => _symbol.GetMethod == null ? null : new RoslynMethod(_symbol.GetMethod, _assembly);
public IXamlMethod Setter => _symbol.SetMethod == null ? null : new RoslynMethod(_symbol.SetMethod, _assembly);
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes { get; } = new List<IXamlCustomAttribute>();
public IReadOnlyList<IXamlType> IndexerParameters { get; } = new List<IXamlType>();
}
internal class RoslynMethod : IXamlMethod
{
private readonly IMethodSymbol _symbol;
private readonly RoslynAssembly _assembly;
public RoslynMethod(IMethodSymbol symbol, RoslynAssembly assembly)
{
_symbol = symbol;
_assembly = assembly;
}
public bool Equals(IXamlMethod other) =>
other is RoslynMethod roslynMethod &&
SymbolEqualityComparer.Default.Equals(roslynMethod._symbol, _symbol);
public string Name => _symbol.Name;
public bool IsPublic => true;
public bool IsStatic => false;
public IXamlType ReturnType => new RoslynType((INamedTypeSymbol) _symbol.ReturnType, _assembly);
public IReadOnlyList<IXamlType> Parameters =>
_symbol.Parameters.Select(parameter => parameter.Type)
.OfType<INamedTypeSymbol>()
.Select(type => new RoslynType(type, _assembly))
.ToList();
public IXamlType DeclaringType => new RoslynType((INamedTypeSymbol)_symbol.ReceiverType, _assembly);
public IXamlMethod MakeGenericMethod(IReadOnlyList<IXamlType> typeArguments) => null;
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes { get; } = new List<IXamlCustomAttribute>();
}

36
src/tools/Avalonia.Generators/GeneratorContextExtensions.cs

@ -0,0 +1,36 @@
using System;
using Microsoft.CodeAnalysis;
namespace Avalonia.Generators;
internal static class GeneratorContextExtensions
{
private const string UnhandledErrorDescriptorId = "AXN0002";
private const string InvalidTypeDescriptorId = "AXN0001";
public static string GetMsBuildProperty(
this GeneratorExecutionContext context,
string name,
string defaultValue = "")
{
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.{name}", out var value);
return value ?? defaultValue;
}
public static void ReportNameGeneratorUnhandledError(this GeneratorExecutionContext context, Exception error) =>
context.Report(UnhandledErrorDescriptorId,
"Unhandled exception occured while generating typed Name references. " +
"Please file an issue: https://github.com/avaloniaui/Avalonia.Generators",
error.ToString());
public static void ReportNameGeneratorInvalidType(this GeneratorExecutionContext context, string typeName) =>
context.Report(InvalidTypeDescriptorId,
$"Avalonia x:Name generator was unable to generate names for type '{typeName}'. " +
$"The type '{typeName}' does not exist in the assembly.");
private static void Report(this GeneratorExecutionContext context, string id, string title, string message = null) =>
context.ReportDiagnostic(
Diagnostic.Create(
new DiagnosticDescriptor(id, title, message ?? title, "Usage", DiagnosticSeverity.Error, true),
Location.None));
}

71
src/tools/Avalonia.Generators/GeneratorOptions.cs

@ -0,0 +1,71 @@
using System;
using Avalonia.Generators.Common.Domain;
using Avalonia.Generators.NameGenerator;
using Microsoft.CodeAnalysis;
namespace Avalonia.Generators;
// When update these enum values, don't forget to update Avalonia.Generators.props.
internal enum BuildProperties
{
AvaloniaNameGeneratorIsEnabled = 0,
AvaloniaNameGeneratorBehavior = 1,
AvaloniaNameGeneratorDefaultFieldModifier = 2,
AvaloniaNameGeneratorFilterByPath = 3,
AvaloniaNameGeneratorFilterByNamespace = 4,
AvaloniaNameGeneratorViewFileNamingStrategy = 5,
// TODO add other generators properties here.
}
internal class GeneratorOptions
{
private readonly GeneratorExecutionContext _context;
public GeneratorOptions(GeneratorExecutionContext context) => _context = context;
public bool AvaloniaNameGeneratorIsEnabled => GetBoolProperty(
BuildProperties.AvaloniaNameGeneratorIsEnabled,
true);
public Behavior AvaloniaNameGeneratorBehavior => GetEnumProperty(
BuildProperties.AvaloniaNameGeneratorBehavior,
Behavior.InitializeComponent);
public NamedFieldModifier AvaloniaNameGeneratorClassFieldModifier => GetEnumProperty(
BuildProperties.AvaloniaNameGeneratorDefaultFieldModifier,
NamedFieldModifier.Internal);
public ViewFileNamingStrategy AvaloniaNameGeneratorViewFileNamingStrategy => GetEnumProperty(
BuildProperties.AvaloniaNameGeneratorViewFileNamingStrategy,
ViewFileNamingStrategy.NamespaceAndClassName);
public string[] AvaloniaNameGeneratorFilterByPath => GetStringArrayProperty(
BuildProperties.AvaloniaNameGeneratorFilterByPath,
"*");
public string[] AvaloniaNameGeneratorFilterByNamespace => GetStringArrayProperty(
BuildProperties.AvaloniaNameGeneratorFilterByNamespace,
"*");
private string[] GetStringArrayProperty(BuildProperties name, string defaultValue)
{
var key = name.ToString();
var value = _context.GetMsBuildProperty(key, defaultValue);
return value.Contains(";") ? value.Split(';') : new[] {value};
}
private TEnum GetEnumProperty<TEnum>(BuildProperties name, TEnum defaultValue) where TEnum : struct
{
var key = name.ToString();
var value = _context.GetMsBuildProperty(key, defaultValue.ToString());
return Enum.TryParse(value, true, out TEnum behavior) ? behavior : defaultValue;
}
private bool GetBoolProperty(BuildProperties name, bool defaultValue)
{
var key = name.ToString();
var value = _context.GetMsBuildProperty(key, defaultValue.ToString());
return bool.TryParse(value, out var result) ? result : defaultValue;
}
}

63
src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Generators.Common.Domain;
using Microsoft.CodeAnalysis;
namespace Avalonia.Generators.NameGenerator;
internal class AvaloniaNameGenerator : INameGenerator
{
private readonly ViewFileNamingStrategy _naming;
private readonly IGlobPattern _pathPattern;
private readonly IGlobPattern _namespacePattern;
private readonly IViewResolver _classes;
private readonly INameResolver _names;
private readonly ICodeGenerator _code;
public AvaloniaNameGenerator(
ViewFileNamingStrategy naming,
IGlobPattern pathPattern,
IGlobPattern namespacePattern,
IViewResolver classes,
INameResolver names,
ICodeGenerator code)
{
_naming = naming;
_pathPattern = pathPattern;
_namespacePattern = namespacePattern;
_classes = classes;
_names = names;
_code = code;
}
public IReadOnlyList<GeneratedPartialClass> GenerateNameReferences(IEnumerable<AdditionalText> additionalFiles)
{
var resolveViews =
from file in additionalFiles
where (file.Path.EndsWith(".xaml") ||
file.Path.EndsWith(".paml") ||
file.Path.EndsWith(".axaml")) &&
_pathPattern.Matches(file.Path)
let xaml = file.GetText()!.ToString()
let view = _classes.ResolveView(xaml)
where view != null && _namespacePattern.Matches(view.Namespace)
select view;
var query =
from view in resolveViews
let names = _names.ResolveNames(view.Xaml)
let code = _code.GenerateCode(view.ClassName, view.Namespace, view.XamlType, names)
let fileName = ResolveViewFileName(view, _naming)
select new GeneratedPartialClass(fileName, code);
return query.ToList();
}
private static string ResolveViewFileName(ResolvedView view, ViewFileNamingStrategy strategy) => strategy switch
{
ViewFileNamingStrategy.ClassName => $"{view.ClassName}.g.cs",
ViewFileNamingStrategy.NamespaceAndClassName => $"{view.Namespace}.{view.ClassName}.g.cs",
_ => throw new ArgumentOutOfRangeException(nameof(strategy), strategy, "Unknown naming strategy!")
};
}

60
src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs

@ -0,0 +1,60 @@
using System;
using Avalonia.Generators.Common;
using Avalonia.Generators.Common.Domain;
using Avalonia.Generators.Compiler;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace Avalonia.Generators.NameGenerator;
[Generator]
public class AvaloniaNameSourceGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context) { }
public void Execute(GeneratorExecutionContext context)
{
try
{
var generator = CreateNameGenerator(context);
if (generator is null)
{
return;
}
var partials = generator.GenerateNameReferences(context.AdditionalFiles);
foreach (var (fileName, content) in partials) context.AddSource(fileName, content);
}
catch (Exception exception)
{
context.ReportNameGeneratorUnhandledError(exception);
}
}
private static INameGenerator CreateNameGenerator(GeneratorExecutionContext context)
{
var options = new GeneratorOptions(context);
if (!options.AvaloniaNameGeneratorIsEnabled)
{
return null;
}
var types = new RoslynTypeSystem((CSharpCompilation)context.Compilation);
ICodeGenerator generator = options.AvaloniaNameGeneratorBehavior switch {
Behavior.OnlyProperties => new OnlyPropertiesCodeGenerator(),
Behavior.InitializeComponent => new InitializeComponentCodeGenerator(types),
_ => throw new ArgumentOutOfRangeException()
};
var compiler = MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute);
return new AvaloniaNameGenerator(
options.AvaloniaNameGeneratorViewFileNamingStrategy,
new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByPath),
new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByNamespace),
new XamlXViewResolver(types, compiler, true,
type => context.ReportNameGeneratorInvalidType(type),
error => context.ReportNameGeneratorUnhandledError(error)),
new XamlXNameResolver(options.AvaloniaNameGeneratorClassFieldModifier),
generator);
}
}

11
src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs

@ -0,0 +1,11 @@
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
namespace Avalonia.Generators.NameGenerator;
internal interface INameGenerator
{
IReadOnlyList<GeneratedPartialClass> GenerateNameReferences(IEnumerable<AdditionalText> additionalFiles);
}
internal record GeneratedPartialClass(string FileName, string Content);

83
src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs

@ -0,0 +1,83 @@
using System.Collections.Generic;
using Avalonia.Generators.Common.Domain;
using XamlX.TypeSystem;
namespace Avalonia.Generators.NameGenerator;
internal class InitializeComponentCodeGenerator: ICodeGenerator
{
private readonly bool _diagnosticsAreConnected;
private const string AttachDevToolsCodeBlock = @"
#if DEBUG
if (attachDevTools)
{
this.AttachDevTools();
}
#endif
";
private const string AttachDevToolsParameterDocumentation
= @" /// <param name=""attachDevTools"">Should the dev tools be attached.</param>
";
public InitializeComponentCodeGenerator(IXamlTypeSystem types)
{
_diagnosticsAreConnected = types.FindAssembly("Avalonia.Diagnostics") != null;
}
public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names)
{
var properties = new List<string>();
var initializations = new List<string>();
foreach (var resolvedName in names)
{
var (typeName, name, fieldModifier) = resolvedName;
properties.Add($" {fieldModifier} {typeName} {name};");
initializations.Add($" {name} = this.FindNameScope()?.Find<{typeName}>(\"{name}\");");
}
var attachDevTools = _diagnosticsAreConnected && IsWindow(xamlType);
return $@"// <auto-generated />
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace {nameSpace}
{{
partial class {className}
{{
{string.Join("\n", properties)}
/// <summary>
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced).
/// </summary>
/// <param name=""loadXaml"">Should the XAML be loaded into the component.</param>
{(attachDevTools ? AttachDevToolsParameterDocumentation : string.Empty)}
public void InitializeComponent(bool loadXaml = true{(attachDevTools ? ", bool attachDevTools = true" : string.Empty)})
{{
if (loadXaml)
{{
AvaloniaXamlLoader.Load(this);
}}
{(attachDevTools ? AttachDevToolsCodeBlock : string.Empty)}
{string.Join("\n", initializations)}
}}
}}
}}
";
}
private static bool IsWindow(IXamlType xamlType)
{
var type = xamlType;
bool isWindow;
do
{
isWindow = type.FullName == "Avalonia.Controls.Window";
type = type.BaseType;
} while (!isWindow && type != null);
return isWindow;
}
}

31
src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs

@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Generators.Common.Domain;
using XamlX.TypeSystem;
namespace Avalonia.Generators.NameGenerator;
internal class OnlyPropertiesCodeGenerator : ICodeGenerator
{
public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names)
{
var namedControls = names
.Select(info => " " +
$"{info.FieldModifier} {info.TypeName} {info.Name} => " +
$"this.FindNameScope()?.Find<{info.TypeName}>(\"{info.Name}\");")
.ToList();
var lines = string.Join("\n", namedControls);
return $@"// <auto-generated />
using Avalonia.Controls;
namespace {nameSpace}
{{
partial class {className}
{{
{lines}
}}
}}
";
}
}

21
src/tools/Avalonia.Generators/NameGenerator/Options.cs

@ -0,0 +1,21 @@
namespace Avalonia.Generators.NameGenerator;
internal enum Options
{
Public = 0,
Private = 1,
Internal = 2,
Protected = 3,
}
internal enum Behavior
{
OnlyProperties = 0,
InitializeComponent = 1,
}
internal enum ViewFileNamingStrategy
{
ClassName = 0,
NamespaceAndClassName = 1,
}

8
src/tools/Avalonia.Generators/Properties/launchSettings.json

@ -0,0 +1,8 @@
{
"profiles": {
"Profile 1": {
"commandName": "DebugRoslynComponent",
"targetProject": "..\\..\\..\\samples\\Generators.Sandbox\\Generators.Sandbox.csproj"
}
}
}

209
src/tools/Avalonia.Generators/README.md

@ -0,0 +1,209 @@
[![NuGet Stats](https://img.shields.io/nuget/v/XamlNameReferenceGenerator.svg)](https://www.nuget.org/packages/XamlNameReferenceGenerator) [![downloads](https://img.shields.io/nuget/dt/XamlNameReferenceGenerator)](https://www.nuget.org/packages/XamlNameReferenceGenerator) ![Build](https://github.com/avaloniaui/Avalonia.NameGenerator/workflows/Build/badge.svg) ![License](https://img.shields.io/github/license/avaloniaui/Avalonia.NameGenerator.svg) ![Size](https://img.shields.io/github/repo-size/avaloniaui/Avalonia.NameGenerator.svg)
### C# `SourceGenerator` for Typed Avalonia `x:Name` References
This is a [C# `SourceGenerator`](https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/) built for generating strongly-typed references to controls with `x:Name` (or just `Name`) attributes declared in XAML (or, in `.axaml`). The source generator will look for the `xaml` (or `axaml`) file with the same name as your partial C# class that is a subclass of `Avalonia.INamed` and parses the XAML markup, finds all XAML tags with `x:Name` attributes and generates the C# code.
### Getting Started
In order to get started, just install the NuGet package:
```
dotnet add package XamlNameReferenceGenerator
```
Or, if you are using [submodules](https://git-scm.com/docs/git-submodule), you can reference the generator as such:
```xml
<ItemGroup>
<!-- Remember to ensure XAML files are included via <AdditionalFiles>,
otherwise C# source generator won't see XAML files. -->
<AdditionalFiles Include="**\*.xaml"/>
<ProjectReference Include="..\Avalonia.NameGenerator\Avalonia.NameGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
```
### Usage
After installing the NuGet package, declare your view class as `partial`. Typed C# references to Avalonia controls declared in XAML files will be generated for classes referenced by the `x:Class` directive in XAML files. For example, for the following XAML markup:
```xml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.SignUpView">
<TextBox x:Name="UserNameTextBox" x:FieldModifier="public" />
</Window>
```
A new C# partial class named `SignUpView` with a single `public` property named `UserNameTextBox` of type `TextBox` will be generated in the `Sample.App` namespace. We won't see the generated file, but we'll be able to access the generated property as shown below:
```cs
using Avalonia.Controls;
namespace Sample.App
{
public partial class SignUpView : Window
{
public SignUpView()
{
// This method is generated. Call it before accessing any
// of the generated properties. The 'UserNameTextBox'
// property is also generated.
InitializeComponent();
UserNameTextBox.Text = "Joseph";
}
}
}
```
<img src="https://hsto.org/getpro/habr/post_images/d9f/4aa/a1e/d9f4aaa1eb450f5dd2fca66631bc16a0.gif" />
### Why do I need this?
The typed `x:Name` references might be useful if you decide to use e.g. [ReactiveUI code-behind bindings](https://www.reactiveui.net/docs/handbook/data-binding/):
```cs
// UserNameValidation and PasswordValidation are auto generated.
public partial class SignUpView : ReactiveWindow<SignUpViewModel>
{
public SignUpView()
{
InitializeComponent();
this.WhenActivated(disposables =>
{
this.BindValidation(ViewModel, x => x.UserName, x => x.UserNameValidation.Text)
.DisposeWith(disposables);
this.BindValidation(ViewModel, x => x.Password, x => x.PasswordValidation.Text)
.DisposeWith(disposables);
});
}
}
```
### Advanced Usage
> Never keep a method named `InitializeComponent` in your code-behind view class if you are using the generator with `AvaloniaNameGeneratorBehavior` set to `InitializeComponent` (this is the default value). The private `InitializeComponent` method declared in your code-behind class hides the `InitializeComponent` method generated by `Avalonia.NameGenerator`, see [Issue 69](https://github.com/AvaloniaUI/Avalonia.NameGenerator/issues/69). If you wish to use your own `InitializeComponent` method (not the generated one), set `AvaloniaNameGeneratorBehavior` to `OnlyProperties`.
The `x:Name` generator can be configured via MsBuild properties that you can put into your C# project file (`.csproj`). Using such options, you can configure the generator behavior, the default field modifier, namespace and path filters. The generator supports the following options:
- `AvaloniaNameGeneratorBehavior`
Possible values: `OnlyProperties`, `InitializeComponent`
Default value: `InitializeComponent`
Determines if the generator should generate get-only properties, or the `InitializeComponent` method.
- `AvaloniaNameGeneratorDefaultFieldModifier`
Possible values: `internal`, `public`, `private`, `protected`
Default value: `internal`
The default field modifier that should be used when there is no `x:FieldModifier` directive specified.
- `AvaloniaNameGeneratorFilterByPath`
Posssible format: `glob_pattern`, `glob_pattern;glob_pattern`
Default value: `*`
The generator will process only XAML files with paths matching the specified glob pattern(s).
Example: `*/Views/*View.xaml`, `*View.axaml;*Control.axaml`
- `AvaloniaNameGeneratorFilterByNamespace`
Posssible format: `glob_pattern`, `glob_pattern;glob_pattern`
Default value: `*`
The generator will process only XAML files with base classes' namespaces matching the specified glob pattern(s).
Example: `MyApp.Presentation.*`, `MyApp.Presentation.Views;MyApp.Presentation.Controls`
- `AvaloniaNameGeneratorViewFileNamingStrategy`
Possible values: `ClassName`, `NamespaceAndClassName`
Default value: `NamespaceAndClassName`
Determines how the automatically generated view files should be [named](https://github.com/AvaloniaUI/Avalonia.NameGenerator/issues/92).
The default values are given by:
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AvaloniaNameGeneratorBehavior>InitializeComponent</AvaloniaNameGeneratorBehavior>
<AvaloniaNameGeneratorDefaultFieldModifier>internal</AvaloniaNameGeneratorDefaultFieldModifier>
<AvaloniaNameGeneratorFilterByPath>*</AvaloniaNameGeneratorFilterByPath>
<AvaloniaNameGeneratorFilterByNamespace>*</AvaloniaNameGeneratorFilterByNamespace>
<AvaloniaNameGeneratorViewFileNamingStrategy>NamespaceAndClassName</AvaloniaNameGeneratorViewFileNamingStrategy>
</PropertyGroup>
<!-- ... -->
</Project>
```
![](https://user-images.githubusercontent.com/6759207/107812261-7ddfea00-6d80-11eb-9c7e-67bf95d0f0d4.gif)
### What do the generated sources look like?
For [`SignUpView`](https://github.com/avaloniaui/Avalonia.NameGenerator/blob/main/src/Avalonia.NameGenerator.Sandbox/Views/SignUpView.xaml), we get the following generated output when the source generator is in the `InitializeComponent` mode:
```cs
// <auto-generated />
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox UserNameTextBox;
public global::Avalonia.Controls.TextBlock UserNameValidation;
private global::Avalonia.Controls.TextBox PasswordTextBox;
internal global::Avalonia.Controls.TextBlock PasswordValidation;
internal global::Avalonia.Controls.ListBox AwesomeListView;
internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox;
internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation;
internal global::Avalonia.Controls.Button SignUpButton;
internal global::Avalonia.Controls.TextBlock CompoundValidation;
public void InitializeComponent(bool loadXaml = true, bool attachDevTools = true)
{
if (loadXaml)
{
AvaloniaXamlLoader.Load(this);
}
// This will be added only if you install Avalonia.Diagnostics.
#if DEBUG
if (attachDevTools)
{
this.AttachDevTools();
}
#endif
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox>("UserNameTextBox");
UserNameValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("UserNameValidation");
PasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox");
PasswordValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("PasswordValidation");
AwesomeListView = this.FindNameScope()?.Find<global::Avalonia.Controls.ListBox>("AwesomeListView");
ConfirmPasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox");
ConfirmPasswordValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("ConfirmPasswordValidation");
SignUpButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton");
CompoundValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("CompoundValidation");
}
}
}
```
If you enable the `OnlyProperties` source generator mode, you get:
```cs
// <auto-generated />
using Avalonia.Controls;
namespace Avalonia.NameGenerator.Sandbox.Views
{
partial class SignUpView
{
internal global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox>("UserNameTextBox");
public global::Avalonia.Controls.TextBlock UserNameValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("UserNameValidation");
private global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox");
internal global::Avalonia.Controls.TextBlock PasswordValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("PasswordValidation");
internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox");
internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("ConfirmPasswordValidation");
internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton");
internal global::Avalonia.Controls.TextBlock CompoundValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("CompoundValidation");
}
}
```

26
tests/Avalonia.Generators.Tests/Avalonia.Generators.Tests.csproj

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>Avalonia.Generators.Tests</RootNamespace>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
<ProjectReference Include="..\..\src\tools\Avalonia.Generators\Avalonia.Generators.csproj" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Views\*.xml" />
<EmbeddedResource Include="OnlyProperties\GeneratedCode\*.txt" />
<EmbeddedResource Include="InitializeComponent\GeneratedInitializeComponent\*.txt" />
<EmbeddedResource Include="InitializeComponent\GeneratedDevTools\*.txt" />
</ItemGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\XUnit.props" />
<Import Project="..\..\build\SharedVersion.props" />
</Project>

31
tests/Avalonia.Generators.Tests/GlobPatternTests.cs

@ -0,0 +1,31 @@
using Avalonia.Generators.Common;
using Xunit;
namespace Avalonia.Generators.Tests;
public class GlobPatternTests
{
[Theory]
[InlineData("*", "anything", true)]
[InlineData("", "anything", false)]
[InlineData("Views/*", "Views/SignUpView.xaml", true)]
[InlineData("Views/*", "Extensions/SignUpView.xaml", false)]
[InlineData("*SignUpView*", "Extensions/SignUpView.xaml", true)]
[InlineData("*SignUpView.paml", "Extensions/SignUpView.xaml", false)]
[InlineData("*.xaml", "Extensions/SignUpView.xaml", true)]
public void Should_Match_Glob_Expressions(string pattern, string value, bool matches)
{
Assert.Equal(matches, new GlobPattern(pattern).Matches(value));
}
[Theory]
[InlineData("Views/SignUpView.xaml", true, new[] { "*.xaml", "Extensions/*" })]
[InlineData("Extensions/SignUpView.paml", true, new[] { "*.xaml", "Extensions/*" })]
[InlineData("Extensions/SignUpView.paml", false, new[] { "*.xaml", "Views/*" })]
[InlineData("anything", true, new[] { "*", "*" })]
[InlineData("anything", false, new[] { "", "" })]
public void Should_Match_Glob_Pattern_Groups(string value, bool matches, string[] patterns)
{
Assert.Equal(matches, new GlobPatternGroup(patterns).Matches(value));
}
}

28
tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/AttachedProps.txt

@ -0,0 +1,28 @@
// <auto-generated />
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.Controls.TextBox UserNameTextBox;
/// <summary>
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced).
/// </summary>
/// <param name="loadXaml">Should the XAML be loaded into the component.</param>
public void InitializeComponent(bool loadXaml = true)
{
if (loadXaml)
{
AvaloniaXamlLoader.Load(this);
}
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox");
}
}
}

36
tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/AttachedPropsWithDevTools.txt

@ -0,0 +1,36 @@
// <auto-generated />
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.Controls.TextBox UserNameTextBox;
/// <summary>
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced).
/// </summary>
/// <param name="loadXaml">Should the XAML be loaded into the component.</param>
/// <param name="attachDevTools">Should the dev tools be attached.</param>
public void InitializeComponent(bool loadXaml = true, bool attachDevTools = true)
{
if (loadXaml)
{
AvaloniaXamlLoader.Load(this);
}
#if DEBUG
if (attachDevTools)
{
this.AttachDevTools();
}
#endif
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox");
}
}
}

28
tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/ControlWithoutWindow.txt

@ -0,0 +1,28 @@
// <auto-generated />
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.Controls.TextBox UserNameTextBox;
/// <summary>
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced).
/// </summary>
/// <param name="loadXaml">Should the XAML be loaded into the component.</param>
public void InitializeComponent(bool loadXaml = true)
{
if (loadXaml)
{
AvaloniaXamlLoader.Load(this);
}
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox");
}
}
}

32
tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/CustomControls.txt

@ -0,0 +1,32 @@
// <auto-generated />
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.ReactiveUI.RoutedViewHost ClrNamespaceRoutedViewHost;
internal global::Avalonia.ReactiveUI.RoutedViewHost UriRoutedViewHost;
internal global::Controls.CustomTextBox UserNameTextBox;
/// <summary>
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced).
/// </summary>
/// <param name="loadXaml">Should the XAML be loaded into the component.</param>
public void InitializeComponent(bool loadXaml = true)
{
if (loadXaml)
{
AvaloniaXamlLoader.Load(this);
}
ClrNamespaceRoutedViewHost = this.FindNameScope()?.Find<global::Avalonia.ReactiveUI.RoutedViewHost>("ClrNamespaceRoutedViewHost");
UriRoutedViewHost = this.FindNameScope()?.Find<global::Avalonia.ReactiveUI.RoutedViewHost>("UriRoutedViewHost");
UserNameTextBox = this.FindNameScope()?.Find<global::Controls.CustomTextBox>("UserNameTextBox");
}
}
}

30
tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/DataTemplates.txt

@ -0,0 +1,30 @@
// <auto-generated />
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.Controls.TextBox UserNameTextBox;
internal global::Avalonia.Controls.ListBox NamedListBox;
/// <summary>
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced).
/// </summary>
/// <param name="loadXaml">Should the XAML be loaded into the component.</param>
public void InitializeComponent(bool loadXaml = true)
{
if (loadXaml)
{
AvaloniaXamlLoader.Load(this);
}
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox");
NamedListBox = this.FindNameScope()?.Find<global::Avalonia.Controls.ListBox>("NamedListBox");
}
}
}

38
tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/FieldModifier.txt

@ -0,0 +1,38 @@
// <auto-generated />
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Sample.App
{
partial class SampleView
{
public global::Avalonia.Controls.TextBox FirstNameTextBox;
public global::Avalonia.Controls.TextBox LastNameTextBox;
protected global::Avalonia.Controls.TextBox PasswordTextBox;
private global::Avalonia.Controls.TextBox ConfirmPasswordTextBox;
internal global::Avalonia.Controls.Button SignUpButton;
internal global::Avalonia.Controls.Button RegisterButton;
/// <summary>
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced).
/// </summary>
/// <param name="loadXaml">Should the XAML be loaded into the component.</param>
public void InitializeComponent(bool loadXaml = true)
{
if (loadXaml)
{
AvaloniaXamlLoader.Load(this);
}
FirstNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("FirstNameTextBox");
LastNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("LastNameTextBox");
PasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox");
ConfirmPasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox");
SignUpButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton");
RegisterButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("RegisterButton");
}
}
}

35
tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/InitializeComponentCode.cs

@ -0,0 +1,35 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace Avalonia.Generators.Tests.InitializeComponent.GeneratedInitializeComponent;
public static class InitializeComponentCode
{
public const string NamedControl = "NamedControl.txt";
public const string NamedControls = "NamedControls.txt";
public const string XNamedControl = "xNamedControl.txt";
public const string XNamedControls = "xNamedControls.txt";
public const string NoNamedControls = "NoNamedControls.txt";
public const string CustomControls = "CustomControls.txt";
public const string DataTemplates = "DataTemplates.txt";
public const string SignUpView = "SignUpView.txt";
public const string FieldModifier = "FieldModifier.txt";
public const string AttachedProps = "AttachedProps.txt";
public const string AttachedPropsWithDevTools = "AttachedPropsWithDevTools.txt";
public const string ControlWithoutWindow = "ControlWithoutWindow.txt";
public static async Task<string> Load(string generatedCodeResourceName)
{
var assembly = typeof(XamlXNameResolverTests).Assembly;
var fullResourceName = assembly
.GetManifestResourceNames()
.First(name => name.Contains("InitializeComponent") &&
name.Contains("GeneratedInitializeComponent") &&
name.EndsWith(generatedCodeResourceName));
await using var stream = assembly.GetManifestResourceStream(fullResourceName);
using var reader = new StreamReader(stream!);
return await reader.ReadToEndAsync();
}
}

28
tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NamedControl.txt

@ -0,0 +1,28 @@
// <auto-generated />
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.Controls.TextBox UserNameTextBox;
/// <summary>
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced).
/// </summary>
/// <param name="loadXaml">Should the XAML be loaded into the component.</param>
public void InitializeComponent(bool loadXaml = true)
{
if (loadXaml)
{
AvaloniaXamlLoader.Load(this);
}
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox");
}
}
}

32
tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NamedControls.txt

@ -0,0 +1,32 @@
// <auto-generated />
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.Controls.TextBox UserNameTextBox;
internal global::Avalonia.Controls.TextBox PasswordTextBox;
internal global::Avalonia.Controls.Button SignUpButton;
/// <summary>
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced).
/// </summary>
/// <param name="loadXaml">Should the XAML be loaded into the component.</param>
public void InitializeComponent(bool loadXaml = true)
{
if (loadXaml)
{
AvaloniaXamlLoader.Load(this);
}
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox");
PasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox");
SignUpButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton");
}
}
}

28
tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NoNamedControls.txt

@ -0,0 +1,28 @@
// <auto-generated />
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Sample.App
{
partial class SampleView
{
/// <summary>
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced).
/// </summary>
/// <param name="loadXaml">Should the XAML be loaded into the component.</param>
public void InitializeComponent(bool loadXaml = true)
{
if (loadXaml)
{
AvaloniaXamlLoader.Load(this);
}
}
}
}

46
tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/SignUpView.txt

@ -0,0 +1,46 @@
// <auto-generated />
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Sample.App
{
partial class SampleView
{
internal global::Controls.CustomTextBox UserNameTextBox;
internal global::Avalonia.Controls.TextBlock UserNameValidation;
internal global::Avalonia.Controls.TextBox PasswordTextBox;
internal global::Avalonia.Controls.TextBlock PasswordValidation;
internal global::Avalonia.Controls.ListBox AwesomeListView;
internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox;
internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation;
internal global::Avalonia.Controls.Documents.Run SignUpButtonDescription;
internal global::Avalonia.Controls.Button SignUpButton;
internal global::Avalonia.Controls.TextBlock CompoundValidation;
/// <summary>
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced).
/// </summary>
/// <param name="loadXaml">Should the XAML be loaded into the component.</param>
public void InitializeComponent(bool loadXaml = true)
{
if (loadXaml)
{
AvaloniaXamlLoader.Load(this);
}
UserNameTextBox = this.FindNameScope()?.Find<global::Controls.CustomTextBox>("UserNameTextBox");
UserNameValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("UserNameValidation");
PasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox");
PasswordValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("PasswordValidation");
AwesomeListView = this.FindNameScope()?.Find<global::Avalonia.Controls.ListBox>("AwesomeListView");
ConfirmPasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox");
ConfirmPasswordValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("ConfirmPasswordValidation");
SignUpButtonDescription = this.FindNameScope()?.Find<global::Avalonia.Controls.Documents.Run>("SignUpButtonDescription");
SignUpButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton");
CompoundValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("CompoundValidation");
}
}
}

28
tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/xNamedControl.txt

@ -0,0 +1,28 @@
// <auto-generated />
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.Controls.TextBox UserNameTextBox;
/// <summary>
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced).
/// </summary>
/// <param name="loadXaml">Should the XAML be loaded into the component.</param>
public void InitializeComponent(bool loadXaml = true)
{
if (loadXaml)
{
AvaloniaXamlLoader.Load(this);
}
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox");
}
}
}

32
tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/xNamedControls.txt

@ -0,0 +1,32 @@
// <auto-generated />
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.Controls.TextBox UserNameTextBox;
internal global::Avalonia.Controls.TextBox PasswordTextBox;
internal global::Avalonia.Controls.Button SignUpButton;
/// <summary>
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced).
/// </summary>
/// <param name="loadXaml">Should the XAML be loaded into the component.</param>
public void InitializeComponent(bool loadXaml = true)
{
if (loadXaml)
{
AvaloniaXamlLoader.Load(this);
}
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox");
PasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox");
SignUpButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton");
}
}
}

63
tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs

@ -0,0 +1,63 @@
using System.Threading.Tasks;
using Avalonia.Generators.Common;
using Avalonia.Generators.Compiler;
using Avalonia.Generators.NameGenerator;
using Avalonia.Generators.Tests.InitializeComponent.GeneratedInitializeComponent;
using Avalonia.Generators.Tests.OnlyProperties.GeneratedCode;
using Avalonia.Generators.Tests.Views;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
namespace Avalonia.Generators.Tests.InitializeComponent;
public class InitializeComponentTests
{
[Theory]
[InlineData(InitializeComponentCode.NamedControl, View.NamedControl, false)]
[InlineData(InitializeComponentCode.NamedControls, View.NamedControls, false)]
[InlineData(InitializeComponentCode.XNamedControl, View.XNamedControl, false)]
[InlineData(InitializeComponentCode.XNamedControls, View.XNamedControls, false)]
[InlineData(InitializeComponentCode.NoNamedControls, View.NoNamedControls, false)]
[InlineData(InitializeComponentCode.CustomControls, View.CustomControls, false)]
[InlineData(InitializeComponentCode.DataTemplates, View.DataTemplates, false)]
[InlineData(InitializeComponentCode.SignUpView, View.SignUpView, false)]
[InlineData(InitializeComponentCode.FieldModifier, View.FieldModifier, false)]
[InlineData(InitializeComponentCode.AttachedPropsWithDevTools, View.AttachedProps, true)]
[InlineData(InitializeComponentCode.AttachedProps, View.AttachedProps, false)]
[InlineData(InitializeComponentCode.ControlWithoutWindow, View.ControlWithoutWindow, true)]
[InlineData(InitializeComponentCode.ControlWithoutWindow, View.ControlWithoutWindow, false)]
public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File(
string expectation,
string markup,
bool devToolsMode)
{
var excluded = devToolsMode ? null : "Avalonia.Diagnostics";
var compilation =
View.CreateAvaloniaCompilation(excluded)
.WithCustomTextBox();
var types = new RoslynTypeSystem(compilation);
var classResolver = new XamlXViewResolver(
types,
MiniCompiler.CreateDefault(
new RoslynTypeSystem(compilation),
MiniCompiler.AvaloniaXmlnsDefinitionAttribute));
var xaml = await View.Load(markup);
var classInfo = classResolver.ResolveView(xaml);
var nameResolver = new XamlXNameResolver();
var names = nameResolver.ResolveNames(classInfo.Xaml);
var generator = new InitializeComponentCodeGenerator(types);
var code = generator
.GenerateCode("SampleView", "Sample.App", classInfo.XamlType, names)
.Replace("\r", string.Empty);
var expected = await InitializeComponentCode.Load(expectation);
CSharpSyntaxTree.ParseText(code);
Assert.Equal(expected.Replace("\r", string.Empty), code);
}
}

59
tests/Avalonia.Generators.Tests/MiniCompilerTests.cs

@ -0,0 +1,59 @@
using System;
using System.ComponentModel;
using Avalonia.Generators.Compiler;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Avalonia.Generators.Tests.Views;
using XamlX;
using XamlX.Parsers;
using Xunit;
namespace Avalonia.Generators.Tests;
public class MiniCompilerTests
{
private const string AvaloniaXaml = "<TextBlock xmlns='clr-namespace:Avalonia.Controls;assembly=Avalonia' />";
private const string MiniClass = "namespace Example { public class Valid { public int Foo() => 21; } }";
private const string MiniInvalidXaml = "<Invalid xmlns='clr-namespace:Example;assembly=Example' />";
private const string MiniValidXaml = "<Valid xmlns='clr-namespace:Example;assembly=Example' />";
[Fact]
public void Should_Resolve_Types_From_Simple_Valid_Xaml_Markup()
{
var xaml = XDocumentXamlParser.Parse(MiniValidXaml);
var compilation = CreateBasicCompilation(MiniClass);
MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)).Transform(xaml);
Assert.NotNull(xaml.Root);
}
[Fact]
public void Should_Throw_When_Unable_To_Resolve_Types_From_Simple_Invalid_Markup()
{
var xaml = XDocumentXamlParser.Parse(MiniInvalidXaml);
var compilation = CreateBasicCompilation(MiniClass);
var compiler = MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation));
Assert.Throws<XamlParseException>(() => compiler.Transform(xaml));
}
[Fact]
public void Should_Resolve_Types_From_Simple_Avalonia_Markup()
{
var xaml = XDocumentXamlParser.Parse(AvaloniaXaml);
var compilation = View.CreateAvaloniaCompilation();
MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)).Transform(xaml);
Assert.NotNull(xaml.Root);
}
private static CSharpCompilation CreateBasicCompilation(string source) =>
CSharpCompilation
.Create("BasicLib", options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.AddReferences(MetadataReference.CreateFromFile(typeof(string).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(IServiceProvider).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(ITypeDescriptorContext).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(ISupportInitialize).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(TypeConverterAttribute).Assembly.Location))
.AddSyntaxTrees(CSharpSyntaxTree.ParseText(source));
}

11
tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/AttachedProps.txt

@ -0,0 +1,11 @@
// <auto-generated />
using Avalonia.Controls;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox");
}
}

11
tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/ControlWithoutWindow.txt

@ -0,0 +1,11 @@
// <auto-generated />
using Avalonia.Controls;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox");
}
}

13
tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/CustomControls.txt

@ -0,0 +1,13 @@
// <auto-generated />
using Avalonia.Controls;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.ReactiveUI.RoutedViewHost ClrNamespaceRoutedViewHost => this.FindNameScope()?.Find<global::Avalonia.ReactiveUI.RoutedViewHost>("ClrNamespaceRoutedViewHost");
internal global::Avalonia.ReactiveUI.RoutedViewHost UriRoutedViewHost => this.FindNameScope()?.Find<global::Avalonia.ReactiveUI.RoutedViewHost>("UriRoutedViewHost");
internal global::Controls.CustomTextBox UserNameTextBox => this.FindNameScope()?.Find<global::Controls.CustomTextBox>("UserNameTextBox");
}
}

12
tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/DataTemplates.txt

@ -0,0 +1,12 @@
// <auto-generated />
using Avalonia.Controls;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox");
internal global::Avalonia.Controls.ListBox NamedListBox => this.FindNameScope()?.Find<global::Avalonia.Controls.ListBox>("NamedListBox");
}
}

16
tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/FieldModifier.txt

@ -0,0 +1,16 @@
// <auto-generated />
using Avalonia.Controls;
namespace Sample.App
{
partial class SampleView
{
public global::Avalonia.Controls.TextBox FirstNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("FirstNameTextBox");
public global::Avalonia.Controls.TextBox LastNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("LastNameTextBox");
protected global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox");
private global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox");
internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton");
internal global::Avalonia.Controls.Button RegisterButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("RegisterButton");
}
}

11
tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NamedControl.txt

@ -0,0 +1,11 @@
// <auto-generated />
using Avalonia.Controls;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox");
}
}

13
tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NamedControls.txt

@ -0,0 +1,13 @@
// <auto-generated />
using Avalonia.Controls;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox");
internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox");
internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton");
}
}

11
tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NoNamedControls.txt

@ -0,0 +1,11 @@
// <auto-generated />
using Avalonia.Controls;
namespace Sample.App
{
partial class SampleView
{
}
}

33
tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/OnlyPropertiesCode.cs

@ -0,0 +1,33 @@
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace Avalonia.Generators.Tests.OnlyProperties.GeneratedCode;
public static class OnlyPropertiesCode
{
public const string NamedControl = "NamedControl.txt";
public const string NamedControls = "NamedControls.txt";
public const string XNamedControl = "xNamedControl.txt";
public const string XNamedControls = "xNamedControls.txt";
public const string NoNamedControls = "NoNamedControls.txt";
public const string CustomControls = "CustomControls.txt";
public const string DataTemplates = "DataTemplates.txt";
public const string SignUpView = "SignUpView.txt";
public const string AttachedProps = "AttachedProps.txt";
public const string FieldModifier = "FieldModifier.txt";
public const string ControlWithoutWindow = "ControlWithoutWindow.txt";
public static async Task<string> Load(string generatedCodeResourceName)
{
var assembly = typeof(XamlXNameResolverTests).Assembly;
var fullResourceName = assembly
.GetManifestResourceNames()
.First(name => name.Contains("OnlyProperties") && name.EndsWith(generatedCodeResourceName));
await using var stream = assembly.GetManifestResourceStream(fullResourceName);
using var reader = new StreamReader(stream!);
return await reader.ReadToEndAsync();
}
}

20
tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/SignUpView.txt

@ -0,0 +1,20 @@
// <auto-generated />
using Avalonia.Controls;
namespace Sample.App
{
partial class SampleView
{
internal global::Controls.CustomTextBox UserNameTextBox => this.FindNameScope()?.Find<global::Controls.CustomTextBox>("UserNameTextBox");
internal global::Avalonia.Controls.TextBlock UserNameValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("UserNameValidation");
internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox");
internal global::Avalonia.Controls.TextBlock PasswordValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("PasswordValidation");
internal global::Avalonia.Controls.ListBox AwesomeListView => this.FindNameScope()?.Find<global::Avalonia.Controls.ListBox>("AwesomeListView");
internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox");
internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("ConfirmPasswordValidation");
internal global::Avalonia.Controls.Documents.Run SignUpButtonDescription => this.FindNameScope()?.Find<global::Avalonia.Controls.Documents.Run>("SignUpButtonDescription");
internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton");
internal global::Avalonia.Controls.TextBlock CompoundValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("CompoundValidation");
}
}

11
tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/xNamedControl.txt

@ -0,0 +1,11 @@
// <auto-generated />
using Avalonia.Controls;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox");
}
}

13
tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/xNamedControls.txt

@ -0,0 +1,13 @@
// <auto-generated />
using Avalonia.Controls;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox");
internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox");
internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton");
}
}

52
tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs

@ -0,0 +1,52 @@
using System.Threading.Tasks;
using Avalonia.Generators.Common;
using Avalonia.Generators.Compiler;
using Avalonia.Generators.NameGenerator;
using Avalonia.Generators.Tests.OnlyProperties.GeneratedCode;
using Avalonia.Generators.Tests.Views;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
namespace Avalonia.Generators.Tests.OnlyProperties;
public class OnlyPropertiesTests
{
[Theory]
[InlineData(OnlyPropertiesCode.NamedControl, View.NamedControl)]
[InlineData(OnlyPropertiesCode.NamedControls, View.NamedControls)]
[InlineData(OnlyPropertiesCode.XNamedControl, View.XNamedControl)]
[InlineData(OnlyPropertiesCode.XNamedControls, View.XNamedControls)]
[InlineData(OnlyPropertiesCode.NoNamedControls, View.NoNamedControls)]
[InlineData(OnlyPropertiesCode.CustomControls, View.CustomControls)]
[InlineData(OnlyPropertiesCode.DataTemplates, View.DataTemplates)]
[InlineData(OnlyPropertiesCode.SignUpView, View.SignUpView)]
[InlineData(OnlyPropertiesCode.AttachedProps, View.AttachedProps)]
[InlineData(OnlyPropertiesCode.FieldModifier, View.FieldModifier)]
[InlineData(OnlyPropertiesCode.ControlWithoutWindow, View.ControlWithoutWindow)]
public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File(string expectation, string markup)
{
var compilation =
View.CreateAvaloniaCompilation()
.WithCustomTextBox();
var classResolver = new XamlXViewResolver(
new RoslynTypeSystem(compilation),
MiniCompiler.CreateDefault(
new RoslynTypeSystem(compilation),
MiniCompiler.AvaloniaXmlnsDefinitionAttribute));
var xaml = await View.Load(markup);
var classInfo = classResolver.ResolveView(xaml);
var nameResolver = new XamlXNameResolver();
var names = nameResolver.ResolveNames(classInfo.Xaml);
var generator = new OnlyPropertiesCodeGenerator();
var code = generator
.GenerateCode("SampleView", "Sample.App", classInfo.XamlType, names)
.Replace("\r", string.Empty);
var expected = await OnlyPropertiesCode.Load(expectation);
CSharpSyntaxTree.ParseText(code);
Assert.Equal(expected.Replace("\r", string.Empty), code);
}
}

10
tests/Avalonia.Generators.Tests/Views/AttachedProps.xml

@ -0,0 +1,10 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:Avalonia.ReactiveUI;assembly=Avalonia.ReactiveUI"
xmlns:rxui="http://reactiveui.net"
x:Class="Sample.App.AttachedProps"
Design.Width="300">
<TextBox Name="UserNameTextBox"
Watermark="Username input"
UseFloatingWatermark="True" />
</Window>

10
tests/Avalonia.Generators.Tests/Views/ControlWithoutWindow.xml

@ -0,0 +1,10 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:Avalonia.ReactiveUI;assembly=Avalonia.ReactiveUI"
xmlns:rxui="http://reactiveui.net"
x:Class="Sample.App.ControlWithoutWindow"
Design.Width="300">
<TextBox Name="UserNameTextBox"
Watermark="Username input"
UseFloatingWatermark="True" />
</UserControl>

11
tests/Avalonia.Generators.Tests/Views/CustomControls.xml

@ -0,0 +1,11 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:Avalonia.ReactiveUI;assembly=Avalonia.ReactiveUI"
xmlns:controls="clr-namespace:Controls"
x:Class="Sample.App.CustomControls"
xmlns:rxui="http://reactiveui.net">
<custom:RoutedViewHost Name="ClrNamespaceRoutedViewHost" />
<rxui:RoutedViewHost Name="UriRoutedViewHost" />
<controls:CustomTextBox Name="UserNameTextBox" />
<controls:EvilControl Name="EvilName" />
</Window>

18
tests/Avalonia.Generators.Tests/Views/DataTemplates.xml

@ -0,0 +1,18 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.DataTemplates">
<StackPanel>
<TextBox x:Name="UserNameTextBox"
Watermark="Username input"
UseFloatingWatermark="True" />
<ListBox Name="NamedListBox">
<ListBox.ItemTemplate>
<DataTemplate x:Name="NamedDataTemplate">
<TextBox x:Name="TemplatedTextBox"
Watermark="Templated input"
UseFloatingWatermark="True" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Window>

28
tests/Avalonia.Generators.Tests/Views/FieldModifier.xml

@ -0,0 +1,28 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.FieldModifier">
<StackPanel>
<TextBox Name="FirstNameTextBox"
x:FieldModifier="Public"
Watermark="Username input"
UseFloatingWatermark="True" />
<TextBox Name="LastNameTextBox"
x:FieldModifier="public"
Watermark="Username input"
UseFloatingWatermark="True" />
<TextBox Name="PasswordTextBox"
x:FieldModifier="protected"
Watermark="Password input"
UseFloatingWatermark="True" />
<TextBox Name="ConfirmPasswordTextBox"
x:FieldModifier="private"
Watermark="Password input"
UseFloatingWatermark="True" />
<Button Name="SignUpButton"
x:FieldModifier="NotPublic"
Content="Sign up" />
<Button Name="RegisterButton"
x:FieldModifier="Nonsense"
Content="Register" />
</StackPanel>
</Window>

7
tests/Avalonia.Generators.Tests/Views/NamedControl.xml

@ -0,0 +1,7 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.NamedControl">
<TextBox Name="UserNameTextBox"
Watermark="Username input"
UseFloatingWatermark="True" />
</Window>

14
tests/Avalonia.Generators.Tests/Views/NamedControls.xml

@ -0,0 +1,14 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.NamedControls">
<StackPanel>
<TextBox Name="UserNameTextBox"
Watermark="Username input"
UseFloatingWatermark="True" />
<TextBox Name="PasswordTextBox"
Watermark="Password input"
UseFloatingWatermark="True" />
<Button Name="SignUpButton"
Content="Sign up" />
</StackPanel>
</Window>

6
tests/Avalonia.Generators.Tests/Views/NoNamedControls.xml

@ -0,0 +1,6 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.NoNamedControls">
<TextBox Watermark="Username input"
UseFloatingWatermark="True" />
</Window>

52
tests/Avalonia.Generators.Tests/Views/SignUpView.xml

@ -0,0 +1,52 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Controls"
x:Class="Sample.App.SignUpView">
<StackPanel>
<controls:CustomTextBox Margin="0 10 0 0"
Name="UserNameTextBox"
Watermark="Please, enter user name..."
UseFloatingWatermark="True" />
<TextBlock Name="UserNameValidation"
Foreground="Red"
FontSize="12" />
<TextBox Margin="0 10 0 0"
Name="PasswordTextBox"
Watermark="Please, enter your password..."
UseFloatingWatermark="True"
PasswordChar="*" />
<TextBlock Name="PasswordValidation"
Foreground="Red"
FontSize="12" />
<ListBox x:Name="AwesomeListView">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock x:Name="MeaningLessName" Text="{Binding}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBox Margin="0 10 0 0"
x:Name="ConfirmPasswordTextBox"
Watermark="Please, confirm the password..."
UseFloatingWatermark="True"
PasswordChar="*" />
<TextBlock x:Name="ConfirmPasswordValidation"
TextWrapping="Wrap"
Foreground="Red"
FontSize="12" />
<TextBlock>
<TextBlock.Inlines>
<InlineCollection>
<Run x:Name="SignUpButtonDescription" />
</InlineCollection>
</TextBlock.Inlines>
</TextBlock>
<Button Margin="0 10 0 5"
Content="Sign up"
x:Name="SignUpButton" />
<TextBlock x:Name="CompoundValidation"
TextWrapping="Wrap"
Foreground="Red"
FontSize="12" />
</StackPanel>
</Window>

76
tests/Avalonia.Generators.Tests/Views/View.cs

@ -0,0 +1,76 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace Avalonia.Generators.Tests.Views;
public static class View
{
public const string NamedControl = "NamedControl.xml";
public const string NamedControls = "NamedControls.xml";
public const string XNamedControl = "xNamedControl.xml";
public const string XNamedControls = "xNamedControls.xml";
public const string NoNamedControls = "NoNamedControls.xml";
public const string CustomControls = "CustomControls.xml";
public const string DataTemplates = "DataTemplates.xml";
public const string SignUpView = "SignUpView.xml";
public const string AttachedProps = "AttachedProps.xml";
public const string FieldModifier = "FieldModifier.xml";
public const string ControlWithoutWindow = "ControlWithoutWindow.xml";
public const string ViewWithGenericBaseView = "ViewWithGenericBaseView.xml";
public static async Task<string> Load(string viewName)
{
var assembly = typeof(XamlXNameResolverTests).Assembly;
var fullResourceName = assembly
.GetManifestResourceNames()
.First(name => name.EndsWith(viewName));
await using var stream = assembly.GetManifestResourceStream(fullResourceName);
using var reader = new StreamReader(stream!);
return await reader.ReadToEndAsync();
}
public static CSharpCompilation CreateAvaloniaCompilation(string excludedPattern = null)
{
var compilation = CSharpCompilation
.Create("AvaloniaLib", options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.AddReferences(MetadataReference.CreateFromFile(typeof(string).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(IServiceProvider).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(ITypeDescriptorContext).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(ISupportInitialize).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(TypeConverterAttribute).Assembly.Location));
var avaloniaAssemblyLocation = typeof(TextBlock).Assembly.Location;
var avaloniaAssemblyDirectory = Path.GetDirectoryName(avaloniaAssemblyLocation);
var avaloniaAssemblyReferences = Directory
.EnumerateFiles(avaloniaAssemblyDirectory!)
.Where(file => file.EndsWith(".dll") &&
file.Contains("Avalonia") &&
(string.IsNullOrWhiteSpace(excludedPattern) || !file.Contains(excludedPattern)))
.Select(file => MetadataReference.CreateFromFile(file))
.ToList();
return compilation.AddReferences(avaloniaAssemblyReferences);
}
public static CSharpCompilation WithCustomTextBox(this CSharpCompilation compilation) =>
compilation.AddSyntaxTrees(
CSharpSyntaxTree.ParseText(
"using Avalonia.Controls;" +
"namespace Controls {" +
" public class CustomTextBox : TextBox { }" +
" public class EvilControl { }" +
"}"));
public static CSharpCompilation WithBaseView(this CSharpCompilation compilation) =>
compilation.AddSyntaxTrees(
CSharpSyntaxTree.ParseText(
"using Avalonia.Controls;" +
"namespace Sample.App { public class BaseView<TViewModel> : UserControl { } }"));
}

13
tests/Avalonia.Generators.Tests/Views/ViewWithGenericBaseView.xml

@ -0,0 +1,13 @@
<local:BaseView
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.ViewWithGenericBaseView"
Design.Width="300"
xmlns:sys="clr-namespace:System"
x:TypeArguments="sys:String"
x:Name="Root"
xmlns:local="clr-namespace:Sample.App">
<Grid>
<local:BaseView x:Name="NotAsRootNode" x:TypeArguments="sys:Int32" />
</Grid>
</local:BaseView>

7
tests/Avalonia.Generators.Tests/Views/xNamedControl.xml

@ -0,0 +1,7 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.xNamedControl">
<TextBox x:Name="UserNameTextBox"
Watermark="Username input"
UseFloatingWatermark="True" />
</Window>

14
tests/Avalonia.Generators.Tests/Views/xNamedControls.xml

@ -0,0 +1,14 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.xNamedControls">
<StackPanel>
<TextBox x:Name="UserNameTextBox"
Watermark="Username input"
UseFloatingWatermark="True" />
<TextBox x:Name="PasswordTextBox"
Watermark="Password input"
UseFloatingWatermark="True" />
<Button x:Name="SignUpButton"
Content="Sign up" />
</StackPanel>
</Window>

40
tests/Avalonia.Generators.Tests/XamlXClassResolverTests.cs

@ -0,0 +1,40 @@
using System.Threading.Tasks;
using Avalonia.Generators.Common;
using Avalonia.Generators.Compiler;
using Avalonia.Generators.Tests.Views;
using Xunit;
namespace Avalonia.Generators.Tests;
public class XamlXClassResolverTests
{
[Theory]
[InlineData("Sample.App", "NamedControl", View.NamedControl)]
[InlineData("Sample.App", "AttachedProps", View.AttachedProps)]
[InlineData("Sample.App", "CustomControls", View.CustomControls)]
[InlineData("Sample.App", "DataTemplates", View.DataTemplates)]
[InlineData("Sample.App", "FieldModifier", View.FieldModifier)]
[InlineData("Sample.App", "NamedControls", View.NamedControls)]
[InlineData("Sample.App", "NoNamedControls", View.NoNamedControls)]
[InlineData("Sample.App", "SignUpView", View.SignUpView)]
[InlineData("Sample.App", "xNamedControl", View.XNamedControl)]
[InlineData("Sample.App", "xNamedControls", View.XNamedControls)]
[InlineData("Sample.App", "ViewWithGenericBaseView", View.ViewWithGenericBaseView)]
public async Task Should_Resolve_Base_Class_From_Xaml_File(string nameSpace, string className, string markup)
{
var xaml = await View.Load(markup);
var compilation = View
.CreateAvaloniaCompilation()
.WithCustomTextBox()
.WithBaseView();
var types = new RoslynTypeSystem(compilation);
var resolver = new XamlXViewResolver(
types,
MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute));
var resolvedClass = resolver.ResolveView(xaml);
Assert.Equal(className, resolvedClass.ClassName);
Assert.Equal(nameSpace, resolvedClass.Namespace);
}
}

141
tests/Avalonia.Generators.Tests/XamlXNameResolverTests.cs

@ -0,0 +1,141 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Generators.Common;
using Avalonia.Generators.Common.Domain;
using Avalonia.Generators.Compiler;
using Avalonia.ReactiveUI;
using Avalonia.Generators.Tests.Views;
using Xunit;
namespace Avalonia.Generators.Tests;
public class XamlXNameResolverTests
{
[Theory]
[InlineData(View.NamedControl)]
[InlineData(View.XNamedControl)]
[InlineData(View.AttachedProps)]
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Named_Control(string resource)
{
var xaml = await View.Load(resource);
var controls = ResolveNames(xaml);
Assert.NotEmpty(controls);
Assert.Equal(1, controls.Count);
Assert.Equal("UserNameTextBox", controls[0].Name);
Assert.Contains(typeof(TextBox).FullName!, controls[0].TypeName);
}
[Theory]
[InlineData(View.NamedControls)]
[InlineData(View.XNamedControls)]
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Named_Controls(string resource)
{
var xaml = await View.Load(resource);
var controls = ResolveNames(xaml);
Assert.NotEmpty(controls);
Assert.Equal(3, controls.Count);
Assert.Equal("UserNameTextBox", controls[0].Name);
Assert.Equal("PasswordTextBox", controls[1].Name);
Assert.Equal("SignUpButton", controls[2].Name);
Assert.Contains(typeof(TextBox).FullName!, controls[0].TypeName);
Assert.Contains(typeof(TextBox).FullName!, controls[1].TypeName);
Assert.Contains(typeof(Button).FullName!, controls[2].TypeName);
}
[Fact]
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Custom_Controls()
{
var xaml = await View.Load(View.CustomControls);
var controls = ResolveNames(xaml);
Assert.NotEmpty(controls);
Assert.Equal(3, controls.Count);
Assert.Equal("ClrNamespaceRoutedViewHost", controls[0].Name);
Assert.Equal("UriRoutedViewHost", controls[1].Name);
Assert.Equal("UserNameTextBox", controls[2].Name);
Assert.Contains(typeof(RoutedViewHost).FullName!, controls[0].TypeName);
Assert.Contains(typeof(RoutedViewHost).FullName!, controls[1].TypeName);
Assert.Contains("Controls.CustomTextBox", controls[2].TypeName);
}
[Fact]
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_When_Types_Contains_Generic_Arguments()
{
var xaml = await View.Load(View.ViewWithGenericBaseView);
var controls = ResolveNames(xaml);
Assert.Equal(2, controls.Count);
var currentControl = controls[0];
Assert.Equal("Root", currentControl.Name);
Assert.Equal("global::Sample.App.BaseView<global::System.String>", currentControl.TypeName);
currentControl = controls[1];
Assert.Equal("NotAsRootNode", currentControl.Name);
Assert.Contains("Sample.App.BaseView", currentControl.TypeName);
Assert.Equal("global::Sample.App.BaseView<global::System.Int32>", currentControl.TypeName);
}
[Fact]
public async Task Should_Not_Resolve_Named_Controls_From_Avalonia_Markup_File_Without_Named_Controls()
{
var xaml = await View.Load(View.NoNamedControls);
var controls = ResolveNames(xaml);
Assert.Empty(controls);
}
[Fact]
public async Task Should_Not_Resolve_Elements_From_DataTemplates()
{
var xaml = await View.Load(View.DataTemplates);
var controls = ResolveNames(xaml);
Assert.NotEmpty(controls);
Assert.Equal(2, controls.Count);
Assert.Equal("UserNameTextBox", controls[0].Name);
Assert.Equal("NamedListBox", controls[1].Name);
Assert.Contains(typeof(TextBox).FullName!, controls[0].TypeName);
Assert.Contains(typeof(ListBox).FullName!, controls[1].TypeName);
}
[Fact]
public async Task Should_Resolve_Names_From_Complex_Views()
{
var xaml = await View.Load(View.SignUpView);
var controls = ResolveNames(xaml);
Assert.NotEmpty(controls);
Assert.Equal(10, controls.Count);
Assert.Equal("UserNameTextBox", controls[0].Name);
Assert.Equal("UserNameValidation", controls[1].Name);
Assert.Equal("PasswordTextBox", controls[2].Name);
Assert.Equal("PasswordValidation", controls[3].Name);
Assert.Equal("AwesomeListView", controls[4].Name);
Assert.Equal("ConfirmPasswordTextBox", controls[5].Name);
Assert.Equal("ConfirmPasswordValidation", controls[6].Name);
Assert.Equal("SignUpButtonDescription", controls[7].Name);
Assert.Equal("SignUpButton", controls[8].Name);
Assert.Equal("CompoundValidation", controls[9].Name);
}
private static IReadOnlyList<ResolvedName> ResolveNames(string xaml)
{
var compilation =
View.CreateAvaloniaCompilation()
.WithCustomTextBox()
.WithBaseView();
var classResolver = new XamlXViewResolver(
new RoslynTypeSystem(compilation),
MiniCompiler.CreateDefault(
new RoslynTypeSystem(compilation),
MiniCompiler.AvaloniaXmlnsDefinitionAttribute));
var classInfo = classResolver.ResolveView(xaml);
var nameResolver = new XamlXNameResolver();
return nameResolver.ResolveNames(classInfo.Xaml);
}
}

1
tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs

@ -140,6 +140,7 @@ namespace Avalonia.IntegrationTests.Appium
.First(x => x.Text == newWindowTitle);
var (close, _, _) = ((AppiumWebElement)newWindow).GetChromeButtons();
close!.Click();
Thread.Sleep(1000);
});
}
}

Loading…
Cancel
Save