12 changed files with 380 additions and 0 deletions
@ -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> |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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."; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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 |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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…
Reference in new issue