Browse Source

Add Avalonia Source Generator for x:Name refs

pull/10407/head
Artyom 5 years ago
parent
commit
5f132c99a2
  1. 7
      XamlNameReferenceGenerator.Sandbox/App.xaml
  2. 16
      XamlNameReferenceGenerator.Sandbox/App.xaml.cs
  3. 17
      XamlNameReferenceGenerator.Sandbox/Program.cs
  4. 40
      XamlNameReferenceGenerator.Sandbox/SignUpView.xaml
  5. 29
      XamlNameReferenceGenerator.Sandbox/SignUpView.xaml.cs
  6. 27
      XamlNameReferenceGenerator.Sandbox/XamlNameReferenceGenerator.Sandbox.csproj
  7. 22
      XamlNameReferenceGenerator.sln
  8. 32
      XamlNameReferenceGenerator/NameReferenceDebugger.cs
  9. 110
      XamlNameReferenceGenerator/NameReferenceGenerator.cs
  10. 19
      XamlNameReferenceGenerator/NameReferenceSyntaxReceiver.cs
  11. 48
      XamlNameReferenceGenerator/NameReferenceXamlParser.cs
  12. 13
      XamlNameReferenceGenerator/XamlNameReferenceGenerator.csproj

7
XamlNameReferenceGenerator.Sandbox/App.xaml

@ -0,0 +1,7 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="XamlNameReferenceGenerator.Sandbox.App">
<Application.Styles>
<StyleInclude Source="avares://Citrus.Avalonia/Citrus.xaml" />
</Application.Styles>
</Application>

16
XamlNameReferenceGenerator.Sandbox/App.xaml.cs

@ -0,0 +1,16 @@
using Avalonia;
using Avalonia.Markup.Xaml;
namespace XamlNameReferenceGenerator.Sandbox
{
public class App : Application
{
public override void Initialize() => AvaloniaXamlLoader.Load(this);
public override void OnFrameworkInitializationCompleted()
{
new SignUpView().Show();
base.OnFrameworkInitializationCompleted();
}
}
}

17
XamlNameReferenceGenerator.Sandbox/Program.cs

@ -0,0 +1,17 @@
using Avalonia;
using Avalonia.Logging.Serilog;
using Avalonia.ReactiveUI;
namespace XamlNameReferenceGenerator.Sandbox
{
class Program
{
public static void Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UseReactiveUI()
.UsePlatformDetect()
.LogToDebug();
}
}

40
XamlNameReferenceGenerator.Sandbox/SignUpView.xaml

@ -0,0 +1,40 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="XamlNameReferenceGenerator.Sandbox.SignUpView">
<StackPanel>
<TextBox 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" />
<Button Margin="0 10 0 5"
Content="Sign up"
x:Name="SignUpButton" />
<TextBlock x:Name="CompoundValidation"
TextWrapping="Wrap"
Foreground="Red"
FontSize="12" />
</StackPanel>
</Window>

29
XamlNameReferenceGenerator.Sandbox/SignUpView.xaml.cs

@ -0,0 +1,29 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace XamlNameReferenceGenerator.Sandbox
{
/// <summary>
/// This is a sample view class with typed x:Name references generated at compile-time using
/// .NET 5 source generators. The class should be marked with [GenerateTypedNameReferences],
/// this attribute is also compile-time generated. 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>
[GenerateTypedNameReferences]
public partial class SignUpView : Window
{
public SignUpView()
{
AvaloniaXamlLoader.Load(this);
UserNameTextBox.Text = "Joseph";
UserNameValidation.Text = "User name is valid.";
PasswordTextBox.Text = "qwerty";
PasswordValidation.Text = "Password is valid.";
ConfirmPasswordTextBox.Text = "qwerty";
ConfirmPasswordValidation.Text = "Password confirmation is valid.";
SignUpButton.Content = "Sign up please!";
CompoundValidation.Text = "Everything is okay.";
}
}
}

