Browse Source

feature: Support x:FieldModifier Directive (#20)

* Refactoring, decomposition, more unit tests
* Support x:FieldModifier
* Use Xamarin.Forms API for x:FieldModifier
* Use <RestoreSources /> directive, bump Avalonia
* CRLF dance
* Use Fluent theme
* Move Avalonia packages to Directory.Build.props
* Bump test SDK version
pull/10407/head 0.1.9
Artyom V. Gorchakov 5 years ago
committed by GitHub
parent
commit
edf0217591
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 0
      external/.gitkeep
  2. 2
      src/Avalonia.NameGenerator.Sandbox/App.xaml
  3. 3
      src/Avalonia.NameGenerator.Sandbox/App.xaml.cs
  4. 12
      src/Avalonia.NameGenerator.Sandbox/Avalonia.NameGenerator.Sandbox.csproj
  5. 4
      src/Avalonia.NameGenerator.Sandbox/Program.cs
  6. 4
      src/Avalonia.NameGenerator.Sandbox/Views/SignUpView.xaml
  7. 3
      src/Avalonia.NameGenerator.Sandbox/Views/SignUpView.xaml.cs
  8. 10
      src/Avalonia.NameGenerator.Tests/Avalonia.NameGenerator.Tests.csproj
  9. 41
      src/Avalonia.NameGenerator.Tests/FindControlNameGeneratorTests.cs
  10. 11
      src/Avalonia.NameGenerator.Tests/GeneratedCode/AttachedProps.txt
  11. 32
      src/Avalonia.NameGenerator.Tests/GeneratedCode/Code.cs
  12. 13
      src/Avalonia.NameGenerator.Tests/GeneratedCode/CustomControls.txt
  13. 12
      src/Avalonia.NameGenerator.Tests/GeneratedCode/DataTemplates.txt
  14. 16
      src/Avalonia.NameGenerator.Tests/GeneratedCode/FieldModifier.txt
  15. 11
      src/Avalonia.NameGenerator.Tests/GeneratedCode/NamedControl.txt
  16. 13
      src/Avalonia.NameGenerator.Tests/GeneratedCode/NamedControls.txt
  17. 11
      src/Avalonia.NameGenerator.Tests/GeneratedCode/NoNamedControls.txt
  18. 19
      src/Avalonia.NameGenerator.Tests/GeneratedCode/SignUpView.txt
  19. 11
      src/Avalonia.NameGenerator.Tests/GeneratedCode/xNamedControl.txt
  20. 13
      src/Avalonia.NameGenerator.Tests/GeneratedCode/xNamedControls.txt
  21. 14
      src/Avalonia.NameGenerator.Tests/MiniCompilerTests.cs
  22. 180
      src/Avalonia.NameGenerator.Tests/NameResolverTests.cs
  23. 27
      src/Avalonia.NameGenerator.Tests/Views/FieldModifier.xml
  24. 67
      src/Avalonia.NameGenerator.Tests/Views/View.cs
  25. 121
      src/Avalonia.NameGenerator.Tests/XamlXNameResolverTests.cs
  26. 8
      src/Avalonia.NameGenerator.sln
  27. 4
      src/Avalonia.NameGenerator/Avalonia.NameGenerator.csproj
  28. 2
      src/Avalonia.NameGenerator/Compiler/DataTemplateTransformer.cs
  29. 3
      src/Avalonia.NameGenerator/Compiler/MiniCompiler.cs
  30. 29
      src/Avalonia.NameGenerator/Compiler/NameDirectiveTransformer.cs
  31. 2
      src/Avalonia.NameGenerator/Compiler/RoslynTypeSystem.cs
  32. 9
      src/Avalonia.NameGenerator/Infrastructure/INameResolver.cs
  33. 29
      src/Avalonia.NameGenerator/Infrastructure/NameDirectiveTransformer.cs
  34. 54
      src/Avalonia.NameGenerator/Infrastructure/NameReceiver.cs
  35. 32
      src/Avalonia.NameGenerator/Infrastructure/NameResolver.cs
  36. 36
      src/Avalonia.NameGenerator/NameReferenceGenerator.cs
  37. 1
      src/Avalonia.NameGenerator/NameReferenceSyntaxReceiver.cs
  38. 31
      src/Avalonia.NameGenerator/Resolver/FindControlNameGenerator.cs
  39. 9
      src/Avalonia.NameGenerator/Resolver/INameGenerator.cs
  40. 43
      src/Avalonia.NameGenerator/Resolver/INameResolver.cs
  41. 102
      src/Avalonia.NameGenerator/Resolver/XamlXNameResolver.cs
  42. 10
      src/Directory.Build.props

0
external/.gitkeep.txt → external/.gitkeep

2
src/Avalonia.NameGenerator.Sandbox/App.xaml

@ -2,6 +2,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Avalonia.NameGenerator.Sandbox.App">
<Application.Styles>
<StyleInclude Source="avares://Citrus.Avalonia/Citrus.xaml" />
<FluentTheme Mode="Dark" />
</Application.Styles>
</Application>

3
src/Avalonia.NameGenerator.Sandbox/App.xaml.cs

@ -9,7 +9,8 @@ namespace Avalonia.NameGenerator.Sandbox
public override void OnFrameworkInitializationCompleted()
{
new SignUpView().Show();
var view = new SignUpView();
view.Show();
base.OnFrameworkInitializationCompleted();
}
}

