Skip to content
This repository has been archived by the owner on Jun 28, 2023. It is now read-only.

fix: Name Generation for Identical View Class Names #93

Merged
merged 2 commits into from
Nov 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ The `x:Name` generator can be configured via MsBuild properties that you can put
The generator will process only XAML files with base classes' namespaces matching the specified glob pattern(s).
Example: `MyApp.Presentation.*`, `MyApp.Presentation.Views;MyApp.Presentation.Controls`

- `AvaloniaNameGeneratorViewFileNamingStrategy`
Possible values: `ClassName`, `NamespaceAndClassName`
Default value: `NamespaceAndClassName`
Determines how the automatically generated view files should be [named](https://github.com/AvaloniaUI/Avalonia.NameGenerator/issues/92).

The default values are given by:

```xml
Expand All @@ -119,6 +124,7 @@ The default values are given by:
<AvaloniaNameGeneratorDefaultFieldModifier>internal</AvaloniaNameGeneratorDefaultFieldModifier>
<AvaloniaNameGeneratorFilterByPath>*</AvaloniaNameGeneratorFilterByPath>
<AvaloniaNameGeneratorFilterByNamespace>*</AvaloniaNameGeneratorFilterByNamespace>
<AvaloniaNameGeneratorViewFileNamingStrategy>NamespaceAndClassName</AvaloniaNameGeneratorViewFileNamingStrategy>
</PropertyGroup>
<!-- ... -->
</Project>
Expand Down
3 changes: 1 addition & 2 deletions src/Avalonia.NameGenerator.Sandbox/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Avalonia.Markup.Xaml;
using Avalonia.NameGenerator.Sandbox.ViewModels;
using Avalonia.NameGenerator.Sandbox.Views;

namespace Avalonia.NameGenerator.Sandbox;

Expand All @@ -10,7 +9,7 @@ public class App : Application

public override void OnFrameworkInitializationCompleted()
{
var view = new SignUpView
var view = new Views.SignUpView
{
ViewModel = new SignUpViewModel()
};
Expand Down
38 changes: 38 additions & 0 deletions src/Avalonia.NameGenerator.Sandbox/Controls/SignUpView.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Avalonia.NameGenerator.Sandbox.Controls"
x:Class="Avalonia.NameGenerator.Sandbox.Controls.SignUpView">
<StackPanel>
<controls:CustomTextBox 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>
</UserControl>
53 changes: 53 additions & 0 deletions src/Avalonia.NameGenerator.Sandbox/Controls/SignUpView.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.Reactive.Disposables;
using Avalonia.NameGenerator.Sandbox.ViewModels;
using Avalonia.ReactiveUI;
using ReactiveUI;
using ReactiveUI.Validation.Extensions;
using ReactiveUI.Validation.Formatters;

namespace Avalonia.NameGenerator.Sandbox.Controls;

/// <summary>
/// This is a sample view class with typed x:Name references generated using
/// .NET 5 source generators. 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>
public partial class SignUpView : ReactiveUserControl<SignUpViewModel>
{
public SignUpView()
{
// The InitializeComponent method is also generated automatically
// and lives in the autogenerated part of the partial class.
InitializeComponent();
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, x => x.UserName, x => x.UserNameTextBox.Text)
.DisposeWith(disposables);
this.Bind(ViewModel, x => x.Password, x => x.PasswordTextBox.Text)
.DisposeWith(disposables);
this.Bind(ViewModel, x => x.ConfirmPassword, x => x.ConfirmPasswordTextBox.Text)
.DisposeWith(disposables);
this.BindCommand(ViewModel, x => x.SignUp, x => x.SignUpButton)
.DisposeWith(disposables);

this.BindValidation(ViewModel, x => x.UserName, x => x.UserNameValidation.Text)
.DisposeWith(disposables);
this.BindValidation(ViewModel, x => x.Password, x => x.PasswordValidation.Text)
.DisposeWith(disposables);
this.BindValidation(ViewModel, x => x.ConfirmPassword, x => x.ConfirmPasswordValidation.Text)
.DisposeWith(disposables);

var newLineFormatter = new SingleLineFormatter(Environment.NewLine);
this.BindValidation(ViewModel, x => x.CompoundValidation.Text, newLineFormatter)
.DisposeWith(disposables);

// The references to text boxes below are also auto generated.
// Use Ctrl+Click in order to view the generated sources.
UserNameTextBox.Text = "Joseph!";
PasswordTextBox.Text = "1234";
ConfirmPasswordTextBox.Text = "1234";
});
}
}
32 changes: 1 addition & 31 deletions src/Avalonia.NameGenerator.Sandbox/Views/SignUpView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,6 @@
x:Class="Avalonia.NameGenerator.Sandbox.Views.SignUpView">
<StackPanel Margin="10">
<TextBlock Text="Sign Up" />
<controls:CustomTextBox 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" />
<controls:SignUpView x:Name="SignUpControl" />
</StackPanel>
</Window>
31 changes: 3 additions & 28 deletions src/Avalonia.NameGenerator.Sandbox/Views/SignUpView.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Disposables;
using Avalonia.NameGenerator.Sandbox.ViewModels;
using Avalonia.ReactiveUI;
using ReactiveUI;
using ReactiveUI.Validation.Extensions;
using ReactiveUI.Validation.Formatters;

namespace Avalonia.NameGenerator.Sandbox.Views;

Expand All @@ -23,31 +20,9 @@ public SignUpView()
InitializeComponent();
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, x => x.UserName, x => x.UserNameTextBox.Text)
this.WhenAnyValue(view => view.ViewModel)
.BindTo(this, view => view.SignUpControl.ViewModel)
.DisposeWith(disposables);
this.Bind(ViewModel, x => x.Password, x => x.PasswordTextBox.Text)
.DisposeWith(disposables);
this.Bind(ViewModel, x => x.ConfirmPassword, x => x.ConfirmPasswordTextBox.Text)
.DisposeWith(disposables);
this.BindCommand(ViewModel, x => x.SignUp, x => x.SignUpButton)
.DisposeWith(disposables);

this.BindValidation(ViewModel, x => x.UserName, x => x.UserNameValidation.Text)
.DisposeWith(disposables);
this.BindValidation(ViewModel, x => x.Password, x => x.PasswordValidation.Text)
.DisposeWith(disposables);
this.BindValidation(ViewModel, x => x.ConfirmPassword, x => x.ConfirmPasswordValidation.Text)
.DisposeWith(disposables);

var newLineFormatter = new SingleLineFormatter(Environment.NewLine);
this.BindValidation(ViewModel, x => x.CompoundValidation.Text, newLineFormatter)
.DisposeWith(disposables);

// The references to text boxes below are also auto generated.
// Use Ctrl+Click in order to view the generated sources.
UserNameTextBox.Text = "Joseph!";
PasswordTextBox.Text = "1234";
ConfirmPasswordTextBox.Text = "1234";
});
}
}
2 changes: 0 additions & 2 deletions src/Avalonia.NameGenerator.Tests/Views/View.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;

using Avalonia.Controls;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

Expand Down
29 changes: 12 additions & 17 deletions src/Avalonia.NameGenerator.Tests/XamlXNameResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Named_Cont
Assert.NotEmpty(controls);
Assert.Equal(1, controls.Count);
Assert.Equal("UserNameTextBox", controls[0].Name);
Assert.Equal(typeof(TextBox).FullName, controls[0].TypeName);
Assert.Contains(typeof(TextBox).FullName!, controls[0].TypeName);
}

[Theory]
Expand All @@ -40,9 +40,9 @@ public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Named_Cont
Assert.Equal("UserNameTextBox", controls[0].Name);
Assert.Equal("PasswordTextBox", controls[1].Name);
Assert.Equal("SignUpButton", controls[2].Name);
Assert.Equal(typeof(TextBox).FullName, controls[0].TypeName);
Assert.Equal(typeof(TextBox).FullName, controls[1].TypeName);
Assert.Equal(typeof(Button).FullName, controls[2].TypeName);
Assert.Contains(typeof(TextBox).FullName!, controls[0].TypeName);
Assert.Contains(typeof(TextBox).FullName!, controls[1].TypeName);
Assert.Contains(typeof(Button).FullName!, controls[2].TypeName);
}

[Fact]
Expand All @@ -56,9 +56,9 @@ public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Custom_Con
Assert.Equal("ClrNamespaceRoutedViewHost", controls[0].Name);
Assert.Equal("UriRoutedViewHost", controls[1].Name);
Assert.Equal("UserNameTextBox", controls[2].Name);
Assert.Equal(typeof(RoutedViewHost).FullName, controls[0].TypeName);
Assert.Equal(typeof(RoutedViewHost).FullName, controls[1].TypeName);
Assert.Equal("Controls.CustomTextBox", controls[2].TypeName);
Assert.Contains(typeof(RoutedViewHost).FullName!, controls[0].TypeName);
Assert.Contains(typeof(RoutedViewHost).FullName!, controls[1].TypeName);
Assert.Contains("Controls.CustomTextBox", controls[2].TypeName);
}

[Fact]
Expand All @@ -70,17 +70,12 @@ public async Task Should_Resolve_Types_From_Avalonia_Markup_File_When_Types_Cont

var currentControl = controls[0];
Assert.Equal("Root", currentControl.Name);
Assert.Equal("Sample.App.BaseView", currentControl.TypeName);
Assert.Equal(1, currentControl.GenericTypeArguments.Count);
Assert.Equal(typeof(string).FullName, currentControl.GenericTypeArguments[0]);
Assert.Equal("global::Sample.App.BaseView<global::System.String>", currentControl.PrintableTypeName);
Assert.Equal("global::Sample.App.BaseView<global::System.String>", currentControl.TypeName);

currentControl = controls[1];
Assert.Equal("NotAsRootNode", currentControl.Name);
Assert.Equal("Sample.App.BaseView", currentControl.TypeName);
Assert.Equal(1, currentControl.GenericTypeArguments.Count);
Assert.Equal(typeof(int).FullName, currentControl.GenericTypeArguments[0]);
Assert.Equal("global::Sample.App.BaseView<global::System.Int32>", currentControl.PrintableTypeName);
Assert.Contains("Sample.App.BaseView", currentControl.TypeName);
Assert.Equal("global::Sample.App.BaseView<global::System.Int32>", currentControl.TypeName);
}

[Fact]
Expand All @@ -102,8 +97,8 @@ public async Task Should_Not_Resolve_Elements_From_DataTemplates()
Assert.Equal(2, controls.Count);
Assert.Equal("UserNameTextBox", controls[0].Name);
Assert.Equal("NamedListBox", controls[1].Name);
Assert.Equal(typeof(TextBox).FullName, controls[0].TypeName);
Assert.Equal(typeof(ListBox).FullName, controls[1].TypeName);
Assert.Contains(typeof(TextBox).FullName!, controls[0].TypeName);
Assert.Contains(typeof(ListBox).FullName!, controls[1].TypeName);
}

[Fact]
Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.NameGenerator/Domain/ICodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ namespace Avalonia.NameGenerator.Domain;

internal interface ICodeGenerator
{
string GenerateCode(string className, string nameSpace, IXamlType XamlType, IEnumerable<ResolvedName> names);
string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names);
}
48 changes: 1 addition & 47 deletions src/Avalonia.NameGenerator/Domain/INameResolver.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System.Collections.Generic;
using System.Linq;

using XamlX.Ast;

namespace Avalonia.NameGenerator.Domain;
Expand All @@ -10,48 +8,4 @@ internal interface INameResolver
IReadOnlyList<ResolvedName> ResolveNames(XamlDocument xaml);
}

internal class ResolvedName
{
public ResolvedName(string typeName, string name, string fieldModifier, IReadOnlyList<string> genericTypeArguments)
{
TypeName = typeName;
Name = name;
FieldModifier = fieldModifier;
GenericTypeArguments = genericTypeArguments;
}

public string TypeName { get; }
public string Name { get; }
public string FieldModifier { get; }
public IReadOnlyList<string> GenericTypeArguments { get; }

public string PrintableTypeName =>
GenericTypeArguments.Count == 0
? $"global::{TypeName}"
: $@"global::{TypeName}<{string.Join(", ", GenericTypeArguments.Select(arg => $"global::{arg}"))}>";

public void Deconstruct(out string typeName, out string name, out string fieldModifier)
{
typeName = TypeName;
name = Name;
fieldModifier = FieldModifier;
}

public override bool Equals(object obj)
{
if (obj is not ResolvedName name)
{
return false;
}

return name.TypeName == TypeName
&& name.Name == Name
&& name.FieldModifier == FieldModifier
&& name.GenericTypeArguments.SequenceEqual(GenericTypeArguments);
}

public override int GetHashCode()
{
return (TypeName, Name, FieldModifier).GetHashCode();
}
}
internal record ResolvedName(string TypeName, string Name, string FieldModifier);
6 changes: 3 additions & 3 deletions src/Avalonia.NameGenerator/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public void Execute(GeneratorExecutionContext context)
{
var generator = CreateNameGenerator(context);
var partials = generator.GenerateNameReferences(context.AdditionalFiles);
foreach (var partial in partials) context.AddSource(partial.FileName, partial.Content);
foreach (var (fileName, content) in partials) context.AddSource(fileName, content);
}
catch (Exception exception)
{
Expand All @@ -33,7 +33,6 @@ private static INameGenerator CreateNameGenerator(GeneratorExecutionContext cont
{
var options = new GeneratorOptions(context);
var types = new RoslynTypeSystem((CSharpCompilation)context.Compilation);
var defaultFieldModifier = options.AvaloniaNameGeneratorDefaultFieldModifier.ToString().ToLowerInvariant();
ICodeGenerator generator = options.AvaloniaNameGeneratorBehavior switch {
Behavior.OnlyProperties => new OnlyPropertiesCodeGenerator(),
Behavior.InitializeComponent => new InitializeComponentCodeGenerator(types),
Expand All @@ -42,10 +41,11 @@ private static INameGenerator CreateNameGenerator(GeneratorExecutionContext cont

var compiler = MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute);
return new AvaloniaNameGenerator(
options.AvaloniaNameGeneratorViewFileNamingStrategy,
new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByPath),
new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByNamespace),
new XamlXViewResolver(types, compiler, true, type => ReportInvalidType(context, type)),
new XamlXNameResolver(defaultFieldModifier),
new XamlXNameResolver(options.AvaloniaNameGeneratorDefaultFieldModifier),
generator);
}

Expand Down
Loading