27
XamlNameReferenceGenerator.Sandbox/XamlNameReferenceGenerator.Sandbox.csproj

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5</TargetFramework>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.9.10" />
<PackageReference Include="Avalonia.Desktop" Version="0.9.10" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.9.10" />
<PackageReference Include="Citrus.Avalonia" Version="1.2.6" />
</ItemGroup>
<ItemGroup>
<Compile Update="**\*.xaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
<EmbeddedResource Include="**\*.xaml">
<SubType>Designer</SubType>
</EmbeddedResource>
<AdditionalFiles Include="**\*.xaml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../XamlNameReferenceGenerator/XamlNameReferenceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>

22
XamlNameReferenceGenerator.sln

@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XamlNameReferenceGenerator", "XamlNameReferenceGenerator\XamlNameReferenceGenerator.csproj", "{684ADF3B-8DFC-4F4A-93AD-F856561FCC39}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XamlNameReferenceGenerator.Sandbox", "XamlNameReferenceGenerator.Sandbox\XamlNameReferenceGenerator.Sandbox.csproj", "{C90BB1C6-5C33-494A-96FA-FEE7B34CA83C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{684ADF3B-8DFC-4F4A-93AD-F856561FCC39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{684ADF3B-8DFC-4F4A-93AD-F856561FCC39}.Debug|Any CPU.Build.0 = Debug|Any CPU
{684ADF3B-8DFC-4F4A-93AD-F856561FCC39}.Release|Any CPU.ActiveCfg = Release|Any CPU
{684ADF3B-8DFC-4F4A-93AD-F856561FCC39}.Release|Any CPU.Build.0 = Release|Any CPU
{C90BB1C6-5C33-494A-96FA-FEE7B34CA83C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C90BB1C6-5C33-494A-96FA-FEE7B34CA83C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C90BB1C6-5C33-494A-96FA-FEE7B34CA83C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C90BB1C6-5C33-494A-96FA-FEE7B34CA83C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

32
XamlNameReferenceGenerator/NameReferenceDebugger.cs

@ -0,0 +1,32 @@
using System;
using System.IO;
namespace XamlNameReferenceGenerator
{
internal class NameReferenceDebugger
{
private readonly string _path;
public NameReferenceDebugger(string path) => _path = path;
public string Debug(Func<string> function)
{
if (File.Exists(_path))
File.Delete(_path);
string sourceCode;
try
{
sourceCode = function();
File.WriteAllText(_path, sourceCode);
}
catch (Exception exception)
{
File.WriteAllText(_path, exception.ToString());
sourceCode = string.Empty;
}
return sourceCode;
}
}
}

110
XamlNameReferenceGenerator/NameReferenceGenerator.cs

@ -0,0 +1,110 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
using Microsoft.CodeAnalysis.CSharp;
namespace XamlNameReferenceGenerator
{
[Generator]
public class NameReferenceGenerator : ISourceGenerator
{
private const string AttributeName = "XamlNameReferenceGenerator.GenerateTypedNameReferencesAttribute";
private const string AttributeFile = "GenerateTypedNameReferencesAttribute";
private const string AttributeCode = @"// <auto-generated />
using System;
namespace XamlNameReferenceGenerator
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
sealed class GenerateTypedNameReferencesAttribute : Attribute { }
}
";
private const string DebugPath = @"C:\Users\prizr\Documents\GitHub\XamlNameReferenceGenerator\debug.txt";
private static readonly NameReferenceXamlParser XamlParser = new NameReferenceXamlParser();
private static readonly NameReferenceDebugger Debugger = new NameReferenceDebugger(DebugPath);
private static readonly SymbolDisplayFormat SymbolDisplayFormat = new SymbolDisplayFormat(
typeQualificationStyle:SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters |
SymbolDisplayGenericsOptions.IncludeTypeConstraints |
SymbolDisplayGenericsOptions.IncludeVariance);
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new NameReferenceSyntaxReceiver());
}
public void Execute(GeneratorExecutionContext context)
{
context.AddSource(AttributeFile, SourceText.From(AttributeCode, Encoding.UTF8));
if (!(context.SyntaxReceiver is NameReferenceSyntaxReceiver receiver))
return;
var symbols = UnpackAnnotatedTypes((CSharpCompilation) context.Compilation, receiver);
foreach (var typeSymbol in symbols)
{
var relevantXamlFile = context.AdditionalFiles
.First(text =>
text.Path.EndsWith($"{typeSymbol.Name}.xaml") ||
text.Path.EndsWith($"{typeSymbol.Name}.axaml"));
var sourceCode = Debugger.Debug(() => GenerateSourceCode(typeSymbol, relevantXamlFile));
context.AddSource($"{typeSymbol.Name}.g.cs", SourceText.From(sourceCode, Encoding.UTF8));
}
}
private static string GenerateSourceCode(INamedTypeSymbol classSymbol, AdditionalText xamlFile)
{
var className = classSymbol.Name;
var nameSpace = classSymbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat);
var namedControls = XamlParser
.GetNamedControls(xamlFile)
.Select(info => " " +
$"public {info.TypeName} {info.Name} => " +
$"this.FindControl<{info.TypeName}>(\"{info.Name}\");");
return $@"// <auto-generated />
using System;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace {nameSpace}
{{
public partial class {className}
{{
{string.Join("\n", namedControls)}
}}
}}
";
}
private static IReadOnlyList<INamedTypeSymbol> UnpackAnnotatedTypes(
CSharpCompilation existingCompilation,
NameReferenceSyntaxReceiver nameReferenceSyntaxReceiver)
{
var options = (CSharpParseOptions)existingCompilation.SyntaxTrees[0].Options;
var compilation = existingCompilation.AddSyntaxTrees(
CSharpSyntaxTree.ParseText(
SourceText.From(AttributeCode, Encoding.UTF8),
options));
var attributeSymbol = compilation.GetTypeByMetadataName(AttributeName);
var typeSymbols = new List<INamedTypeSymbol>();
foreach (var candidateClass in nameReferenceSyntaxReceiver.CandidateClasses)
{
var model = compilation.GetSemanticModel(candidateClass.SyntaxTree);
var typeSymbol = (INamedTypeSymbol) model.GetDeclaredSymbol(candidateClass);
var containsAttribute = typeSymbol!
.GetAttributes()
.Any(attr => attr.AttributeClass!.Equals(attributeSymbol, SymbolEqualityComparer.Default));
if (containsAttribute)
typeSymbols.Add(typeSymbol);
}
return typeSymbols;
}
}
}