12
src/Avalonia.NameGenerator.Sandbox/Avalonia.NameGenerator.Sandbox.csproj

@ -4,13 +4,8 @@
<TargetFramework>net5</TargetFramework>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<InstallAvalonia>true</InstallAvalonia>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.0-preview5" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.0-preview5" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.0-preview5" />
<PackageReference Include="Citrus.Avalonia" Version="1.2.6" />
</ItemGroup>
<ItemGroup>
<Compile Update="**\*.xaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
@ -18,11 +13,10 @@
<EmbeddedResource Include="**\*.xaml">
<SubType>Designer</SubType>
</EmbeddedResource>
<!-- Note this AdditionalFiles directive. -->
<AdditionalFiles Include="**\*.xaml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.NameGenerator\Avalonia.NameGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Avalonia.NameGenerator\Avalonia.NameGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>

4
src/Avalonia.NameGenerator.Sandbox/Program.cs

@ -6,10 +6,10 @@ namespace Avalonia.NameGenerator.Sandbox
{
public static void Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
public static AppBuilder BuildAvaloniaApp()
private static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UseReactiveUI()
.UsePlatformDetect()
.LogToDebug();
.LogToTrace();
}
}

4
src/Avalonia.NameGenerator.Sandbox/Views/SignUpView.xaml

@ -10,9 +10,11 @@
UseFloatingWatermark="True" />
<TextBlock Name="UserNameValidation"
Foreground="Red"
x:FieldModifier="public"
FontSize="12" />
<TextBox Margin="0 10 0 0"
Name="PasswordTextBox"
x:FieldModifier="private"
Watermark="Please, enter your password..."
UseFloatingWatermark="True"
PasswordChar="*" />
@ -47,4 +49,4 @@
Foreground="Red"
FontSize="12" />
</StackPanel>
</Window>
</Window>

3
src/Avalonia.NameGenerator.Sandbox/Views/SignUpView.xaml.cs

@ -1,5 +1,4 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Avalonia.NameGenerator.Sandbox.Views

10
src/Avalonia.NameGenerator.Tests/Avalonia.NameGenerator.Tests.csproj

@ -4,19 +4,19 @@
<TargetFramework>net5</TargetFramework>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<InstallAvalonia>true</InstallAvalonia>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0-3.final" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.console" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="Avalonia" Version="0.10.0-preview5" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.0-preview5" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Views/*.xml" />
<EmbeddedResource Include="GeneratedCode/*.txt" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.NameGenerator\Avalonia.NameGenerator.csproj" />

41
src/Avalonia.NameGenerator.Tests/FindControlNameGeneratorTests.cs

@ -0,0 +1,41 @@
using System.Threading.Tasks;
using Avalonia.NameGenerator.Resolver;
using Avalonia.NameGenerator.Tests.GeneratedCode;
using Avalonia.NameGenerator.Tests.Views;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
namespace Avalonia.NameGenerator.Tests
{
public class FindControlNameGeneratorTests
{
[Theory]
[InlineData(Code.NamedControl, View.NamedControl)]
[InlineData(Code.NamedControls, View.NamedControls)]
[InlineData(Code.XNamedControl, View.XNamedControl)]
[InlineData(Code.XNamedControls, View.XNamedControls)]
[InlineData(Code.NoNamedControls, View.NoNamedControls)]
[InlineData(Code.CustomControls, View.CustomControls)]
[InlineData(Code.DataTemplates, View.DataTemplates)]
[InlineData(Code.SignUpView, View.SignUpView)]
[InlineData(Code.AttachedProps, View.AttachedProps)]
[InlineData(Code.FieldModifier, View.FieldModifier)]
public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File(string expectation, string markup)
{
var xaml = await View.Load(markup);
var compilation =
View.CreateAvaloniaCompilation()
.WithCustomTextBox();
var resolver = new XamlXNameResolver(compilation);
var generator = new FindControlNameGenerator();
var code = generator
.GenerateNames("SampleView", "Sample.App", resolver.ResolveNames(xaml))
.Replace("\r", string.Empty);
var expected = await Code.Load(expectation);
CSharpSyntaxTree.ParseText(code);
Assert.Equal(expected.Replace("\r", string.Empty), code);
}
}
}

