Browse Source

feature: Generate x:Name mapping without the GenerateTypedNameReferences attribute (#22)

* feature: Generate x:Name mapping without the GenerateTypedNameReferences attribute.
* fix: Update README.md
pull/10407/head
workgroupengineering 5 years ago
committed by GitHub
parent
commit
cff386b326
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 45
      README.md
  2. 7
      src/Avalonia.NameGenerator/Avalonia.NameGenerator.csproj
  3. 16
      src/Avalonia.NameGenerator/Generator.props
  4. 52
      src/Avalonia.NameGenerator/NameReferenceGenerator.cs
  5. 3
      src/Avalonia.NameGenerator/NameReferenceSyntaxReceiver.cs
  6. 28
      src/Avalonia.NameGenerator/SourceGeneratorContextExtensions.cs

45
README.md

@ -6,20 +6,11 @@
<img src="https://hsto.org/webt/6a/j6/v5/6aj6v5vemc3g6zqcks0wm_irg1s.gif" />
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 `<AdditionalFiles Include="**\*.xaml" />` 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
<ItemGroup>
<!-- Note this AdditionalFiles directive. -->
<AdditionalFiles Include="**\*.xaml" />
</ItemGroup>
```
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" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="**\*.xaml"/>
</ItemGroup>
```
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
<PropertyGroup>
<AvaloniaNameGenerator>false</AvaloniaNameGenerator>
</PropertyGroup>
```
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()

7
src/Avalonia.NameGenerator/Avalonia.NameGenerator.csproj

@ -5,6 +5,7 @@
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IncludeBuildOutput>false</IncludeBuildOutput>
<PackageId>XamlNameReferenceGenerator</PackageId>
<NoPackageAnalysis>true</NoPackageAnalysis>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" PrivateAssets="all" />
@ -14,6 +15,12 @@
<Compile Link="XamlX\filename" Include="../../external/XamlX/src/XamlX/**/*.cs" />
<Compile Remove="../../external/XamlX/src/XamlX/**/SreTypeSystem.cs" />
</ItemGroup>
<ItemGroup>
<None Include="Generator.props">
<Pack>true</Pack>
<PackagePath>buildTransitive\$(PackageId).props</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>

16
src/Avalonia.NameGenerator/Generator.props

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

52
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 = @"// <auto-generated />
@ -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;
}
}
}

3
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);
}
}

28
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();
}
}
Loading…
Cancel
Save