19
XamlNameReferenceGenerator/NameReferenceSyntaxReceiver.cs

@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace XamlNameReferenceGenerator
{
internal class NameReferenceSyntaxReceiver : ISyntaxReceiver
{
public List<ClassDeclarationSyntax> CandidateClasses { get; } = new List<ClassDeclarationSyntax>();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax &&
classDeclarationSyntax.AttributeLists.Count > 0)
CandidateClasses.Add(classDeclarationSyntax);
}
}
}

48
XamlNameReferenceGenerator/NameReferenceXamlParser.cs

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Xml;
using Microsoft.CodeAnalysis;
namespace XamlNameReferenceGenerator
{
internal class NameReferenceXamlParser
{
public List<(string TypeName, string Name)> GetNamedControls(AdditionalText additionalText)
{
var xaml = additionalText.GetText()!.ToString();
var document = new XmlDocument();
document.LoadXml(xaml);
var namespaceManager = new XmlNamespaceManager(document.NameTable);
namespaceManager.AddNamespace("x", "http://schemas.microsoft.com/winfx/2006/xaml");
var names = new List<(string TypeName, string Name)>();
IterateThroughAllNodes(document, node =>
{
var type = node.Name;
var name = node.Attributes?["x:Name"]?.Value ??
node.Attributes?["Name"]?.Value;
if (!string.IsNullOrWhiteSpace(name))
names.Add((type, name));
});
return names;
}
private static void IterateThroughAllNodes(XmlDocument doc, Action<XmlNode> elementVisitor)
{
foreach (XmlNode node in doc.ChildNodes)
{
IterateNode(node, elementVisitor);
}
}
private static void IterateNode(XmlNode node, Action<XmlNode> elementVisitor)
{
elementVisitor(node);
foreach (XmlNode childNode in node.ChildNodes)
{
IterateNode(childNode, elementVisitor);
}
}
}
}

13
XamlNameReferenceGenerator/XamlNameReferenceGenerator.csproj

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.9.10" />
<PackageReference Include="Avalonia.Desktop" Version="0.9.10" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.9.10" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0-3.final" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0" PrivateAssets="all" />
</ItemGroup>
</Project>
Loading…
Cancel
Save