11
src/Avalonia.NameGenerator.Tests/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.FindControl<global::Avalonia.Controls.TextBox>("UserNameTextBox");
}
}

32
src/Avalonia.NameGenerator.Tests/GeneratedCode/Code.cs

@ -0,0 +1,32 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace Avalonia.NameGenerator.Tests.GeneratedCode
{
public class Code
{
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 static async Task<string> Load(string generatedCodeResourceName)
{
var assembly = typeof(XamlXNameResolverTests).Assembly;
var fullResourceName = assembly
.GetManifestResourceNames()
.First(name => name.EndsWith(generatedCodeResourceName));
await using var stream = assembly.GetManifestResourceStream(fullResourceName);
using var reader = new StreamReader(stream!);
return await reader.ReadToEndAsync();
}
}
}

13
src/Avalonia.NameGenerator.Tests/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.FindControl<global::Avalonia.ReactiveUI.RoutedViewHost>("ClrNamespaceRoutedViewHost");
internal global::Avalonia.ReactiveUI.RoutedViewHost UriRoutedViewHost => this.FindControl<global::Avalonia.ReactiveUI.RoutedViewHost>("UriRoutedViewHost");
internal global::Controls.CustomTextBox UserNameTextBox => this.FindControl<global::Controls.CustomTextBox>("UserNameTextBox");
}
}

12
src/Avalonia.NameGenerator.Tests/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.FindControl<global::Avalonia.Controls.TextBox>("UserNameTextBox");
internal global::Avalonia.Controls.ListBox NamedListBox => this.FindControl<global::Avalonia.Controls.ListBox>("NamedListBox");
}
}

16
src/Avalonia.NameGenerator.Tests/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.FindControl<global::Avalonia.Controls.TextBox>("FirstNameTextBox");
public global::Avalonia.Controls.TextBox LastNameTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("LastNameTextBox");
protected global::Avalonia.Controls.TextBox PasswordTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("PasswordTextBox");
private global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox");
internal global::Avalonia.Controls.Button SignUpButton => this.FindControl<global::Avalonia.Controls.Button>("SignUpButton");
internal global::Avalonia.Controls.Button RegisterButton => this.FindControl<global::Avalonia.Controls.Button>("RegisterButton");
}
}

11
src/Avalonia.NameGenerator.Tests/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.FindControl<global::Avalonia.Controls.TextBox>("UserNameTextBox");
}
}

13
src/Avalonia.NameGenerator.Tests/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.FindControl<global::Avalonia.Controls.TextBox>("UserNameTextBox");
internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("PasswordTextBox");
internal global::Avalonia.Controls.Button SignUpButton => this.FindControl<global::Avalonia.Controls.Button>("SignUpButton");
}
}

11
src/Avalonia.NameGenerator.Tests/GeneratedCode/NoNamedControls.txt

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

19
src/Avalonia.NameGenerator.Tests/GeneratedCode/SignUpView.txt

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

11
src/Avalonia.NameGenerator.Tests/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.FindControl<global::Avalonia.Controls.TextBox>("UserNameTextBox");
}
}

13
src/Avalonia.NameGenerator.Tests/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.FindControl<global::Avalonia.Controls.TextBox>("UserNameTextBox");
internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("PasswordTextBox");
internal global::Avalonia.Controls.Button SignUpButton => this.FindControl<global::Avalonia.Controls.Button>("SignUpButton");
}
}

14
src/Avalonia.NameGenerator.Tests/MiniCompilerTests.cs

