diff --git a/README.md b/README.md index d401414ab4..469d51604e 100644 --- a/README.md +++ b/README.md @@ -6,20 +6,11 @@ -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 idea is that you include your Avalonia XAML files into your project via `` and then decorate your view class with `[GenerateTypedNameReferences]` and the source generator will look for the `xaml` (or `axaml`) file with the same name as your C# class. The source generator then parses the XML markup, finds all XML tags with `x:Name` attributes and generates the C# code. +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 subclasse of `Avalonia.INambe` and parses the XML markup, finds all XML tags with `x:Name` attributes and generates the C# code. ### Getting Started -So in your project file you paste the following code: - -```xml - - - - -``` - -And then you reference the source generator by installing a NuGet package: +Add reference the source generator by installing a NuGet package: ``` dotnet add package XamlNameReferenceGenerator @@ -33,14 +24,42 @@ Or, if you are using [submodules](https://git-scm.com/docs/git-submodule), you c OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> + + + +``` + +Finally, you declare your view class as `partial` + +```cs +using Avalonia.Controls; + +public partial class SignUpView : Window +{ + public SignUpView() + { + AvaloniaXamlLoader.Load(this); + UserNameTextBox.Text = "Joseph"; // Coolstuff! + } +} +``` + +If you just want specific classes: + +add at your csproj this lines: + +```xml + + false + ``` -Finally, you declare your view class as `partial` and decorate it with `[GenerateTypedNameReferences]`: +Finally, decorate yours class with attribute `[GenerateTypedNameReferences]`: ```cs using Avalonia.Controls; -[GenerateTypedNameReferences] // Note the 'partial' keyword. +[GenerateTypedNameReferences] public partial class SignUpView : Window { public SignUpView() diff --git a/src/Avalonia.NameGenerator/Avalonia.NameGenerator.csproj b/src/Avalonia.NameGenerator/Avalonia.NameGenerator.csproj index 3e965b969b..4ec57fbe66 100644 --- a/src/Avalonia.NameGenerator/Avalonia.NameGenerator.csproj +++ b/src/Avalonia.NameGenerator/Avalonia.NameGenerator.csproj @@ -5,6 +5,7 @@ true false XamlNameReferenceGenerator + true @@ -14,6 +15,12 @@ + + + true + buildTransitive\$(PackageId).props + + diff --git a/src/Avalonia.NameGenerator/Generator.props b/src/Avalonia.NameGenerator/Generator.props new file mode 100644 index 0000000000..f41d0800bd --- /dev/null +++ b/src/Avalonia.NameGenerator/Generator.props @@ -0,0 +1,16 @@ + + + true + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia.NameGenerator/NameReferenceGenerator.cs b/src/Avalonia.NameGenerator/NameReferenceGenerator.cs index f67e75f0f9..687608a738 100644 --- a/src/Avalonia.NameGenerator/NameReferenceGenerator.cs +++ b/src/Avalonia.NameGenerator/NameReferenceGenerator.cs @@ -15,6 +15,7 @@ namespace Avalonia.NameGenerator [Generator] public class NameReferenceGenerator : ISourceGenerator { + private const string INamedType = "Avalonia.INamed"; private const string AttributeName = "GenerateTypedNameReferencesAttribute"; private const string AttributeFile = "GenerateTypedNameReferencesAttribute"; private const string AttributeCode = @"// @@ -43,7 +44,7 @@ internal sealed class GenerateTypedNameReferencesAttribute : Attribute { } return; } - var compilation = (CSharpCompilation) context.Compilation; + var compilation = (CSharpCompilation)context.Compilation; var nameResolver = new XamlXNameResolver(compilation); var nameGenerator = new FindControlNameGenerator(); var symbols = UnpackAnnotatedTypes(context, compilation, receiver); @@ -108,6 +109,10 @@ internal sealed class GenerateTypedNameReferencesAttribute : Attribute { } CSharpCompilation existingCompilation, NameReferenceSyntaxReceiver nameReferenceSyntaxReceiver) { + var allowedNameGenerator = context + .GetMSBuildProperty("AvaloniaNameGenerator", "false") + .Equals("true", StringComparison.OrdinalIgnoreCase); + var options = (CSharpParseOptions)existingCompilation.SyntaxTrees[0].Options; var compilation = existingCompilation.AddSyntaxTrees( CSharpSyntaxTree.ParseText( @@ -119,15 +124,22 @@ internal sealed class GenerateTypedNameReferencesAttribute : Attribute { } foreach (var candidateClass in nameReferenceSyntaxReceiver.CandidateClasses) { var model = compilation.GetSemanticModel(candidateClass.SyntaxTree); - var typeSymbol = (INamedTypeSymbol) model.GetDeclaredSymbol(candidateClass); - var relevantAttribute = typeSymbol! - .GetAttributes() - .FirstOrDefault(attr => attr.AttributeClass!.Equals(attributeSymbol, SymbolEqualityComparer.Default)); - - if (relevantAttribute == null) + var typeSymbol = (INamedTypeSymbol)model.GetDeclaredSymbol(candidateClass); + if (InheritsFrom(typeSymbol, INamedType) == false) { continue; } + if (allowedNameGenerator == false) + { + var relevantAttribute = typeSymbol! + .GetAttributes() + .FirstOrDefault(attr => attr.AttributeClass!.Equals(attributeSymbol, SymbolEqualityComparer.Default)); + + if (relevantAttribute == null) + { + continue; + } + } var isPartial = candidateClass .Modifiers @@ -159,5 +171,31 @@ internal sealed class GenerateTypedNameReferencesAttribute : Attribute { } return symbols; } + + static bool InheritsFrom(INamedTypeSymbol symbol, string typeName) + { + while (true) + { + if (symbol.ToString() == typeName) + { + return true; + } + if (symbol.BaseType != null) + { + var intefaces = symbol.AllInterfaces; + foreach (var @interface in intefaces) + { + if (@interface.ToString() == typeName) + { + return true; + } + } + symbol = symbol.BaseType; + continue; + } + break; + } + return false; + } } } diff --git a/src/Avalonia.NameGenerator/NameReferenceSyntaxReceiver.cs b/src/Avalonia.NameGenerator/NameReferenceSyntaxReceiver.cs index fec82c8e14..f74a54c467 100644 --- a/src/Avalonia.NameGenerator/NameReferenceSyntaxReceiver.cs +++ b/src/Avalonia.NameGenerator/NameReferenceSyntaxReceiver.cs @@ -10,8 +10,7 @@ namespace Avalonia.NameGenerator public void OnVisitSyntaxNode(SyntaxNode syntaxNode) { - if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax && - classDeclarationSyntax.AttributeLists.Count > 0) + if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax) CandidateClasses.Add(classDeclarationSyntax); } } diff --git a/src/Avalonia.NameGenerator/SourceGeneratorContextExtensions.cs b/src/Avalonia.NameGenerator/SourceGeneratorContextExtensions.cs new file mode 100644 index 0000000000..6c30ac512f --- /dev/null +++ b/src/Avalonia.NameGenerator/SourceGeneratorContextExtensions.cs @@ -0,0 +1,28 @@ +using Microsoft.CodeAnalysis; +using System.Linq; + +namespace Avalonia.NameGenerator +{ + internal static class SourceGeneratorContextExtensions + { + private const string SourceItemGroupMetadata = "build_metadata.AdditionalFiles.SourceItemGroup"; + + 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 string[] GetMSBuildItems(this GeneratorExecutionContext context, string name) + => context + .AdditionalFiles + .Where(f => context.AnalyzerConfigOptions + .GetOptions(f).TryGetValue(SourceItemGroupMetadata, out var sourceItemGroup) + && sourceItemGroup == name) + .Select(f => f.Path) + .ToArray(); + } +}