Browse Source

Allow specifying namespaces manually

pull/10407/head
Artyom 6 years ago
parent
commit
433c279e72
  1. 97
      README.md
  2. 3
      XamlNameReferenceGenerator.Sandbox/Program.cs
  3. 3
      XamlNameReferenceGenerator.Sandbox/SignUpView.xaml.cs
  4. 53
      XamlNameReferenceGenerator/NameReferenceGenerator.cs
  5. 3
      XamlNameReferenceGenerator/NameReferenceXamlParser.cs

97
README.md

@ -0,0 +1,97 @@
> **Warning** This is just a proof of concept, don't use it for production purposes! There are no unit tests in this project yet. Also, this project hasn't been extensively tested with any other app except for the `XamlNameReferenceGenerator.Sandbox` app.
### C# `SourceGenerator` for Typed Avalonia `x:Name` References
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 sandbox app is targeting `net5` which is still in preview, so this source generator is an early proof-of-concept. 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.
### Getting Started
So in your project file you write the following code:
```xml
<ItemGroup>
<Compile Update="**\*.xaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
<EmbeddedResource Include="**\*.xaml">
<SubType>Designer</SubType>
</EmbeddedResource>
<!-- Note this AdditionalFiles directive. -->
<AdditionalFiles Include="**\*.xaml" />
</ItemGroup>
```
And then you reference the source generator as such:
```xml
<ItemGroup>
<ProjectReference Include="../XamlNameReferenceGenerator/XamlNameReferenceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
```
Finally, you declare your view class as `partial` and decorate it with `[GenerateTypedNameReferences]`:
```cs
[GenerateTypedNameReferences] // Coolstuff!
public partial class SignUpView : Window
{
public SignUpView()
{
AvaloniaXamlLoader.Load(this);
UserNameTextBox.Text = "Joseph"; // Coolstuff!
}
}
```
### What do the generated sources look like?
For the [`SignUpView` view class](https://github.com/worldbeater/XamlNameReferenceGenerator/blob/main/XamlNameReferenceGenerator.Sandbox/SignUpView.xaml#L6) from [the sandbox project](https://github.com/worldbeater/XamlNameReferenceGenerator/tree/main/XamlNameReferenceGenerator.Sandbox), we get the following generated output:
```cs
// <auto-generated />
using System;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace XamlNameReferenceGenerator.Sandbox
{
public partial class SignUpView
{
public TextBox UserNameTextBox => this.FindControl<TextBox>("UserNameTextBox");
public TextBlock UserNameValidation => this.FindControl<TextBlock>("UserNameValidation");
public TextBox PasswordTextBox => this.FindControl<TextBox>("PasswordTextBox");
public TextBlock PasswordValidation => this.FindControl<TextBlock>("PasswordValidation");
public TextBox ConfirmPasswordTextBox => this.FindControl<TextBox>("ConfirmPasswordTextBox");
public TextBlock ConfirmPasswordValidation => this.FindControl<TextBlock>("ConfirmPasswordValidation");
public Button SignUpButton => this.FindControl<Button>("SignUpButton");
public TextBlock CompoundValidation => this.FindControl<TextBlock>("CompoundValidation");
}
}
```
### Why do I need this?
The typed `x:Name` references might be useful if you decide to use e.g. [ReactiveUI code-behind bindings](https://www.reactiveui.net/docs/handbook/data-binding/):
```cs
[GenerateTypedNameReferences] // Coolstuff!
public partial class SignUpView : ReactiveWindow<SignUpViewModel>
{
public SignUpView()
{
AvaloniaXamlLoader.Load(this);
this.WhenActivated(disposables =>
{
// Coolstuff!
this.BindValidation(ViewModel, x => x.UserName, x => x.UserNameValidation.Text)
.DisposeWith(disposables);
this.BindValidation(ViewModel, x => x.Password, x => x.PasswordValidation.Text)
.DisposeWith(disposables);
});
}
}
```

3
XamlNameReferenceGenerator.Sandbox/Program.cs

@ -1,4 +1,5 @@
using Avalonia;
using System;
using Avalonia;
using Avalonia.Logging.Serilog;
using Avalonia.ReactiveUI;

3
XamlNameReferenceGenerator.Sandbox/SignUpView.xaml.cs

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

53
XamlNameReferenceGenerator/NameReferenceGenerator.cs

@ -19,14 +19,23 @@ using System;
namespace XamlNameReferenceGenerator
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
sealed class GenerateTypedNameReferencesAttribute : Attribute { }
sealed class GenerateTypedNameReferencesAttribute : Attribute
{
public GenerateTypedNameReferencesAttribute() { }
public GenerateTypedNameReferencesAttribute(
params string[] additionalNamespaces) =>
AdditionalNamespaces = additionalNamespaces;
public string[] AdditionalNamespaces { get; set; } = null;
}
}
";
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,
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters |
SymbolDisplayGenericsOptions.IncludeTypeConstraints |
SymbolDisplayGenericsOptions.IncludeVariance);
@ -43,22 +52,27 @@ namespace XamlNameReferenceGenerator
return;
var symbols = UnpackAnnotatedTypes((CSharpCompilation) context.Compilation, receiver);
foreach (var typeSymbol in symbols)
foreach (var (typeSymbol, additionalNamespaces) 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));
var sourceCode = Debugger.Debug(
() => GenerateSourceCode(typeSymbol, relevantXamlFile, additionalNamespaces));
context.AddSource($"{typeSymbol.Name}.g.cs", SourceText.From(sourceCode, Encoding.UTF8));
}
}
private static string GenerateSourceCode(INamedTypeSymbol classSymbol, AdditionalText xamlFile)
private static string GenerateSourceCode(
INamedTypeSymbol classSymbol,
AdditionalText xamlFile,
IList<string> additionalNamespaces)
{
var className = classSymbol.Name;
var nameSpace = classSymbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat);
var namespaces = additionalNamespaces.Select(name => $"using {name};");
var namedControls = XamlParser
.GetNamedControls(xamlFile)
.Select(info => " " +
@ -69,6 +83,7 @@ namespace XamlNameReferenceGenerator
using System;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
{string.Join("\n", namespaces)}
namespace {nameSpace}
{{
@ -80,7 +95,7 @@ namespace {nameSpace}
";
}
private static IReadOnlyList<INamedTypeSymbol> UnpackAnnotatedTypes(
private static IReadOnlyList<(INamedTypeSymbol Type, IList<string> Namespaces)> UnpackAnnotatedTypes(
CSharpCompilation existingCompilation,
NameReferenceSyntaxReceiver nameReferenceSyntaxReceiver)
{
@ -91,20 +106,34 @@ namespace {nameSpace}
options));
var attributeSymbol = compilation.GetTypeByMetadataName(AttributeName);
var typeSymbols = new List<INamedTypeSymbol>();
var symbols = new List<(INamedTypeSymbol Type, IList<string> Namespaces)>();
foreach (var candidateClass in nameReferenceSyntaxReceiver.CandidateClasses)
{
var model = compilation.GetSemanticModel(candidateClass.SyntaxTree);
var typeSymbol = (INamedTypeSymbol) model.GetDeclaredSymbol(candidateClass);
var containsAttribute = typeSymbol!
var relevantAttribute = typeSymbol!
.GetAttributes()
.Any(attr => attr.AttributeClass!.Equals(attributeSymbol, SymbolEqualityComparer.Default));
.FirstOrDefault(attr => attr.AttributeClass!.Equals(attributeSymbol, SymbolEqualityComparer.Default));
if (containsAttribute)
typeSymbols.Add(typeSymbol);
if (relevantAttribute != null)
{
var additionalNamespaces = new List<string>();
if (relevantAttribute.NamedArguments.Any(kvp => kvp.Key == "AdditionalNamespaces"))
{
additionalNamespaces = relevantAttribute
.NamedArguments
.First(kvp => kvp.Key == "AdditionalNamespaces")
.Value.Values
.Where(constant => !constant.IsNull)
.Select(constant => constant.Value!.ToString())
.ToList();
}
symbols.Add((typeSymbol, additionalNamespaces));
}
}
return typeSymbols;
return symbols;
}
}
}

3
XamlNameReferenceGenerator/NameReferenceXamlParser.cs

@ -13,9 +13,6 @@ namespace XamlNameReferenceGenerator
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 =>
{

Loading…
Cancel
Save