@ -1,9 +1,9 @@
using System;
using System.ComponentModel;
using Avalonia.Controls;
using Avalonia.NameGenerator.Compiler;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Avalonia.NameGenerator.Infrastructure;
using Avalonia.NameGenerator.Tests.Views;
using XamlX;
using XamlX.Parsers;
using Xunit;
@ -41,19 +41,15 @@ namespace Avalonia.NameGenerator.Tests
public void Should_Resolve_Types_From_Simple_Avalonia_Markup()
{
var xaml = XDocumentXamlParser.Parse(AvaloniaXaml);
var compilation = CreateAvaloniaCompilation();
var compilation = View.CreateAvaloniaCompilation();
MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)).Transform(xaml);
Assert.NotNull(xaml.Root);
}
private static CSharpCompilation CreateAvaloniaCompilation(string name = "AvaloniaCompilation") =>
CreateBasicCompilation(string.Empty, name)
.AddReferences(MetadataReference.CreateFromFile(typeof(TextBlock).Assembly.Location));
private static CSharpCompilation CreateBasicCompilation(string source, string name = "BasicCompilation") =>
private static CSharpCompilation CreateBasicCompilation(string source) =>
CSharpCompilation
.Create(name, options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.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))

180
src/Avalonia.NameGenerator.Tests/NameResolverTests.cs

@ -1,180 +0,0 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.ReactiveUI;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Avalonia.NameGenerator.Infrastructure;
using Xunit;
namespace Avalonia.NameGenerator.Tests
{
public class NameResolverTests
{
private const string NamedControl = "NamedControl.xml";
private const string NamedControls = "NamedControls.xml";
private const string XNamedControl = "xNamedControl.xml";
private const string XNamedControls = "xNamedControls.xml";
private const string NoNamedControls = "NoNamedControls.xml";
private const string CustomControls = "CustomControls.xml";
private const string DataTemplates = "DataTemplates.xml";
private const string SignUpView = "SignUpView.xml";
private const string AttachedProps = "AttachedProps.xml";
[Theory]
[InlineData(NamedControl)]
[InlineData(XNamedControl)]
[InlineData(AttachedProps)]
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Named_Control(string resource)
{
var xaml = await LoadEmbeddedResource(resource);
var compilation = CreateAvaloniaCompilation();
var resolver = new NameResolver(compilation);
var controls = resolver.ResolveNames(xaml);
Assert.NotEmpty(controls);
Assert.Equal(1, controls.Count);
Assert.Equal("UserNameTextBox", controls[0].Name);
Assert.Equal(typeof(TextBox).FullName, controls[0].TypeName);
}
[Theory]
[InlineData(NamedControls)]
[InlineData(XNamedControls)]
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Named_Controls(string resource)
{
var xaml = await LoadEmbeddedResource(resource);
var compilation = CreateAvaloniaCompilation();
var resolver = new NameResolver(compilation);
var controls = resolver.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.Equal(typeof(TextBox).FullName, controls[0].TypeName);
Assert.Equal(typeof(TextBox).FullName, controls[1].TypeName);
Assert.Equal(typeof(Button).FullName, controls[2].TypeName);
}
[Fact]
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Custom_Controls()
{
var compilation =
CreateAvaloniaCompilation()
.AddSyntaxTrees(
CSharpSyntaxTree.ParseText(
"using Avalonia.Controls;" +
"namespace Controls {" +
" public class CustomTextBox : TextBox { }" +
" public class EvilControl { }" +
"}"));
var xaml = await LoadEmbeddedResource(CustomControls);
var resolver = new NameResolver(compilation);
var controls = resolver.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.Equal(typeof(RoutedViewHost).FullName, controls[0].TypeName);
Assert.Equal(typeof(RoutedViewHost).FullName, controls[1].TypeName);
Assert.Equal("Controls.CustomTextBox", controls[2].TypeName);
}
[Fact]
public async Task Should_Not_Resolve_Named_Controls_From_Avalonia_Markup_File_Without_Named_Controls()
{
var xaml = await LoadEmbeddedResource(NoNamedControls);
var compilation = CreateAvaloniaCompilation();
var resolver = new NameResolver(compilation);
var controls = resolver.ResolveNames(xaml);
Assert.Empty(controls);
}
[Fact]
public async Task Should_Not_Resolve_Elements_From_DataTemplates()
{
var xaml = await LoadEmbeddedResource(DataTemplates);
var compilation = CreateAvaloniaCompilation();
var resolver = new NameResolver(compilation);
var controls = resolver.ResolveNames(xaml);
Assert.NotEmpty(controls);
Assert.Equal(2, controls.Count);
Assert.Equal("UserNameTextBox", controls[0].Name);
Assert.Equal("NamedListBox", controls[1].Name);
Assert.Equal(typeof(TextBox).FullName, controls[0].TypeName);
Assert.Equal(typeof(ListBox).FullName, controls[1].TypeName);
}
[Fact]
public async Task Should_Resolve_Names_From_Complex_Views()
{
var compilation =
CreateAvaloniaCompilation()
.AddSyntaxTrees(
CSharpSyntaxTree.ParseText(
"using Avalonia.Controls;" +
"namespace Controls {" +
" public class CustomTextBox : TextBox { }" +
"}"));
var xaml = await LoadEmbeddedResource(SignUpView);
var resolver = new NameResolver(compilation);
var controls = resolver.ResolveNames(xaml);
Assert.NotEmpty(controls);
Assert.Equal(9, 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("SignUpButton", controls[7].Name);
Assert.Equal("CompoundValidation", controls[8].Name);
}
private static CSharpCompilation CreateAvaloniaCompilation(string name = "AvaloniaCompilation2")
{
var compilation = CSharpCompilation
.Create(name, 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"))
.Select(file => MetadataReference.CreateFromFile(file))
.ToList();
return compilation.AddReferences(avaloniaAssemblyReferences);
}
private static async Task<string> LoadEmbeddedResource(string shortResourceName)
{
var assembly = typeof(NameResolverTests).Assembly;
var fullResourceName = assembly
.GetManifestResourceNames()
.First(name => name.EndsWith(shortResourceName));
await using var stream = assembly.GetManifestResourceStream(fullResourceName);
using var reader = new StreamReader(stream!);
return await reader.ReadToEndAsync();
}
}
}

27
src/Avalonia.NameGenerator.Tests/Views/FieldModifier.xml

@ -0,0 +1,27 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<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>

67
src/Avalonia.NameGenerator.Tests/Views/View.cs

@ -0,0 +1,67 @@
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.NameGenerator.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 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()
{
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"))
.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 { }" +
"}"));
}
}

121
src/Avalonia.NameGenerator.Tests/XamlXNameResolverTests.cs

@ -0,0 +1,121 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.NameGenerator.Resolver;
using Avalonia.ReactiveUI;
using Avalonia.NameGenerator.Tests.Views;
using Xunit;
namespace Avalonia.NameGenerator.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 compilation = View.CreateAvaloniaCompilation();
var resolver = new XamlXNameResolver(compilation);
var controls = resolver.ResolveNames(xaml);
Assert.NotEmpty(controls);
Assert.Equal(1, controls.Count);
Assert.Equal("UserNameTextBox", controls[0].Name);
Assert.Equal(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 compilation = View.CreateAvaloniaCompilation();
var resolver = new XamlXNameResolver(compilation);
var controls = resolver.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.Equal(typeof(TextBox).FullName, controls[0].TypeName);
Assert.Equal(typeof(TextBox).FullName, controls[1].TypeName);
Assert.Equal(typeof(Button).FullName, controls[2].TypeName);
}
[Fact]
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Custom_Controls()
{
var compilation =
View.CreateAvaloniaCompilation()
.WithCustomTextBox();
var xaml = await View.Load(View.CustomControls);
var resolver = new XamlXNameResolver(compilation);
var controls = resolver.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.Equal(typeof(RoutedViewHost).FullName, controls[0].TypeName);
Assert.Equal(typeof(RoutedViewHost).FullName, controls[1].TypeName);
Assert.Equal("Controls.CustomTextBox", controls[2].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 compilation = View.CreateAvaloniaCompilation();
var resolver = new XamlXNameResolver(compilation);
var controls = resolver.ResolveNames(xaml);
Assert.Empty(controls);
}
[Fact]
public async Task Should_Not_Resolve_Elements_From_DataTemplates()
{
var xaml = await View.Load(View.DataTemplates);
var compilation = View.CreateAvaloniaCompilation();
var resolver = new XamlXNameResolver(compilation);
var controls = resolver.ResolveNames(xaml);
Assert.NotEmpty(controls);
Assert.Equal(2, controls.Count);
Assert.Equal("UserNameTextBox", controls[0].Name);
Assert.Equal("NamedListBox", controls[1].Name);
Assert.Equal(typeof(TextBox).FullName, controls[0].TypeName);
Assert.Equal(typeof(ListBox).FullName, controls[1].TypeName);
}
[Fact]
public async Task Should_Resolve_Names_From_Complex_Views()
{
var compilation =
View.CreateAvaloniaCompilation()
.WithCustomTextBox();
var xaml = await View.Load(View.SignUpView);
var resolver = new XamlXNameResolver(compilation);
var controls = resolver.ResolveNames(xaml);
Assert.NotEmpty(controls);
Assert.Equal(9, 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("SignUpButton", controls[7].Name);
Assert.Equal("CompoundValidation", controls[8].Name);
}
}
}

8
src/Avalonia.NameGenerator.sln

@ -6,6 +6,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.NameGenerator.Sand
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.NameGenerator.Tests", "Avalonia.NameGenerator.Tests\Avalonia.NameGenerator.Tests.csproj", "{B13A0A44-85BC-49A7-970F-6C9BF8BDFD54}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{56EF74A3-1D59-42BC-B7EC-2E07C08B9F95}"
ProjectSection(SolutionItems) = preProject
..\version.json = ..\version.json
..\.gitignore = ..\.gitignore
..\.gitmodules = ..\.gitmodules
Directory.Build.props = Directory.Build.props
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU

4
src/Avalonia.NameGenerator/Avalonia.NameGenerator.csproj

@ -6,8 +6,8 @@
<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0-5.final" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<Compile Link="XamlX\filename" Include="../../external/XamlX/src/XamlX/**/*.cs" />

2
src/Avalonia.NameGenerator/Infrastructure/DataTemplateTransformer.cs → src/Avalonia.NameGenerator/Compiler/DataTemplateTransformer.cs

@ -1,7 +1,7 @@
using XamlX.Ast;
using XamlX.Transform;
namespace Avalonia.NameGenerator.Infrastructure
namespace Avalonia.NameGenerator.Compiler
{
internal class DataTemplateTransformer : IXamlAstTransformer
{

3
src/Avalonia.NameGenerator/Infrastructure/MiniCompiler.cs → src/Avalonia.NameGenerator/Compiler/MiniCompiler.cs

@ -1,12 +1,11 @@
using System;
using System.Collections.Generic;
using XamlX.Compiler;
using XamlX.Emit;
using XamlX.Transform;
using XamlX.Transform.Transformers;
using XamlX.TypeSystem;
namespace Avalonia.NameGenerator.Infrastructure
namespace Avalonia.NameGenerator.Compiler
{
internal sealed class MiniCompiler : XamlCompiler<object, IXamlEmitResult>
{

29
src/Avalonia.NameGenerator/Compiler/NameDirectiveTransformer.cs

@ -0,0 +1,29 @@
using XamlX;
using XamlX.Ast;
using XamlX.Transform;
namespace Avalonia.NameGenerator.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;
}
}
}

2
src/Avalonia.NameGenerator/Infrastructure/RoslynTypeSystem.cs → src/Avalonia.NameGenerator/Compiler/RoslynTypeSystem.cs

@ -4,7 +4,7 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using XamlX.TypeSystem;
namespace Avalonia.NameGenerator.Infrastructure
namespace Avalonia.NameGenerator.Compiler
{
public class RoslynTypeSystem : IXamlTypeSystem
{

9
src/Avalonia.NameGenerator/Infrastructure/INameResolver.cs

@ -1,9 +0,0 @@
using System.Collections.Generic;
namespace Avalonia.NameGenerator.Infrastructure
{
internal interface INameResolver
{
IReadOnlyList<(string TypeName, string Name)> ResolveNames(string xaml);
}
}

29
src/Avalonia.NameGenerator/Infrastructure/NameDirectiveTransformer.cs

@ -1,29 +0,0 @@
using XamlX;
using XamlX.Ast;
using XamlX.Transform;
namespace Avalonia.NameGenerator.Infrastructure
{
internal class NameDirectiveTransformer : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (node is XamlAstObjectNode objectNode)
{
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;
}
}
}

54
src/Avalonia.NameGenerator/Infrastructure/NameReceiver.cs

@ -1,54 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using XamlX.Ast;
namespace Avalonia.NameGenerator.Infrastructure
{
internal sealed class NameReceiver : IXamlAstVisitor
{
private readonly List<(string TypeName, string Name)> _items = new List<(string TypeName, string Name)>();
public IReadOnlyList<(string TypeName, string Name)> Controls => _items;
public IXamlAstNode Visit(IXamlAstNode node)
{
if (node is XamlAstObjectNode objectNode)
{
var clrType = objectNode.Type.GetClrType();
var isAvaloniaControl = clrType
.Interfaces
.Any(abstraction => abstraction.IsInterface &&
abstraction.FullName == "Avalonia.Controls.IControl");
if (!isAvaloniaControl)
{
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 typeNamePair = ($@"{clrType.Namespace}.{clrType.Name}", text.Text);
if (!_items.Contains(typeNamePair))
{
_items.Add(typeNamePair);
}
}
}
return node;
}
return node;
}
public void Push(IXamlAstNode node) { }
public void Pop() { }
}
}

32
src/Avalonia.NameGenerator/Infrastructure/NameResolver.cs

@ -1,32 +0,0 @@
using System.Collections.Generic;
using Microsoft.CodeAnalysis.CSharp;
using XamlX;
using XamlX.Parsers;
namespace Avalonia.NameGenerator.Infrastructure
{
internal class NameResolver : INameResolver
{
private const string AvaloniaXmlnsAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute";
private readonly CSharpCompilation _compilation;
public NameResolver(CSharpCompilation compilation) => _compilation = compilation;
public IReadOnlyList<(string TypeName, string Name)> ResolveNames(string xaml)
{
var parsed = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string>
{
{XamlNamespaces.Blend2008, XamlNamespaces.Blend2008}
});
MiniCompiler
.CreateDefault(new RoslynTypeSystem(_compilation), AvaloniaXmlnsAttribute)
.Transform(parsed);
var visitor = new NameReceiver();
parsed.Root.Visit(visitor);
parsed.Root.VisitChildren(visitor);
return visitor.Controls;
}
}
}

36
src/Avalonia.NameGenerator/NameReferenceGenerator.cs

@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
using Avalonia.NameGenerator.Infrastructure;
using Avalonia.NameGenerator.Resolver;
using Microsoft.CodeAnalysis.CSharp;
[assembly: InternalsVisibleTo("Avalonia.NameGenerator.Tests")]
@ -44,7 +44,8 @@ internal sealed class GenerateTypedNameReferencesAttribute : Attribute { }
}
var compilation = (CSharpCompilation) context.Compilation;
var xamlParser = new NameResolver(compilation);
var nameResolver = new XamlXNameResolver(compilation);
var nameGenerator = new FindControlNameGenerator();
var symbols = UnpackAnnotatedTypes(context, compilation, receiver);
if (symbols == null)
{
@ -79,7 +80,10 @@ internal sealed class GenerateTypedNameReferencesAttribute : Attribute { }
try
{
var sourceCode = GenerateSourceCode(xamlParser, typeSymbol, relevantXamlFile);
var nameSpace = typeSymbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat);
var xaml = relevantXamlFile.GetText()!.ToString();
var names = nameResolver.ResolveNames(xaml);
var sourceCode = nameGenerator.GenerateNames(typeSymbol.Name, nameSpace, names);
context.AddSource($"{typeSymbol.Name}.g.cs", SourceText.From(sourceCode, Encoding.UTF8));
}
catch (Exception exception)
@ -99,32 +103,6 @@ internal sealed class GenerateTypedNameReferencesAttribute : Attribute { }
}
}
private static string GenerateSourceCode(
INameResolver nameResolver,
INamedTypeSymbol classSymbol,
AdditionalText xamlFile)
{
var className = classSymbol.Name;
var nameSpace = classSymbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat);
var namedControls = nameResolver
.ResolveNames(xamlFile.GetText()!.ToString())
.Select(info => " " +
$"internal global::{info.TypeName} {info.Name} => " +
$"this.FindControl<global::{info.TypeName}>(\"{info.Name}\");");
return $@"// <auto-generated />
using Avalonia.Controls;
namespace {nameSpace}
{{
partial class {className}
{{
{string.Join("\n", namedControls)}
}}
}}
";
}
private static IReadOnlyList<INamedTypeSymbol> UnpackAnnotatedTypes(
GeneratorExecutionContext context,
CSharpCompilation existingCompilation,

1
src/Avalonia.NameGenerator/NameReferenceSyntaxReceiver.cs

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

31
src/Avalonia.NameGenerator/Resolver/FindControlNameGenerator.cs

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.NameGenerator.Resolver
{
internal class FindControlNameGenerator : INameGenerator
{
public string GenerateNames(string className, string nameSpace, IEnumerable<ResolvedName> names)
{
var namedControls = names
.Select(info => " " +
$"{info.FieldModifier} global::{info.TypeName} {info.Name} => " +
$"this.FindControl<global::{info.TypeName}>(\"{info.Name}\");")
.ToList();
var lines = string.Join("\n", namedControls);
return $@"// <auto-generated />
using Avalonia.Controls;
namespace {nameSpace}
{{
partial class {className}
{{
{lines}
}}
}}
";
}
}
}

9
src/Avalonia.NameGenerator/Resolver/INameGenerator.cs

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace Avalonia.NameGenerator.Resolver
{
internal interface INameGenerator
{
string GenerateNames(string className, string nameSpace, IEnumerable<ResolvedName> names);
}
}

43
src/Avalonia.NameGenerator/Resolver/INameResolver.cs

@ -0,0 +1,43 @@
using System.Collections.Generic;
namespace Avalonia.NameGenerator.Resolver
{
internal interface INameResolver
{
IReadOnlyList<ResolvedName> ResolveNames(string xaml);
}
internal class ResolvedName
{
public string TypeName { get; }
public string Name { get; }
public string FieldModifier { get; }
public ResolvedName(string typeName, string name, string fieldModifier)
{
TypeName = typeName;
Name = name;
FieldModifier = fieldModifier;
}
public override bool Equals(object obj)
{
if (obj is not ResolvedName name)
return false;
return name.Name == Name &&
name.TypeName == TypeName &&
name.FieldModifier == FieldModifier;
}
public override int GetHashCode()
{
unchecked
{
var hashCode = TypeName != null ? TypeName.GetHashCode() : 0;
hashCode = (hashCode * 397) ^ (Name != null ? Name.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (FieldModifier != null ? FieldModifier.GetHashCode() : 0);
return hashCode;
}
}
}
}

102
src/Avalonia.NameGenerator/Resolver/XamlXNameResolver.cs

@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.NameGenerator.Compiler;
using Microsoft.CodeAnalysis.CSharp;
using XamlX;
using XamlX.Ast;
using XamlX.Parsers;
namespace Avalonia.NameGenerator.Resolver
{
internal class XamlXNameResolver : INameResolver, IXamlAstVisitor
{
private const string AvaloniaXmlnsAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute";
private readonly List<ResolvedName> _items = new();
private readonly MiniCompiler _compiler;
public XamlXNameResolver(CSharpCompilation compilation) =>
_compiler = MiniCompiler
.CreateDefault(
new RoslynTypeSystem(compilation),
AvaloniaXmlnsAttribute);
public IReadOnlyList<ResolvedName> ResolveNames(string xaml)
{
var parsed = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string>
{
{XamlNamespaces.Blend2008, XamlNamespaces.Blend2008}
});
_compiler.Transform(parsed);
parsed.Root.Visit(this);
parsed.Root.VisitChildren(this);
return _items;
}
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node)
{
if (node is not XamlAstObjectNode objectNode)
return node;
var clrType = objectNode.Type.GetClrType();
var isAvaloniaControl = clrType
.Interfaces
.Any(abstraction => abstraction.IsInterface &&
abstraction.FullName == "Avalonia.Controls.IControl");
if (!isAvaloniaControl)
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 resolvedName = new ResolvedName(typeName, text.Text, fieldModifier);
if (_items.Contains(resolvedName))
continue;
_items.Add(resolvedName);
}
}
return node;
}
void IXamlAstVisitor.Push(IXamlAstNode node) { }
void IXamlAstVisitor.Pop() { }
private static 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",
_ => "internal"
};
}
}
}

10
src/Directory.build.props → src/Directory.Build.props

@ -8,10 +8,20 @@
<RepositoryUrl>https://github.com/avaloniaui/Avalonia.NameGenerator</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<InstallAvalonia>false</InstallAvalonia>
<RestoreSources>
https://nuget.avaloniaui.net/repository/avalonia-all/index.json;
https://api.nuget.org/v3/index.json;
</RestoreSources>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nerdbank.GitVersioning" Version="3.3.37" PrivateAssets="all" />
</ItemGroup>
<ItemGroup Condition="'$(InstallAvalonia)' == 'true'">
<PackageReference Include="Avalonia" Version="0.10.999-cibuild0012810-beta" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.999-cibuild0012810-beta" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.999-cibuild0012810-beta" />
</ItemGroup>
<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)..\LICENSE" Pack="true" PackagePath="LICENSE" />
</ItemGroup>
Loading…
Cancel
Save