committed by
GitHub
92 changed files with 2951 additions and 69 deletions
@ -1,19 +1,12 @@ |
|||||
using Avalonia; |
using Avalonia.Controls; |
||||
using Avalonia.Controls; |
|
||||
using Avalonia.Markup.Xaml; |
|
||||
|
|
||||
namespace ControlCatalog.Pages |
namespace ControlCatalog.Pages |
||||
{ |
{ |
||||
public class RelativePanelPage : UserControl |
public partial class RelativePanelPage : UserControl |
||||
{ |
{ |
||||
public RelativePanelPage() |
public RelativePanelPage() |
||||
{ |
{ |
||||
this.InitializeComponent(); |
InitializeComponent(); |
||||
} |
|
||||
|
|
||||
private void InitializeComponent() |
|
||||
{ |
|
||||
AvaloniaXamlLoader.Load(this); |
|
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,7 @@ |
|||||
|
<Application xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="Generators.Sandbox.App"> |
||||
|
<Application.Styles> |
||||
|
<FluentTheme /> |
||||
|
</Application.Styles> |
||||
|
</Application> |
||||
@ -0,0 +1,20 @@ |
|||||
|
using Avalonia; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
using Generators.Sandbox.ViewModels; |
||||
|
|
||||
|
namespace Generators.Sandbox; |
||||
|
|
||||
|
public class App : Application |
||||
|
{ |
||||
|
public override void Initialize() => AvaloniaXamlLoader.Load(this); |
||||
|
|
||||
|
public override void OnFrameworkInitializationCompleted() |
||||
|
{ |
||||
|
var view = new Views.SignUpView |
||||
|
{ |
||||
|
ViewModel = new SignUpViewModel() |
||||
|
}; |
||||
|
view.Show(); |
||||
|
base.OnFrameworkInitializationCompleted(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Styling; |
||||
|
|
||||
|
namespace Generators.Sandbox.Controls; |
||||
|
|
||||
|
public class CustomTextBox : TextBox, IStyleable |
||||
|
{ |
||||
|
Type IStyleable.StyleKey => typeof(TextBox); |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:controls="using:Generators.Sandbox.Controls" |
||||
|
x:Class="Generators.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" /> |
||||
|
<TextBlock> |
||||
|
<TextBlock.Inlines> |
||||
|
<InlineCollection> |
||||
|
<Run x:Name="SignUpButtonDescription" /> |
||||
|
</InlineCollection> |
||||
|
</TextBlock.Inlines> |
||||
|
</TextBlock> |
||||
|
<Button Margin="0 10 0 5" |
||||
|
Content="Sign up" |
||||
|
x:Name="SignUpButton" /> |
||||
|
<TextBlock x:Name="CompoundValidation" |
||||
|
TextWrapping="Wrap" |
||||
|
Foreground="Red" |
||||
|
FontSize="12" /> |
||||
|
</StackPanel> |
||||
|
</UserControl> |
||||
@ -0,0 +1,54 @@ |
|||||
|
using System; |
||||
|
using System.Reactive.Disposables; |
||||
|
using Avalonia.ReactiveUI; |
||||
|
using Generators.Sandbox.ViewModels; |
||||
|
using ReactiveUI; |
||||
|
using ReactiveUI.Validation.Extensions; |
||||
|
using ReactiveUI.Validation.Formatters; |
||||
|
|
||||
|
namespace Generators.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"; |
||||
|
SignUpButtonDescription.Text = "Press the button below to sign up."; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
<PropertyGroup> |
||||
|
<OutputType>Exe</OutputType> |
||||
|
<TargetFramework>net6.0</TargetFramework> |
||||
|
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<AvaloniaResource Include="**\*.xaml"/> |
||||
|
<!-- Note this AdditionalFiles directive. --> |
||||
|
<AdditionalFiles Include="**\*.xaml"/> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="ReactiveUI.Validation" Version="3.0.22"/> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj"/> |
||||
|
<ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj"/> |
||||
|
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj"/> |
||||
|
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj"/> |
||||
|
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj"/> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<Import Project="..\..\build\BuildTargets.targets"/> |
||||
|
<Import Project="..\..\build\SourceGenerators.props"/> |
||||
|
</Project> |
||||
@ -0,0 +1,15 @@ |
|||||
|
using Avalonia; |
||||
|
using Avalonia.ReactiveUI; |
||||
|
|
||||
|
namespace Generators.Sandbox; |
||||
|
|
||||
|
internal static class Program |
||||
|
{ |
||||
|
public static void Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); |
||||
|
|
||||
|
private static AppBuilder BuildAvaloniaApp() |
||||
|
=> AppBuilder.Configure<App>() |
||||
|
.UseReactiveUI() |
||||
|
.UsePlatformDetect() |
||||
|
.LogToTrace(); |
||||
|
} |
||||
@ -0,0 +1,70 @@ |
|||||
|
using System.Reactive; |
||||
|
using ReactiveUI; |
||||
|
using ReactiveUI.Validation.Extensions; |
||||
|
using ReactiveUI.Validation.Helpers; |
||||
|
|
||||
|
namespace Generators.Sandbox.ViewModels; |
||||
|
|
||||
|
public class SignUpViewModel : ReactiveValidationObject |
||||
|
{ |
||||
|
private string _userName = string.Empty; |
||||
|
private string _password = string.Empty; |
||||
|
private string _confirmPassword = string.Empty; |
||||
|
|
||||
|
public SignUpViewModel() |
||||
|
{ |
||||
|
this.ValidationRule( |
||||
|
vm => vm.UserName, |
||||
|
name => !string.IsNullOrWhiteSpace(name), |
||||
|
"UserName is required."); |
||||
|
|
||||
|
this.ValidationRule( |
||||
|
vm => vm.Password, |
||||
|
password => !string.IsNullOrWhiteSpace(password), |
||||
|
"Password is required."); |
||||
|
|
||||
|
this.ValidationRule( |
||||
|
vm => vm.Password, |
||||
|
password => password?.Length > 2, |
||||
|
password => $"Password should be longer, current length: {password.Length}"); |
||||
|
|
||||
|
this.ValidationRule( |
||||
|
vm => vm.ConfirmPassword, |
||||
|
confirmation => !string.IsNullOrWhiteSpace(confirmation), |
||||
|
"Confirm password field is required."); |
||||
|
|
||||
|
var passwordsObservable = |
||||
|
this.WhenAnyValue( |
||||
|
x => x.Password, |
||||
|
x => x.ConfirmPassword, |
||||
|
(password, confirmation) => |
||||
|
password == confirmation); |
||||
|
|
||||
|
this.ValidationRule( |
||||
|
vm => vm.ConfirmPassword, |
||||
|
passwordsObservable, |
||||
|
"Passwords must match."); |
||||
|
|
||||
|
SignUp = ReactiveCommand.Create(() => {}, this.IsValid()); |
||||
|
} |
||||
|
|
||||
|
public ReactiveCommand<Unit, Unit> SignUp { get; } |
||||
|
|
||||
|
public string UserName |
||||
|
{ |
||||
|
get => _userName; |
||||
|
set => this.RaiseAndSetIfChanged(ref _userName, value); |
||||
|
} |
||||
|
|
||||
|
public string Password |
||||
|
{ |
||||
|
get => _password; |
||||
|
set => this.RaiseAndSetIfChanged(ref _password, value); |
||||
|
} |
||||
|
|
||||
|
public string ConfirmPassword |
||||
|
{ |
||||
|
get => _confirmPassword; |
||||
|
set => this.RaiseAndSetIfChanged(ref _confirmPassword, value); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:controls="using:Generators.Sandbox.Controls" |
||||
|
x:Class="Generators.Sandbox.Views.SignUpView"> |
||||
|
<StackPanel Margin="10"> |
||||
|
<TextBlock Text="Sign Up" /> |
||||
|
<controls:SignUpView x:Name="SignUpControl" /> |
||||
|
</StackPanel> |
||||
|
</Window> |
||||
@ -0,0 +1,28 @@ |
|||||
|
using System.Reactive.Disposables; |
||||
|
using Avalonia.ReactiveUI; |
||||
|
using Generators.Sandbox.ViewModels; |
||||
|
using ReactiveUI; |
||||
|
|
||||
|
namespace Generators.Sandbox.Views; |
||||
|
|
||||
|
/// <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 : ReactiveWindow<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.WhenAnyValue(view => view.ViewModel) |
||||
|
.BindTo(this, view => view.SignUpControl.ViewModel) |
||||
|
.DisposeWith(disposables); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
<IncludeBuildOutput>false</IncludeBuildOutput> |
||||
|
<PackageId>Avalonia.Generators</PackageId> |
||||
|
<DefineConstants>$(DefineConstants);XAMLX_INTERNAL</DefineConstants> |
||||
|
<IsPackable>true</IsPackable> |
||||
|
<IsRoslynComponent>true</IsRoslynComponent> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" /> |
||||
|
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<Compile Link="Compiler\XamlX\filename" Include="../../Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/src/XamlX/**/*.cs" /> |
||||
|
<Compile Remove="../../Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/src/XamlX/**/SreTypeSystem.cs" /> |
||||
|
<Compile Include="..\..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<None Include="Avalonia.Generators.props" Pack="true" PackagePath="buildTransitive/$(PackageId).props" /> |
||||
|
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup Label="InternalsVisibleTo"> |
||||
|
<InternalsVisibleTo Include="Avalonia.Generators.Tests, PublicKey=$(AvaloniaPublicKey)" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<Import Project="..\..\..\build\TrimmingEnable.props" /> |
||||
|
</Project> |
||||
@ -0,0 +1,22 @@ |
|||||
|
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
||||
|
<PropertyGroup> |
||||
|
<AvaloniaNameGeneratorIsEnabled Condition="'$(AvaloniaNameGeneratorIsEnabled)' == ''">true</AvaloniaNameGeneratorIsEnabled> |
||||
|
<AvaloniaNameGeneratorBehavior Condition="'$(AvaloniaNameGeneratorBehavior)' == ''">InitializeComponent</AvaloniaNameGeneratorBehavior> |
||||
|
<AvaloniaNameGeneratorDefaultFieldModifier Condition="'$(AvaloniaNameGeneratorDefaultFieldModifier)' == ''">internal</AvaloniaNameGeneratorDefaultFieldModifier> |
||||
|
<AvaloniaNameGeneratorFilterByPath Condition="'$(AvaloniaNameGeneratorFilterByPath)' == ''">*</AvaloniaNameGeneratorFilterByPath> |
||||
|
<AvaloniaNameGeneratorFilterByNamespace Condition="'$(AvaloniaNameGeneratorFilterByNamespace)' == ''">*</AvaloniaNameGeneratorFilterByNamespace> |
||||
|
</PropertyGroup> |
||||
|
<ItemGroup> |
||||
|
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="SourceItemGroup"/> |
||||
|
<CompilerVisibleProperty Include="AvaloniaNameGeneratorIsEnabled" /> |
||||
|
<CompilerVisibleProperty Include="AvaloniaNameGeneratorBehavior" /> |
||||
|
<CompilerVisibleProperty Include="AvaloniaNameGeneratorDefaultFieldModifier" /> |
||||
|
<CompilerVisibleProperty Include="AvaloniaNameGeneratorFilterByPath" /> |
||||
|
<CompilerVisibleProperty Include="AvaloniaNameGeneratorFilterByNamespace" /> |
||||
|
</ItemGroup> |
||||
|
<Target Name="_InjectAdditionalFiles" BeforeTargets="GenerateMSBuildEditorConfigFileShouldRun"> |
||||
|
<ItemGroup> |
||||
|
<AdditionalFiles Include="@(AvaloniaXaml)" SourceItemGroup="AvaloniaXaml" /> |
||||
|
</ItemGroup> |
||||
|
</Target> |
||||
|
</Project> |
||||
@ -0,0 +1,9 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using XamlX.TypeSystem; |
||||
|
|
||||
|
namespace Avalonia.Generators.Common.Domain; |
||||
|
|
||||
|
internal interface ICodeGenerator |
||||
|
{ |
||||
|
string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names); |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
namespace Avalonia.Generators.Common.Domain; |
||||
|
|
||||
|
internal interface IGlobPattern |
||||
|
{ |
||||
|
bool Matches(string str); |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using XamlX.Ast; |
||||
|
|
||||
|
namespace Avalonia.Generators.Common.Domain; |
||||
|
|
||||
|
internal enum NamedFieldModifier |
||||
|
{ |
||||
|
Public = 0, |
||||
|
Private = 1, |
||||
|
Internal = 2, |
||||
|
Protected = 3, |
||||
|
} |
||||
|
|
||||
|
internal interface INameResolver |
||||
|
{ |
||||
|
IReadOnlyList<ResolvedName> ResolveNames(XamlDocument xaml); |
||||
|
} |
||||
|
|
||||
|
internal record ResolvedName(string TypeName, string Name, string FieldModifier); |
||||
@ -0,0 +1,11 @@ |
|||||
|
using XamlX.Ast; |
||||
|
using XamlX.TypeSystem; |
||||
|
|
||||
|
namespace Avalonia.Generators.Common.Domain; |
||||
|
|
||||
|
internal interface IViewResolver |
||||
|
{ |
||||
|
ResolvedView ResolveView(string xaml); |
||||
|
} |
||||
|
|
||||
|
internal record ResolvedView(string ClassName, IXamlType XamlType, string Namespace, XamlDocument Xaml); |
||||
@ -0,0 +1,18 @@ |
|||||
|
using System.Text.RegularExpressions; |
||||
|
using Avalonia.Generators.Common.Domain; |
||||
|
|
||||
|
namespace Avalonia.Generators.Common; |
||||
|
|
||||
|
internal class GlobPattern : IGlobPattern |
||||
|
{ |
||||
|
private const RegexOptions GlobOptions = RegexOptions.IgnoreCase | RegexOptions.Singleline; |
||||
|
private readonly Regex _regex; |
||||
|
|
||||
|
public GlobPattern(string pattern) |
||||
|
{ |
||||
|
var expression = "^" + Regex.Escape(pattern).Replace(@"\*", ".*").Replace(@"\?", ".") + "$"; |
||||
|
_regex = new Regex(expression, GlobOptions); |
||||
|
} |
||||
|
|
||||
|
public bool Matches(string str) => _regex.IsMatch(str); |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Generators.Common.Domain; |
||||
|
|
||||
|
namespace Avalonia.Generators.Common; |
||||
|
|
||||
|
internal class GlobPatternGroup : IGlobPattern |
||||
|
{ |
||||
|
private readonly GlobPattern[] _patterns; |
||||
|
|
||||
|
public GlobPatternGroup(IEnumerable<string> patterns) => |
||||
|
_patterns = patterns |
||||
|
.Select(pattern => new GlobPattern(pattern)) |
||||
|
.ToArray(); |
||||
|
|
||||
|
public bool Matches(string str) => _patterns.Any(pattern => pattern.Matches(str)); |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
using System.Linq; |
||||
|
using XamlX.TypeSystem; |
||||
|
|
||||
|
namespace Avalonia.Generators.Common; |
||||
|
|
||||
|
internal static class ResolverExtensions |
||||
|
{ |
||||
|
public static bool IsAvaloniaStyledElement(this IXamlType clrType) => |
||||
|
clrType.HasStyledElementBaseType() || |
||||
|
clrType.HasIStyledElementInterface(); |
||||
|
|
||||
|
private static bool HasStyledElementBaseType(this IXamlType clrType) |
||||
|
{ |
||||
|
// Check for the base type since IStyledElement interface is removed.
|
||||
|
// https://github.com/AvaloniaUI/Avalonia/pull/9553
|
||||
|
if (clrType.FullName == "Avalonia.StyledElement") |
||||
|
return true; |
||||
|
return clrType.BaseType != null && IsAvaloniaStyledElement(clrType.BaseType); |
||||
|
} |
||||
|
|
||||
|
private static bool HasIStyledElementInterface(this IXamlType clrType) => |
||||
|
clrType.Interfaces.Any(abstraction => |
||||
|
abstraction.IsInterface && |
||||
|
abstraction.FullName == "Avalonia.IStyledElement"); |
||||
|
} |
||||
@ -0,0 +1,92 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Collections.Immutable; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Generators.Common.Domain; |
||||
|
using XamlX; |
||||
|
using XamlX.Ast; |
||||
|
|
||||
|
namespace Avalonia.Generators.Common; |
||||
|
|
||||
|
internal class XamlXNameResolver : INameResolver, IXamlAstVisitor |
||||
|
{ |
||||
|
private readonly List<ResolvedName> _items = new(); |
||||
|
private readonly string _defaultFieldModifier; |
||||
|
|
||||
|
public XamlXNameResolver(NamedFieldModifier namedFieldModifier = NamedFieldModifier.Internal) |
||||
|
{ |
||||
|
_defaultFieldModifier = namedFieldModifier.ToString().ToLowerInvariant(); |
||||
|
} |
||||
|
|
||||
|
public IReadOnlyList<ResolvedName> ResolveNames(XamlDocument xaml) |
||||
|
{ |
||||
|
_items.Clear(); |
||||
|
xaml.Root.Visit(this); |
||||
|
xaml.Root.VisitChildren(this); |
||||
|
return _items; |
||||
|
} |
||||
|
|
||||
|
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) |
||||
|
{ |
||||
|
if (node is not XamlAstObjectNode objectNode) |
||||
|
return node; |
||||
|
|
||||
|
var clrType = objectNode.Type.GetClrType(); |
||||
|
if (!clrType.IsAvaloniaStyledElement()) |
||||
|
return node; |
||||
|
|
||||
|
foreach (var child in objectNode.Children) |
||||
|
{ |
||||
|
if (child is XamlAstXamlPropertyValueNode propertyValueNode && |
||||
|
propertyValueNode.Property is XamlAstNamePropertyReference namedProperty && |
||||
|
namedProperty.Name == "Name" && |
||||
|
propertyValueNode.Values.Count > 0 && |
||||
|
propertyValueNode.Values[0] is XamlAstTextNode text) |
||||
|
{ |
||||
|
var fieldModifier = TryGetFieldModifier(objectNode); |
||||
|
var typeName = $@"{clrType.Namespace}.{clrType.Name}"; |
||||
|
var typeAgs = clrType.GenericArguments.Select(arg => arg.FullName).ToImmutableList(); |
||||
|
var genericTypeName = typeAgs.Count == 0 |
||||
|
? $"global::{typeName}" |
||||
|
: $@"global::{typeName}<{string.Join(", ", typeAgs.Select(arg => $"global::{arg}"))}>"; |
||||
|
|
||||
|
var resolvedName = new ResolvedName(genericTypeName, text.Text, fieldModifier); |
||||
|
if (_items.Contains(resolvedName)) |
||||
|
continue; |
||||
|
_items.Add(resolvedName); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return node; |
||||
|
} |
||||
|
|
||||
|
void IXamlAstVisitor.Push(IXamlAstNode node) { } |
||||
|
|
||||
|
void IXamlAstVisitor.Pop() { } |
||||
|
|
||||
|
private string TryGetFieldModifier(XamlAstObjectNode objectNode) |
||||
|
{ |
||||
|
// We follow Xamarin.Forms API behavior in terms of x:FieldModifier here:
|
||||
|
// https://docs.microsoft.com/en-us/xamarin/xamarin-forms/xaml/field-modifiers
|
||||
|
// However, by default we use 'internal' field modifier here for generated
|
||||
|
// x:Name references for historical purposes and WPF compatibility.
|
||||
|
//
|
||||
|
var fieldModifierType = objectNode |
||||
|
.Children |
||||
|
.OfType<XamlAstXmlDirective>() |
||||
|
.Where(dir => dir.Name == "FieldModifier" && dir.Namespace == XamlNamespaces.Xaml2006) |
||||
|
.Select(dir => dir.Values[0]) |
||||
|
.OfType<XamlAstTextNode>() |
||||
|
.Select(txt => txt.Text) |
||||
|
.FirstOrDefault(); |
||||
|
|
||||
|
return fieldModifierType?.ToLowerInvariant() switch |
||||
|
{ |
||||
|
"private" => "private", |
||||
|
"public" => "public", |
||||
|
"protected" => "protected", |
||||
|
"internal" => "internal", |
||||
|
"notpublic" => "internal", |
||||
|
_ => _defaultFieldModifier |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,100 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Generators.Common.Domain; |
||||
|
using Avalonia.Generators.Compiler; |
||||
|
using XamlX; |
||||
|
using XamlX.Ast; |
||||
|
using XamlX.Parsers; |
||||
|
|
||||
|
namespace Avalonia.Generators.Common; |
||||
|
|
||||
|
internal class XamlXViewResolver : IViewResolver, IXamlAstVisitor |
||||
|
{ |
||||
|
private readonly RoslynTypeSystem _typeSystem; |
||||
|
private readonly MiniCompiler _compiler; |
||||
|
private readonly bool _checkTypeValidity; |
||||
|
private readonly Action<string> _onTypeInvalid; |
||||
|
private readonly Action<Exception> _onUnhandledError; |
||||
|
|
||||
|
private ResolvedView _resolvedClass; |
||||
|
private XamlDocument _xaml; |
||||
|
|
||||
|
public XamlXViewResolver( |
||||
|
RoslynTypeSystem typeSystem, |
||||
|
MiniCompiler compiler, |
||||
|
bool checkTypeValidity = false, |
||||
|
Action<string> onTypeInvalid = null, |
||||
|
Action<Exception> onUnhandledError = null) |
||||
|
{ |
||||
|
_checkTypeValidity = checkTypeValidity; |
||||
|
_onTypeInvalid = onTypeInvalid; |
||||
|
_onUnhandledError = onUnhandledError; |
||||
|
_typeSystem = typeSystem; |
||||
|
_compiler = compiler; |
||||
|
} |
||||
|
|
||||
|
public ResolvedView ResolveView(string xaml) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
_resolvedClass = null; |
||||
|
_xaml = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string> |
||||
|
{ |
||||
|
{XamlNamespaces.Blend2008, XamlNamespaces.Blend2008} |
||||
|
}); |
||||
|
|
||||
|
_compiler.Transform(_xaml); |
||||
|
_xaml.Root.Visit(this); |
||||
|
_xaml.Root.VisitChildren(this); |
||||
|
return _resolvedClass; |
||||
|
} |
||||
|
catch (Exception exception) |
||||
|
{ |
||||
|
_onUnhandledError?.Invoke(exception); |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) |
||||
|
{ |
||||
|
if (node is not XamlAstObjectNode objectNode) |
||||
|
return node; |
||||
|
|
||||
|
var clrType = objectNode.Type.GetClrType(); |
||||
|
if (!clrType.IsAvaloniaStyledElement()) |
||||
|
return node; |
||||
|
|
||||
|
foreach (var child in objectNode.Children) |
||||
|
{ |
||||
|
if (child is XamlAstXmlDirective directive && |
||||
|
directive.Name == "Class" && |
||||
|
directive.Namespace == XamlNamespaces.Xaml2006 && |
||||
|
directive.Values[0] is XamlAstTextNode text) |
||||
|
{ |
||||
|
if (_checkTypeValidity) |
||||
|
{ |
||||
|
var existingType = _typeSystem.FindType(text.Text); |
||||
|
if (existingType == null) |
||||
|
{ |
||||
|
_onTypeInvalid?.Invoke(text.Text); |
||||
|
return node; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var split = text.Text.Split('.'); |
||||
|
var nameSpace = string.Join(".", split.Take(split.Length - 1)); |
||||
|
var className = split.Last(); |
||||
|
|
||||
|
_resolvedClass = new ResolvedView(className, clrType, nameSpace, _xaml); |
||||
|
return node; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return node; |
||||
|
} |
||||
|
|
||||
|
void IXamlAstVisitor.Push(IXamlAstNode node) { } |
||||
|
|
||||
|
void IXamlAstVisitor.Pop() { } |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
using XamlX.Ast; |
||||
|
using XamlX.Transform; |
||||
|
|
||||
|
namespace Avalonia.Generators.Compiler; |
||||
|
|
||||
|
internal class DataTemplateTransformer : IXamlAstTransformer |
||||
|
{ |
||||
|
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) |
||||
|
{ |
||||
|
if (node is XamlAstObjectNode objectNode && |
||||
|
objectNode.Type is XamlAstXmlTypeReference typeReference && |
||||
|
(typeReference.Name == "DataTemplate" || |
||||
|
typeReference.Name == "ControlTemplate")) |
||||
|
objectNode.Children.Clear(); |
||||
|
return node; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using XamlX.Compiler; |
||||
|
using XamlX.Emit; |
||||
|
using XamlX.Transform; |
||||
|
using XamlX.Transform.Transformers; |
||||
|
using XamlX.TypeSystem; |
||||
|
|
||||
|
namespace Avalonia.Generators.Compiler; |
||||
|
|
||||
|
internal sealed class MiniCompiler : XamlCompiler<object, IXamlEmitResult> |
||||
|
{ |
||||
|
public const string AvaloniaXmlnsDefinitionAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute"; |
||||
|
|
||||
|
public static MiniCompiler CreateDefault(RoslynTypeSystem typeSystem, params string[] additionalTypes) |
||||
|
{ |
||||
|
var mappings = new XamlLanguageTypeMappings(typeSystem); |
||||
|
foreach (var additionalType in additionalTypes) |
||||
|
mappings.XmlnsAttributes.Add(typeSystem.GetType(additionalType)); |
||||
|
|
||||
|
var configuration = new TransformerConfiguration( |
||||
|
typeSystem, |
||||
|
typeSystem.Assemblies.First(), |
||||
|
mappings); |
||||
|
return new MiniCompiler(configuration); |
||||
|
} |
||||
|
|
||||
|
private MiniCompiler(TransformerConfiguration configuration) |
||||
|
: base(configuration, new XamlLanguageEmitMappings<object, IXamlEmitResult>(), false) |
||||
|
{ |
||||
|
Transformers.Add(new NameDirectiveTransformer()); |
||||
|
Transformers.Add(new DataTemplateTransformer()); |
||||
|
Transformers.Add(new KnownDirectivesTransformer()); |
||||
|
Transformers.Add(new XamlIntrinsicsTransformer()); |
||||
|
Transformers.Add(new XArgumentsTransformer()); |
||||
|
Transformers.Add(new TypeReferenceResolver()); |
||||
|
} |
||||
|
|
||||
|
protected override XamlEmitContext<object, IXamlEmitResult> InitCodeGen( |
||||
|
IFileSource file, |
||||
|
Func<string, IXamlType, |
||||
|
IXamlTypeBuilder<object>> createSubType, |
||||
|
Func<string, IXamlType, IEnumerable<IXamlType>, |
||||
|
IXamlTypeBuilder<object>> createDelegateType, |
||||
|
object codeGen, |
||||
|
XamlRuntimeContext<object, IXamlEmitResult> context, |
||||
|
bool needContextLocal) => |
||||
|
throw new NotSupportedException(); |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
using XamlX; |
||||
|
using XamlX.Ast; |
||||
|
using XamlX.Transform; |
||||
|
|
||||
|
namespace Avalonia.Generators.Compiler; |
||||
|
|
||||
|
internal class NameDirectiveTransformer : IXamlAstTransformer |
||||
|
{ |
||||
|
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) |
||||
|
{ |
||||
|
if (node is not XamlAstObjectNode objectNode) |
||||
|
return node; |
||||
|
|
||||
|
for (var index = 0; index < objectNode.Children.Count; index++) |
||||
|
{ |
||||
|
var child = objectNode.Children[index]; |
||||
|
if (child is XamlAstXmlDirective directive && |
||||
|
directive.Namespace == XamlNamespaces.Xaml2006 && |
||||
|
directive.Name == "Name") |
||||
|
objectNode.Children[index] = new XamlAstXamlPropertyValueNode( |
||||
|
directive, |
||||
|
new XamlAstNamePropertyReference(directive, objectNode.Type, "Name", objectNode.Type), |
||||
|
directive.Values); |
||||
|
} |
||||
|
|
||||
|
return node; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,276 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Microsoft.CodeAnalysis; |
||||
|
using Microsoft.CodeAnalysis.CSharp; |
||||
|
using XamlX.TypeSystem; |
||||
|
|
||||
|
namespace Avalonia.Generators.Compiler; |
||||
|
|
||||
|
internal class RoslynTypeSystem : IXamlTypeSystem |
||||
|
{ |
||||
|
private readonly List<IXamlAssembly> _assemblies = new(); |
||||
|
|
||||
|
public RoslynTypeSystem(CSharpCompilation compilation) |
||||
|
{ |
||||
|
_assemblies.Add(new RoslynAssembly(compilation.Assembly)); |
||||
|
|
||||
|
var assemblySymbols = compilation |
||||
|
.References |
||||
|
.Select(compilation.GetAssemblyOrModuleSymbol) |
||||
|
.OfType<IAssemblySymbol>() |
||||
|
.Select(assembly => new RoslynAssembly(assembly)) |
||||
|
.ToList(); |
||||
|
|
||||
|
_assemblies.AddRange(assemblySymbols); |
||||
|
} |
||||
|
|
||||
|
public IEnumerable<IXamlAssembly> Assemblies => _assemblies; |
||||
|
|
||||
|
public IXamlAssembly FindAssembly(string name) => |
||||
|
Assemblies |
||||
|
.FirstOrDefault(a => string.Equals(a.Name, name, StringComparison.OrdinalIgnoreCase)); |
||||
|
|
||||
|
public IXamlType FindType(string name) => |
||||
|
_assemblies |
||||
|
.Select(assembly => assembly.FindType(name)) |
||||
|
.FirstOrDefault(type => type != null); |
||||
|
|
||||
|
public IXamlType FindType(string name, string assembly) => |
||||
|
_assemblies |
||||
|
.Select(assemblyInstance => assemblyInstance.FindType(name)) |
||||
|
.FirstOrDefault(type => type != null); |
||||
|
} |
||||
|
|
||||
|
internal class RoslynAssembly : IXamlAssembly |
||||
|
{ |
||||
|
private readonly IAssemblySymbol _symbol; |
||||
|
|
||||
|
public RoslynAssembly(IAssemblySymbol symbol) => _symbol = symbol; |
||||
|
|
||||
|
public bool Equals(IXamlAssembly other) => |
||||
|
other is RoslynAssembly roslynAssembly && |
||||
|
SymbolEqualityComparer.Default.Equals(_symbol, roslynAssembly._symbol); |
||||
|
|
||||
|
public string Name => _symbol.Name; |
||||
|
|
||||
|
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes => |
||||
|
_symbol.GetAttributes() |
||||
|
.Select(data => new RoslynAttribute(data, this)) |
||||
|
.ToList(); |
||||
|
|
||||
|
public IXamlType FindType(string fullName) |
||||
|
{ |
||||
|
var type = _symbol.GetTypeByMetadataName(fullName); |
||||
|
return type is null ? null : new RoslynType(type, this); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal class RoslynAttribute : IXamlCustomAttribute |
||||
|
{ |
||||
|
private readonly AttributeData _data; |
||||
|
private readonly RoslynAssembly _assembly; |
||||
|
|
||||
|
public RoslynAttribute(AttributeData data, RoslynAssembly assembly) |
||||
|
{ |
||||
|
_data = data; |
||||
|
_assembly = assembly; |
||||
|
} |
||||
|
|
||||
|
public bool Equals(IXamlCustomAttribute other) => |
||||
|
other is RoslynAttribute attribute && |
||||
|
_data == attribute._data; |
||||
|
|
||||
|
public IXamlType Type => new RoslynType(_data.AttributeClass, _assembly); |
||||
|
|
||||
|
public List<object> Parameters => |
||||
|
_data.ConstructorArguments |
||||
|
.Select(argument => argument.Value) |
||||
|
.ToList(); |
||||
|
|
||||
|
public Dictionary<string, object> Properties => |
||||
|
_data.NamedArguments.ToDictionary( |
||||
|
pair => pair.Key, |
||||
|
pair => pair.Value.Value); |
||||
|
} |
||||
|
|
||||
|
internal class RoslynType : IXamlType |
||||
|
{ |
||||
|
private static readonly SymbolDisplayFormat SymbolDisplayFormat = new SymbolDisplayFormat( |
||||
|
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, |
||||
|
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | |
||||
|
SymbolDisplayGenericsOptions.IncludeTypeConstraints | |
||||
|
SymbolDisplayGenericsOptions.IncludeVariance); |
||||
|
|
||||
|
private readonly RoslynAssembly _assembly; |
||||
|
private readonly INamedTypeSymbol _symbol; |
||||
|
|
||||
|
public RoslynType(INamedTypeSymbol symbol, RoslynAssembly assembly) |
||||
|
{ |
||||
|
_symbol = symbol; |
||||
|
_assembly = assembly; |
||||
|
} |
||||
|
|
||||
|
public bool Equals(IXamlType other) => |
||||
|
other is RoslynType roslynType && |
||||
|
SymbolEqualityComparer.Default.Equals(_symbol, roslynType._symbol); |
||||
|
|
||||
|
public object Id => _symbol; |
||||
|
|
||||
|
public string Name => _symbol.Name; |
||||
|
|
||||
|
public string Namespace => _symbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat); |
||||
|
|
||||
|
public string FullName => $"{Namespace}.{Name}"; |
||||
|
|
||||
|
public IXamlAssembly Assembly => _assembly; |
||||
|
|
||||
|
public IReadOnlyList<IXamlProperty> Properties => |
||||
|
_symbol.GetMembers() |
||||
|
.Where(member => member.Kind == SymbolKind.Property) |
||||
|
.OfType<IPropertySymbol>() |
||||
|
.Select(property => new RoslynProperty(property, _assembly)) |
||||
|
.ToList(); |
||||
|
|
||||
|
public IReadOnlyList<IXamlEventInfo> Events { get; } = new List<IXamlEventInfo>(); |
||||
|
|
||||
|
public IReadOnlyList<IXamlField> Fields { get; } = new List<IXamlField>(); |
||||
|
|
||||
|
public IReadOnlyList<IXamlMethod> Methods { get; } = new List<IXamlMethod>(); |
||||
|
|
||||
|
public IReadOnlyList<IXamlConstructor> Constructors => |
||||
|
_symbol.Constructors |
||||
|
.Select(method => new RoslynConstructor(method, _assembly)) |
||||
|
.ToList(); |
||||
|
|
||||
|
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes { get; } = new List<IXamlCustomAttribute>(); |
||||
|
|
||||
|
public IReadOnlyList<IXamlType> GenericArguments { get; private set; } = new List<IXamlType>(); |
||||
|
|
||||
|
public bool IsAssignableFrom(IXamlType type) => type == this; |
||||
|
|
||||
|
public IXamlType MakeGenericType(IReadOnlyList<IXamlType> typeArguments) |
||||
|
{ |
||||
|
GenericArguments = typeArguments; |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
public IXamlType GenericTypeDefinition => this; |
||||
|
|
||||
|
public bool IsArray => false; |
||||
|
|
||||
|
public IXamlType ArrayElementType { get; } = null; |
||||
|
|
||||
|
public IXamlType MakeArrayType(int dimensions) => null; |
||||
|
|
||||
|
public IXamlType BaseType => _symbol.BaseType == null ? null : new RoslynType(_symbol.BaseType, _assembly); |
||||
|
|
||||
|
public bool IsValueType { get; } = false; |
||||
|
|
||||
|
public bool IsEnum { get; } = false; |
||||
|
|
||||
|
public IReadOnlyList<IXamlType> Interfaces => |
||||
|
_symbol.AllInterfaces |
||||
|
.Select(abstraction => new RoslynType(abstraction, _assembly)) |
||||
|
.ToList(); |
||||
|
|
||||
|
public bool IsInterface => _symbol.IsAbstract; |
||||
|
|
||||
|
public IXamlType GetEnumUnderlyingType() => null; |
||||
|
|
||||
|
public IReadOnlyList<IXamlType> GenericParameters { get; } = new List<IXamlType>(); |
||||
|
} |
||||
|
|
||||
|
internal class RoslynConstructor : IXamlConstructor |
||||
|
{ |
||||
|
private readonly IMethodSymbol _symbol; |
||||
|
private readonly RoslynAssembly _assembly; |
||||
|
|
||||
|
public RoslynConstructor(IMethodSymbol symbol, RoslynAssembly assembly) |
||||
|
{ |
||||
|
_symbol = symbol; |
||||
|
_assembly = assembly; |
||||
|
} |
||||
|
|
||||
|
public bool Equals(IXamlConstructor other) => |
||||
|
other is RoslynConstructor roslynConstructor && |
||||
|
SymbolEqualityComparer.Default.Equals(_symbol, roslynConstructor._symbol); |
||||
|
|
||||
|
public bool IsPublic => true; |
||||
|
|
||||
|
public bool IsStatic => false; |
||||
|
|
||||
|
public IReadOnlyList<IXamlType> Parameters => |
||||
|
_symbol.Parameters |
||||
|
.Select(parameter => parameter.Type) |
||||
|
.OfType<INamedTypeSymbol>() |
||||
|
.Select(type => new RoslynType(type, _assembly)) |
||||
|
.ToList(); |
||||
|
} |
||||
|
|
||||
|
internal class RoslynProperty : IXamlProperty |
||||
|
{ |
||||
|
private readonly IPropertySymbol _symbol; |
||||
|
private readonly RoslynAssembly _assembly; |
||||
|
|
||||
|
public RoslynProperty(IPropertySymbol symbol, RoslynAssembly assembly) |
||||
|
{ |
||||
|
_symbol = symbol; |
||||
|
_assembly = assembly; |
||||
|
} |
||||
|
|
||||
|
public bool Equals(IXamlProperty other) => |
||||
|
other is RoslynProperty roslynProperty && |
||||
|
SymbolEqualityComparer.Default.Equals(_symbol, roslynProperty._symbol); |
||||
|
|
||||
|
public string Name => _symbol.Name; |
||||
|
|
||||
|
public IXamlType PropertyType => |
||||
|
_symbol.Type is INamedTypeSymbol namedTypeSymbol |
||||
|
? new RoslynType(namedTypeSymbol, _assembly) |
||||
|
: null; |
||||
|
|
||||
|
public IXamlMethod Getter => _symbol.GetMethod == null ? null : new RoslynMethod(_symbol.GetMethod, _assembly); |
||||
|
|
||||
|
public IXamlMethod Setter => _symbol.SetMethod == null ? null : new RoslynMethod(_symbol.SetMethod, _assembly); |
||||
|
|
||||
|
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes { get; } = new List<IXamlCustomAttribute>(); |
||||
|
|
||||
|
public IReadOnlyList<IXamlType> IndexerParameters { get; } = new List<IXamlType>(); |
||||
|
} |
||||
|
|
||||
|
internal class RoslynMethod : IXamlMethod |
||||
|
{ |
||||
|
private readonly IMethodSymbol _symbol; |
||||
|
private readonly RoslynAssembly _assembly; |
||||
|
|
||||
|
public RoslynMethod(IMethodSymbol symbol, RoslynAssembly assembly) |
||||
|
{ |
||||
|
_symbol = symbol; |
||||
|
_assembly = assembly; |
||||
|
} |
||||
|
|
||||
|
public bool Equals(IXamlMethod other) => |
||||
|
other is RoslynMethod roslynMethod && |
||||
|
SymbolEqualityComparer.Default.Equals(roslynMethod._symbol, _symbol); |
||||
|
|
||||
|
public string Name => _symbol.Name; |
||||
|
|
||||
|
public bool IsPublic => true; |
||||
|
|
||||
|
public bool IsStatic => false; |
||||
|
|
||||
|
public IXamlType ReturnType => new RoslynType((INamedTypeSymbol) _symbol.ReturnType, _assembly); |
||||
|
|
||||
|
public IReadOnlyList<IXamlType> Parameters => |
||||
|
_symbol.Parameters.Select(parameter => parameter.Type) |
||||
|
.OfType<INamedTypeSymbol>() |
||||
|
.Select(type => new RoslynType(type, _assembly)) |
||||
|
.ToList(); |
||||
|
|
||||
|
public IXamlType DeclaringType => new RoslynType((INamedTypeSymbol)_symbol.ReceiverType, _assembly); |
||||
|
|
||||
|
public IXamlMethod MakeGenericMethod(IReadOnlyList<IXamlType> typeArguments) => null; |
||||
|
|
||||
|
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes { get; } = new List<IXamlCustomAttribute>(); |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
using System; |
||||
|
using Microsoft.CodeAnalysis; |
||||
|
|
||||
|
namespace Avalonia.Generators; |
||||
|
|
||||
|
internal static class GeneratorContextExtensions |
||||
|
{ |
||||
|
private const string UnhandledErrorDescriptorId = "AXN0002"; |
||||
|
private const string InvalidTypeDescriptorId = "AXN0001"; |
||||
|
|
||||
|
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 void ReportNameGeneratorUnhandledError(this GeneratorExecutionContext context, Exception error) => |
||||
|
context.Report(UnhandledErrorDescriptorId, |
||||
|
"Unhandled exception occured while generating typed Name references. " + |
||||
|
"Please file an issue: https://github.com/avaloniaui/Avalonia.Generators", |
||||
|
error.ToString()); |
||||
|
|
||||
|
public static void ReportNameGeneratorInvalidType(this GeneratorExecutionContext context, string typeName) => |
||||
|
context.Report(InvalidTypeDescriptorId, |
||||
|
$"Avalonia x:Name generator was unable to generate names for type '{typeName}'. " + |
||||
|
$"The type '{typeName}' does not exist in the assembly."); |
||||
|
|
||||
|
private static void Report(this GeneratorExecutionContext context, string id, string title, string message = null) => |
||||
|
context.ReportDiagnostic( |
||||
|
Diagnostic.Create( |
||||
|
new DiagnosticDescriptor(id, title, message ?? title, "Usage", DiagnosticSeverity.Error, true), |
||||
|
Location.None)); |
||||
|
} |
||||
@ -0,0 +1,71 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Generators.Common.Domain; |
||||
|
using Avalonia.Generators.NameGenerator; |
||||
|
using Microsoft.CodeAnalysis; |
||||
|
|
||||
|
namespace Avalonia.Generators; |
||||
|
|
||||
|
// When update these enum values, don't forget to update Avalonia.Generators.props.
|
||||
|
internal enum BuildProperties |
||||
|
{ |
||||
|
AvaloniaNameGeneratorIsEnabled = 0, |
||||
|
AvaloniaNameGeneratorBehavior = 1, |
||||
|
AvaloniaNameGeneratorDefaultFieldModifier = 2, |
||||
|
AvaloniaNameGeneratorFilterByPath = 3, |
||||
|
AvaloniaNameGeneratorFilterByNamespace = 4, |
||||
|
AvaloniaNameGeneratorViewFileNamingStrategy = 5, |
||||
|
|
||||
|
// TODO add other generators properties here.
|
||||
|
} |
||||
|
|
||||
|
internal class GeneratorOptions |
||||
|
{ |
||||
|
private readonly GeneratorExecutionContext _context; |
||||
|
|
||||
|
public GeneratorOptions(GeneratorExecutionContext context) => _context = context; |
||||
|
|
||||
|
public bool AvaloniaNameGeneratorIsEnabled => GetBoolProperty( |
||||
|
BuildProperties.AvaloniaNameGeneratorIsEnabled, |
||||
|
true); |
||||
|
|
||||
|
public Behavior AvaloniaNameGeneratorBehavior => GetEnumProperty( |
||||
|
BuildProperties.AvaloniaNameGeneratorBehavior, |
||||
|
Behavior.InitializeComponent); |
||||
|
|
||||
|
public NamedFieldModifier AvaloniaNameGeneratorClassFieldModifier => GetEnumProperty( |
||||
|
BuildProperties.AvaloniaNameGeneratorDefaultFieldModifier, |
||||
|
NamedFieldModifier.Internal); |
||||
|
|
||||
|
public ViewFileNamingStrategy AvaloniaNameGeneratorViewFileNamingStrategy => GetEnumProperty( |
||||
|
BuildProperties.AvaloniaNameGeneratorViewFileNamingStrategy, |
||||
|
ViewFileNamingStrategy.NamespaceAndClassName); |
||||
|
|
||||
|
public string[] AvaloniaNameGeneratorFilterByPath => GetStringArrayProperty( |
||||
|
BuildProperties.AvaloniaNameGeneratorFilterByPath, |
||||
|
"*"); |
||||
|
|
||||
|
public string[] AvaloniaNameGeneratorFilterByNamespace => GetStringArrayProperty( |
||||
|
BuildProperties.AvaloniaNameGeneratorFilterByNamespace, |
||||
|
"*"); |
||||
|
|
||||
|
private string[] GetStringArrayProperty(BuildProperties name, string defaultValue) |
||||
|
{ |
||||
|
var key = name.ToString(); |
||||
|
var value = _context.GetMsBuildProperty(key, defaultValue); |
||||
|
return value.Contains(";") ? value.Split(';') : new[] {value}; |
||||
|
} |
||||
|
|
||||
|
private TEnum GetEnumProperty<TEnum>(BuildProperties name, TEnum defaultValue) where TEnum : struct |
||||
|
{ |
||||
|
var key = name.ToString(); |
||||
|
var value = _context.GetMsBuildProperty(key, defaultValue.ToString()); |
||||
|
return Enum.TryParse(value, true, out TEnum behavior) ? behavior : defaultValue; |
||||
|
} |
||||
|
|
||||
|
private bool GetBoolProperty(BuildProperties name, bool defaultValue) |
||||
|
{ |
||||
|
var key = name.ToString(); |
||||
|
var value = _context.GetMsBuildProperty(key, defaultValue.ToString()); |
||||
|
return bool.TryParse(value, out var result) ? result : defaultValue; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Generators.Common.Domain; |
||||
|
using Microsoft.CodeAnalysis; |
||||
|
|
||||
|
namespace Avalonia.Generators.NameGenerator; |
||||
|
|
||||
|
internal class AvaloniaNameGenerator : INameGenerator |
||||
|
{ |
||||
|
private readonly ViewFileNamingStrategy _naming; |
||||
|
private readonly IGlobPattern _pathPattern; |
||||
|
private readonly IGlobPattern _namespacePattern; |
||||
|
private readonly IViewResolver _classes; |
||||
|
private readonly INameResolver _names; |
||||
|
private readonly ICodeGenerator _code; |
||||
|
|
||||
|
public AvaloniaNameGenerator( |
||||
|
ViewFileNamingStrategy naming, |
||||
|
IGlobPattern pathPattern, |
||||
|
IGlobPattern namespacePattern, |
||||
|
IViewResolver classes, |
||||
|
INameResolver names, |
||||
|
ICodeGenerator code) |
||||
|
{ |
||||
|
_naming = naming; |
||||
|
_pathPattern = pathPattern; |
||||
|
_namespacePattern = namespacePattern; |
||||
|
_classes = classes; |
||||
|
_names = names; |
||||
|
_code = code; |
||||
|
} |
||||
|
|
||||
|
public IReadOnlyList<GeneratedPartialClass> GenerateNameReferences(IEnumerable<AdditionalText> additionalFiles) |
||||
|
{ |
||||
|
var resolveViews = |
||||
|
from file in additionalFiles |
||||
|
where (file.Path.EndsWith(".xaml") || |
||||
|
file.Path.EndsWith(".paml") || |
||||
|
file.Path.EndsWith(".axaml")) && |
||||
|
_pathPattern.Matches(file.Path) |
||||
|
let xaml = file.GetText()!.ToString() |
||||
|
let view = _classes.ResolveView(xaml) |
||||
|
where view != null && _namespacePattern.Matches(view.Namespace) |
||||
|
select view; |
||||
|
|
||||
|
var query = |
||||
|
from view in resolveViews |
||||
|
let names = _names.ResolveNames(view.Xaml) |
||||
|
let code = _code.GenerateCode(view.ClassName, view.Namespace, view.XamlType, names) |
||||
|
let fileName = ResolveViewFileName(view, _naming) |
||||
|
select new GeneratedPartialClass(fileName, code); |
||||
|
|
||||
|
return query.ToList(); |
||||
|
} |
||||
|
|
||||
|
private static string ResolveViewFileName(ResolvedView view, ViewFileNamingStrategy strategy) => strategy switch |
||||
|
{ |
||||
|
ViewFileNamingStrategy.ClassName => $"{view.ClassName}.g.cs", |
||||
|
ViewFileNamingStrategy.NamespaceAndClassName => $"{view.Namespace}.{view.ClassName}.g.cs", |
||||
|
_ => throw new ArgumentOutOfRangeException(nameof(strategy), strategy, "Unknown naming strategy!") |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Generators.Common; |
||||
|
using Avalonia.Generators.Common.Domain; |
||||
|
using Avalonia.Generators.Compiler; |
||||
|
using Microsoft.CodeAnalysis; |
||||
|
using Microsoft.CodeAnalysis.CSharp; |
||||
|
|
||||
|
namespace Avalonia.Generators.NameGenerator; |
||||
|
|
||||
|
[Generator] |
||||
|
public class AvaloniaNameSourceGenerator : ISourceGenerator |
||||
|
{ |
||||
|
public void Initialize(GeneratorInitializationContext context) { } |
||||
|
|
||||
|
public void Execute(GeneratorExecutionContext context) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var generator = CreateNameGenerator(context); |
||||
|
if (generator is null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var partials = generator.GenerateNameReferences(context.AdditionalFiles); |
||||
|
foreach (var (fileName, content) in partials) context.AddSource(fileName, content); |
||||
|
} |
||||
|
catch (Exception exception) |
||||
|
{ |
||||
|
context.ReportNameGeneratorUnhandledError(exception); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static INameGenerator CreateNameGenerator(GeneratorExecutionContext context) |
||||
|
{ |
||||
|
var options = new GeneratorOptions(context); |
||||
|
if (!options.AvaloniaNameGeneratorIsEnabled) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var types = new RoslynTypeSystem((CSharpCompilation)context.Compilation); |
||||
|
ICodeGenerator generator = options.AvaloniaNameGeneratorBehavior switch { |
||||
|
Behavior.OnlyProperties => new OnlyPropertiesCodeGenerator(), |
||||
|
Behavior.InitializeComponent => new InitializeComponentCodeGenerator(types), |
||||
|
_ => throw new ArgumentOutOfRangeException() |
||||
|
}; |
||||
|
|
||||
|
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 => context.ReportNameGeneratorInvalidType(type), |
||||
|
error => context.ReportNameGeneratorUnhandledError(error)), |
||||
|
new XamlXNameResolver(options.AvaloniaNameGeneratorClassFieldModifier), |
||||
|
generator); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using Microsoft.CodeAnalysis; |
||||
|
|
||||
|
namespace Avalonia.Generators.NameGenerator; |
||||
|
|
||||
|
internal interface INameGenerator |
||||
|
{ |
||||
|
IReadOnlyList<GeneratedPartialClass> GenerateNameReferences(IEnumerable<AdditionalText> additionalFiles); |
||||
|
} |
||||
|
|
||||
|
internal record GeneratedPartialClass(string FileName, string Content); |
||||
@ -0,0 +1,83 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Generators.Common.Domain; |
||||
|
using XamlX.TypeSystem; |
||||
|
|
||||
|
namespace Avalonia.Generators.NameGenerator; |
||||
|
|
||||
|
internal class InitializeComponentCodeGenerator: ICodeGenerator |
||||
|
{ |
||||
|
private readonly bool _diagnosticsAreConnected; |
||||
|
private const string AttachDevToolsCodeBlock = @"
|
||||
|
#if DEBUG
|
||||
|
if (attachDevTools) |
||||
|
{ |
||||
|
this.AttachDevTools(); |
||||
|
} |
||||
|
#endif
|
||||
|
";
|
||||
|
private const string AttachDevToolsParameterDocumentation |
||||
|
= @" /// <param name=""attachDevTools"">Should the dev tools be attached.</param>
|
||||
|
";
|
||||
|
|
||||
|
public InitializeComponentCodeGenerator(IXamlTypeSystem types) |
||||
|
{ |
||||
|
_diagnosticsAreConnected = types.FindAssembly("Avalonia.Diagnostics") != null; |
||||
|
} |
||||
|
|
||||
|
public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names) |
||||
|
{ |
||||
|
var properties = new List<string>(); |
||||
|
var initializations = new List<string>(); |
||||
|
foreach (var resolvedName in names) |
||||
|
{ |
||||
|
var (typeName, name, fieldModifier) = resolvedName; |
||||
|
properties.Add($" {fieldModifier} {typeName} {name};"); |
||||
|
initializations.Add($" {name} = this.FindNameScope()?.Find<{typeName}>(\"{name}\");"); |
||||
|
} |
||||
|
|
||||
|
var attachDevTools = _diagnosticsAreConnected && IsWindow(xamlType); |
||||
|
|
||||
|
return $@"// <auto-generated />
|
||||
|
|
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
|
||||
|
namespace {nameSpace} |
||||
|
{{ |
||||
|
partial class {className} |
||||
|
{{ |
||||
|
{string.Join("\n", properties)} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced).
|
||||
|
/// </summary>
|
||||
|
/// <param name=""loadXaml"">Should the XAML be loaded into the component.</param>
|
||||
|
{(attachDevTools ? AttachDevToolsParameterDocumentation : string.Empty)} |
||||
|
public void InitializeComponent(bool loadXaml = true{(attachDevTools ? ", bool attachDevTools = true" : string.Empty)}) |
||||
|
{{ |
||||
|
if (loadXaml) |
||||
|
{{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
}} |
||||
|
{(attachDevTools ? AttachDevToolsCodeBlock : string.Empty)} |
||||
|
{string.Join("\n", initializations)} |
||||
|
}} |
||||
|
}} |
||||
|
}} |
||||
|
";
|
||||
|
} |
||||
|
|
||||
|
private static bool IsWindow(IXamlType xamlType) |
||||
|
{ |
||||
|
var type = xamlType; |
||||
|
bool isWindow; |
||||
|
do |
||||
|
{ |
||||
|
isWindow = type.FullName == "Avalonia.Controls.Window"; |
||||
|
type = type.BaseType; |
||||
|
} while (!isWindow && type != null); |
||||
|
|
||||
|
return isWindow; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Generators.Common.Domain; |
||||
|
using XamlX.TypeSystem; |
||||
|
|
||||
|
namespace Avalonia.Generators.NameGenerator; |
||||
|
|
||||
|
internal class OnlyPropertiesCodeGenerator : ICodeGenerator |
||||
|
{ |
||||
|
public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names) |
||||
|
{ |
||||
|
var namedControls = names |
||||
|
.Select(info => " " + |
||||
|
$"{info.FieldModifier} {info.TypeName} {info.Name} => " + |
||||
|
$"this.FindNameScope()?.Find<{info.TypeName}>(\"{info.Name}\");") |
||||
|
.ToList(); |
||||
|
var lines = string.Join("\n", namedControls); |
||||
|
return $@"// <auto-generated />
|
||||
|
|
||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace {nameSpace} |
||||
|
{{ |
||||
|
partial class {className} |
||||
|
{{ |
||||
|
{lines} |
||||
|
}} |
||||
|
}} |
||||
|
";
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
namespace Avalonia.Generators.NameGenerator; |
||||
|
|
||||
|
internal enum Options |
||||
|
{ |
||||
|
Public = 0, |
||||
|
Private = 1, |
||||
|
Internal = 2, |
||||
|
Protected = 3, |
||||
|
} |
||||
|
|
||||
|
internal enum Behavior |
||||
|
{ |
||||
|
OnlyProperties = 0, |
||||
|
InitializeComponent = 1, |
||||
|
} |
||||
|
|
||||
|
internal enum ViewFileNamingStrategy |
||||
|
{ |
||||
|
ClassName = 0, |
||||
|
NamespaceAndClassName = 1, |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
{ |
||||
|
"profiles": { |
||||
|
"Profile 1": { |
||||
|
"commandName": "DebugRoslynComponent", |
||||
|
"targetProject": "..\\..\\..\\samples\\Generators.Sandbox\\Generators.Sandbox.csproj" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,209 @@ |
|||||
|
[](https://www.nuget.org/packages/XamlNameReferenceGenerator) [](https://www.nuget.org/packages/XamlNameReferenceGenerator)    |
||||
|
|
||||
|
### 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 source generator will look for the `xaml` (or `axaml`) file with the same name as your partial C# class that is a subclass of `Avalonia.INamed` and parses the XAML markup, finds all XAML tags with `x:Name` attributes and generates the C# code. |
||||
|
|
||||
|
### Getting Started |
||||
|
|
||||
|
In order to get started, just install the NuGet package: |
||||
|
|
||||
|
``` |
||||
|
dotnet add package XamlNameReferenceGenerator |
||||
|
``` |
||||
|
|
||||
|
Or, if you are using [submodules](https://git-scm.com/docs/git-submodule), you can reference the generator as such: |
||||
|
|
||||
|
```xml |
||||
|
<ItemGroup> |
||||
|
<!-- Remember to ensure XAML files are included via <AdditionalFiles>, |
||||
|
otherwise C# source generator won't see XAML files. --> |
||||
|
<AdditionalFiles Include="**\*.xaml"/> |
||||
|
<ProjectReference Include="..\Avalonia.NameGenerator\Avalonia.NameGenerator.csproj" |
||||
|
OutputItemType="Analyzer" |
||||
|
ReferenceOutputAssembly="false" /> |
||||
|
</ItemGroup> |
||||
|
``` |
||||
|
|
||||
|
### Usage |
||||
|
|
||||
|
After installing the NuGet package, declare your view class as `partial`. Typed C# references to Avalonia controls declared in XAML files will be generated for classes referenced by the `x:Class` directive in XAML files. For example, for the following XAML markup: |
||||
|
|
||||
|
```xml |
||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="Sample.App.SignUpView"> |
||||
|
<TextBox x:Name="UserNameTextBox" x:FieldModifier="public" /> |
||||
|
</Window> |
||||
|
``` |
||||
|
|
||||
|
A new C# partial class named `SignUpView` with a single `public` property named `UserNameTextBox` of type `TextBox` will be generated in the `Sample.App` namespace. We won't see the generated file, but we'll be able to access the generated property as shown below: |
||||
|
|
||||
|
```cs |
||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
public partial class SignUpView : Window |
||||
|
{ |
||||
|
public SignUpView() |
||||
|
{ |
||||
|
// This method is generated. Call it before accessing any |
||||
|
// of the generated properties. The 'UserNameTextBox' |
||||
|
// property is also generated. |
||||
|
InitializeComponent(); |
||||
|
UserNameTextBox.Text = "Joseph"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
<img src="https://hsto.org/getpro/habr/post_images/d9f/4aa/a1e/d9f4aaa1eb450f5dd2fca66631bc16a0.gif" /> |
||||
|
|
||||
|
### 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 |
||||
|
// UserNameValidation and PasswordValidation are auto generated. |
||||
|
public partial class SignUpView : ReactiveWindow<SignUpViewModel> |
||||
|
{ |
||||
|
public SignUpView() |
||||
|
{ |
||||
|
InitializeComponent(); |
||||
|
this.WhenActivated(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); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Advanced Usage |
||||
|
|
||||
|
> Never keep a method named `InitializeComponent` in your code-behind view class if you are using the generator with `AvaloniaNameGeneratorBehavior` set to `InitializeComponent` (this is the default value). The private `InitializeComponent` method declared in your code-behind class hides the `InitializeComponent` method generated by `Avalonia.NameGenerator`, see [Issue 69](https://github.com/AvaloniaUI/Avalonia.NameGenerator/issues/69). If you wish to use your own `InitializeComponent` method (not the generated one), set `AvaloniaNameGeneratorBehavior` to `OnlyProperties`. |
||||
|
|
||||
|
The `x:Name` generator can be configured via MsBuild properties that you can put into your C# project file (`.csproj`). Using such options, you can configure the generator behavior, the default field modifier, namespace and path filters. The generator supports the following options: |
||||
|
|
||||
|
- `AvaloniaNameGeneratorBehavior` |
||||
|
Possible values: `OnlyProperties`, `InitializeComponent` |
||||
|
Default value: `InitializeComponent` |
||||
|
Determines if the generator should generate get-only properties, or the `InitializeComponent` method. |
||||
|
|
||||
|
- `AvaloniaNameGeneratorDefaultFieldModifier` |
||||
|
Possible values: `internal`, `public`, `private`, `protected` |
||||
|
Default value: `internal` |
||||
|
The default field modifier that should be used when there is no `x:FieldModifier` directive specified. |
||||
|
|
||||
|
- `AvaloniaNameGeneratorFilterByPath` |
||||
|
Posssible format: `glob_pattern`, `glob_pattern;glob_pattern` |
||||
|
Default value: `*` |
||||
|
The generator will process only XAML files with paths matching the specified glob pattern(s). |
||||
|
Example: `*/Views/*View.xaml`, `*View.axaml;*Control.axaml` |
||||
|
|
||||
|
- `AvaloniaNameGeneratorFilterByNamespace` |
||||
|
Posssible format: `glob_pattern`, `glob_pattern;glob_pattern` |
||||
|
Default value: `*` |
||||
|
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 |
||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
<PropertyGroup> |
||||
|
<AvaloniaNameGeneratorBehavior>InitializeComponent</AvaloniaNameGeneratorBehavior> |
||||
|
<AvaloniaNameGeneratorDefaultFieldModifier>internal</AvaloniaNameGeneratorDefaultFieldModifier> |
||||
|
<AvaloniaNameGeneratorFilterByPath>*</AvaloniaNameGeneratorFilterByPath> |
||||
|
<AvaloniaNameGeneratorFilterByNamespace>*</AvaloniaNameGeneratorFilterByNamespace> |
||||
|
<AvaloniaNameGeneratorViewFileNamingStrategy>NamespaceAndClassName</AvaloniaNameGeneratorViewFileNamingStrategy> |
||||
|
</PropertyGroup> |
||||
|
<!-- ... --> |
||||
|
</Project> |
||||
|
``` |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### What do the generated sources look like? |
||||
|
|
||||
|
For [`SignUpView`](https://github.com/avaloniaui/Avalonia.NameGenerator/blob/main/src/Avalonia.NameGenerator.Sandbox/Views/SignUpView.xaml), we get the following generated output when the source generator is in the `InitializeComponent` mode: |
||||
|
|
||||
|
```cs |
||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox UserNameTextBox; |
||||
|
public global::Avalonia.Controls.TextBlock UserNameValidation; |
||||
|
private global::Avalonia.Controls.TextBox PasswordTextBox; |
||||
|
internal global::Avalonia.Controls.TextBlock PasswordValidation; |
||||
|
internal global::Avalonia.Controls.ListBox AwesomeListView; |
||||
|
internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox; |
||||
|
internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation; |
||||
|
internal global::Avalonia.Controls.Button SignUpButton; |
||||
|
internal global::Avalonia.Controls.TextBlock CompoundValidation; |
||||
|
|
||||
|
public void InitializeComponent(bool loadXaml = true, bool attachDevTools = true) |
||||
|
{ |
||||
|
if (loadXaml) |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
|
||||
|
// This will be added only if you install Avalonia.Diagnostics. |
||||
|
#if DEBUG |
||||
|
if (attachDevTools) |
||||
|
{ |
||||
|
this.AttachDevTools(); |
||||
|
} |
||||
|
#endif |
||||
|
|
||||
|
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox>("UserNameTextBox"); |
||||
|
UserNameValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("UserNameValidation"); |
||||
|
PasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
||||
|
PasswordValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("PasswordValidation"); |
||||
|
AwesomeListView = this.FindNameScope()?.Find<global::Avalonia.Controls.ListBox>("AwesomeListView"); |
||||
|
ConfirmPasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox"); |
||||
|
ConfirmPasswordValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("ConfirmPasswordValidation"); |
||||
|
SignUpButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
||||
|
CompoundValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("CompoundValidation"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
If you enable the `OnlyProperties` source generator mode, you get: |
||||
|
|
||||
|
```cs |
||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace Avalonia.NameGenerator.Sandbox.Views |
||||
|
{ |
||||
|
partial class SignUpView |
||||
|
{ |
||||
|
internal global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox>("UserNameTextBox"); |
||||
|
public global::Avalonia.Controls.TextBlock UserNameValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("UserNameValidation"); |
||||
|
private global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
||||
|
internal global::Avalonia.Controls.TextBlock PasswordValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("PasswordValidation"); |
||||
|
internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox"); |
||||
|
internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("ConfirmPasswordValidation"); |
||||
|
internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
||||
|
internal global::Avalonia.Controls.TextBlock CompoundValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("CompoundValidation"); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
@ -0,0 +1,26 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
<PropertyGroup> |
||||
|
<OutputType>Exe</OutputType> |
||||
|
<TargetFramework>net6.0</TargetFramework> |
||||
|
<RootNamespace>Avalonia.Generators.Tests</RootNamespace> |
||||
|
<IsTestProject>true</IsTestProject> |
||||
|
</PropertyGroup> |
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" /> |
||||
|
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" /> |
||||
|
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" /> |
||||
|
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" /> |
||||
|
<ProjectReference Include="..\..\src\tools\Avalonia.Generators\Avalonia.Generators.csproj" /> |
||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" /> |
||||
|
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" /> |
||||
|
</ItemGroup> |
||||
|
<ItemGroup> |
||||
|
<EmbeddedResource Include="Views\*.xml" /> |
||||
|
<EmbeddedResource Include="OnlyProperties\GeneratedCode\*.txt" /> |
||||
|
<EmbeddedResource Include="InitializeComponent\GeneratedInitializeComponent\*.txt" /> |
||||
|
<EmbeddedResource Include="InitializeComponent\GeneratedDevTools\*.txt" /> |
||||
|
</ItemGroup> |
||||
|
<Import Project="..\..\build\UnitTests.NetCore.targets" /> |
||||
|
<Import Project="..\..\build\XUnit.props" /> |
||||
|
<Import Project="..\..\build\SharedVersion.props" /> |
||||
|
</Project> |
||||
@ -0,0 +1,31 @@ |
|||||
|
using Avalonia.Generators.Common; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Generators.Tests; |
||||
|
|
||||
|
public class GlobPatternTests |
||||
|
{ |
||||
|
[Theory] |
||||
|
[InlineData("*", "anything", true)] |
||||
|
[InlineData("", "anything", false)] |
||||
|
[InlineData("Views/*", "Views/SignUpView.xaml", true)] |
||||
|
[InlineData("Views/*", "Extensions/SignUpView.xaml", false)] |
||||
|
[InlineData("*SignUpView*", "Extensions/SignUpView.xaml", true)] |
||||
|
[InlineData("*SignUpView.paml", "Extensions/SignUpView.xaml", false)] |
||||
|
[InlineData("*.xaml", "Extensions/SignUpView.xaml", true)] |
||||
|
public void Should_Match_Glob_Expressions(string pattern, string value, bool matches) |
||||
|
{ |
||||
|
Assert.Equal(matches, new GlobPattern(pattern).Matches(value)); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData("Views/SignUpView.xaml", true, new[] { "*.xaml", "Extensions/*" })] |
||||
|
[InlineData("Extensions/SignUpView.paml", true, new[] { "*.xaml", "Extensions/*" })] |
||||
|
[InlineData("Extensions/SignUpView.paml", false, new[] { "*.xaml", "Views/*" })] |
||||
|
[InlineData("anything", true, new[] { "*", "*" })] |
||||
|
[InlineData("anything", false, new[] { "", "" })] |
||||
|
public void Should_Match_Glob_Pattern_Groups(string value, bool matches, string[] patterns) |
||||
|
{ |
||||
|
Assert.Equal(matches, new GlobPatternGroup(patterns).Matches(value)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Avalonia.Controls.TextBox UserNameTextBox; |
||||
|
|
||||
|
/// <summary> |
||||
|
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
||||
|
/// </summary> |
||||
|
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
||||
|
|
||||
|
public void InitializeComponent(bool loadXaml = true) |
||||
|
{ |
||||
|
if (loadXaml) |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
|
||||
|
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Avalonia.Controls.TextBox UserNameTextBox; |
||||
|
|
||||
|
/// <summary> |
||||
|
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
||||
|
/// </summary> |
||||
|
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
||||
|
/// <param name="attachDevTools">Should the dev tools be attached.</param> |
||||
|
|
||||
|
public void InitializeComponent(bool loadXaml = true, bool attachDevTools = true) |
||||
|
{ |
||||
|
if (loadXaml) |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
|
||||
|
#if DEBUG |
||||
|
if (attachDevTools) |
||||
|
{ |
||||
|
this.AttachDevTools(); |
||||
|
} |
||||
|
#endif |
||||
|
|
||||
|
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Avalonia.Controls.TextBox UserNameTextBox; |
||||
|
|
||||
|
/// <summary> |
||||
|
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
||||
|
/// </summary> |
||||
|
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
||||
|
|
||||
|
public void InitializeComponent(bool loadXaml = true) |
||||
|
{ |
||||
|
if (loadXaml) |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
|
||||
|
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Avalonia.ReactiveUI.RoutedViewHost ClrNamespaceRoutedViewHost; |
||||
|
internal global::Avalonia.ReactiveUI.RoutedViewHost UriRoutedViewHost; |
||||
|
internal global::Controls.CustomTextBox UserNameTextBox; |
||||
|
|
||||
|
/// <summary> |
||||
|
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
||||
|
/// </summary> |
||||
|
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
||||
|
|
||||
|
public void InitializeComponent(bool loadXaml = true) |
||||
|
{ |
||||
|
if (loadXaml) |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
|
||||
|
ClrNamespaceRoutedViewHost = this.FindNameScope()?.Find<global::Avalonia.ReactiveUI.RoutedViewHost>("ClrNamespaceRoutedViewHost"); |
||||
|
UriRoutedViewHost = this.FindNameScope()?.Find<global::Avalonia.ReactiveUI.RoutedViewHost>("UriRoutedViewHost"); |
||||
|
UserNameTextBox = this.FindNameScope()?.Find<global::Controls.CustomTextBox>("UserNameTextBox"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Avalonia.Controls.TextBox UserNameTextBox; |
||||
|
internal global::Avalonia.Controls.ListBox NamedListBox; |
||||
|
|
||||
|
/// <summary> |
||||
|
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
||||
|
/// </summary> |
||||
|
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
||||
|
|
||||
|
public void InitializeComponent(bool loadXaml = true) |
||||
|
{ |
||||
|
if (loadXaml) |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
|
||||
|
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
||||
|
NamedListBox = this.FindNameScope()?.Find<global::Avalonia.Controls.ListBox>("NamedListBox"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
public global::Avalonia.Controls.TextBox FirstNameTextBox; |
||||
|
public global::Avalonia.Controls.TextBox LastNameTextBox; |
||||
|
protected global::Avalonia.Controls.TextBox PasswordTextBox; |
||||
|
private global::Avalonia.Controls.TextBox ConfirmPasswordTextBox; |
||||
|
internal global::Avalonia.Controls.Button SignUpButton; |
||||
|
internal global::Avalonia.Controls.Button RegisterButton; |
||||
|
|
||||
|
/// <summary> |
||||
|
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
||||
|
/// </summary> |
||||
|
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
||||
|
|
||||
|
public void InitializeComponent(bool loadXaml = true) |
||||
|
{ |
||||
|
if (loadXaml) |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
|
||||
|
FirstNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("FirstNameTextBox"); |
||||
|
LastNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("LastNameTextBox"); |
||||
|
PasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
||||
|
ConfirmPasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox"); |
||||
|
SignUpButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
||||
|
RegisterButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("RegisterButton"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Avalonia.Generators.Tests.InitializeComponent.GeneratedInitializeComponent; |
||||
|
|
||||
|
public static class InitializeComponentCode |
||||
|
{ |
||||
|
public const string NamedControl = "NamedControl.txt"; |
||||
|
public const string NamedControls = "NamedControls.txt"; |
||||
|
public const string XNamedControl = "xNamedControl.txt"; |
||||
|
public const string XNamedControls = "xNamedControls.txt"; |
||||
|
public const string NoNamedControls = "NoNamedControls.txt"; |
||||
|
public const string CustomControls = "CustomControls.txt"; |
||||
|
public const string DataTemplates = "DataTemplates.txt"; |
||||
|
public const string SignUpView = "SignUpView.txt"; |
||||
|
public const string FieldModifier = "FieldModifier.txt"; |
||||
|
public const string AttachedProps = "AttachedProps.txt"; |
||||
|
public const string AttachedPropsWithDevTools = "AttachedPropsWithDevTools.txt"; |
||||
|
public const string ControlWithoutWindow = "ControlWithoutWindow.txt"; |
||||
|
|
||||
|
public static async Task<string> Load(string generatedCodeResourceName) |
||||
|
{ |
||||
|
var assembly = typeof(XamlXNameResolverTests).Assembly; |
||||
|
var fullResourceName = assembly |
||||
|
.GetManifestResourceNames() |
||||
|
.First(name => name.Contains("InitializeComponent") && |
||||
|
name.Contains("GeneratedInitializeComponent") && |
||||
|
name.EndsWith(generatedCodeResourceName)); |
||||
|
|
||||
|
await using var stream = assembly.GetManifestResourceStream(fullResourceName); |
||||
|
using var reader = new StreamReader(stream!); |
||||
|
return await reader.ReadToEndAsync(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Avalonia.Controls.TextBox UserNameTextBox; |
||||
|
|
||||
|
/// <summary> |
||||
|
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
||||
|
/// </summary> |
||||
|
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
||||
|
|
||||
|
public void InitializeComponent(bool loadXaml = true) |
||||
|
{ |
||||
|
if (loadXaml) |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
|
||||
|
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Avalonia.Controls.TextBox UserNameTextBox; |
||||
|
internal global::Avalonia.Controls.TextBox PasswordTextBox; |
||||
|
internal global::Avalonia.Controls.Button SignUpButton; |
||||
|
|
||||
|
/// <summary> |
||||
|
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
||||
|
/// </summary> |
||||
|
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
||||
|
|
||||
|
public void InitializeComponent(bool loadXaml = true) |
||||
|
{ |
||||
|
if (loadXaml) |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
|
||||
|
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
||||
|
PasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
||||
|
SignUpButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
|
||||
|
|
||||
|
/// <summary> |
||||
|
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
||||
|
/// </summary> |
||||
|
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
||||
|
|
||||
|
public void InitializeComponent(bool loadXaml = true) |
||||
|
{ |
||||
|
if (loadXaml) |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Controls.CustomTextBox UserNameTextBox; |
||||
|
internal global::Avalonia.Controls.TextBlock UserNameValidation; |
||||
|
internal global::Avalonia.Controls.TextBox PasswordTextBox; |
||||
|
internal global::Avalonia.Controls.TextBlock PasswordValidation; |
||||
|
internal global::Avalonia.Controls.ListBox AwesomeListView; |
||||
|
internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox; |
||||
|
internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation; |
||||
|
internal global::Avalonia.Controls.Documents.Run SignUpButtonDescription; |
||||
|
internal global::Avalonia.Controls.Button SignUpButton; |
||||
|
internal global::Avalonia.Controls.TextBlock CompoundValidation; |
||||
|
|
||||
|
/// <summary> |
||||
|
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
||||
|
/// </summary> |
||||
|
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
||||
|
|
||||
|
public void InitializeComponent(bool loadXaml = true) |
||||
|
{ |
||||
|
if (loadXaml) |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
|
||||
|
UserNameTextBox = this.FindNameScope()?.Find<global::Controls.CustomTextBox>("UserNameTextBox"); |
||||
|
UserNameValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("UserNameValidation"); |
||||
|
PasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
||||
|
PasswordValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("PasswordValidation"); |
||||
|
AwesomeListView = this.FindNameScope()?.Find<global::Avalonia.Controls.ListBox>("AwesomeListView"); |
||||
|
ConfirmPasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox"); |
||||
|
ConfirmPasswordValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("ConfirmPasswordValidation"); |
||||
|
SignUpButtonDescription = this.FindNameScope()?.Find<global::Avalonia.Controls.Documents.Run>("SignUpButtonDescription"); |
||||
|
SignUpButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
||||
|
CompoundValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("CompoundValidation"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Avalonia.Controls.TextBox UserNameTextBox; |
||||
|
|
||||
|
/// <summary> |
||||
|
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
||||
|
/// </summary> |
||||
|
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
||||
|
|
||||
|
public void InitializeComponent(bool loadXaml = true) |
||||
|
{ |
||||
|
if (loadXaml) |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
|
||||
|
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Avalonia.Controls.TextBox UserNameTextBox; |
||||
|
internal global::Avalonia.Controls.TextBox PasswordTextBox; |
||||
|
internal global::Avalonia.Controls.Button SignUpButton; |
||||
|
|
||||
|
/// <summary> |
||||
|
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
||||
|
/// </summary> |
||||
|
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
||||
|
|
||||
|
public void InitializeComponent(bool loadXaml = true) |
||||
|
{ |
||||
|
if (loadXaml) |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
|
||||
|
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
||||
|
PasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
||||
|
SignUpButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Generators.Common; |
||||
|
using Avalonia.Generators.Compiler; |
||||
|
using Avalonia.Generators.NameGenerator; |
||||
|
using Avalonia.Generators.Tests.InitializeComponent.GeneratedInitializeComponent; |
||||
|
using Avalonia.Generators.Tests.OnlyProperties.GeneratedCode; |
||||
|
using Avalonia.Generators.Tests.Views; |
||||
|
using Microsoft.CodeAnalysis.CSharp; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Generators.Tests.InitializeComponent; |
||||
|
|
||||
|
public class InitializeComponentTests |
||||
|
{ |
||||
|
[Theory] |
||||
|
[InlineData(InitializeComponentCode.NamedControl, View.NamedControl, false)] |
||||
|
[InlineData(InitializeComponentCode.NamedControls, View.NamedControls, false)] |
||||
|
[InlineData(InitializeComponentCode.XNamedControl, View.XNamedControl, false)] |
||||
|
[InlineData(InitializeComponentCode.XNamedControls, View.XNamedControls, false)] |
||||
|
[InlineData(InitializeComponentCode.NoNamedControls, View.NoNamedControls, false)] |
||||
|
[InlineData(InitializeComponentCode.CustomControls, View.CustomControls, false)] |
||||
|
[InlineData(InitializeComponentCode.DataTemplates, View.DataTemplates, false)] |
||||
|
[InlineData(InitializeComponentCode.SignUpView, View.SignUpView, false)] |
||||
|
[InlineData(InitializeComponentCode.FieldModifier, View.FieldModifier, false)] |
||||
|
[InlineData(InitializeComponentCode.AttachedPropsWithDevTools, View.AttachedProps, true)] |
||||
|
[InlineData(InitializeComponentCode.AttachedProps, View.AttachedProps, false)] |
||||
|
[InlineData(InitializeComponentCode.ControlWithoutWindow, View.ControlWithoutWindow, true)] |
||||
|
[InlineData(InitializeComponentCode.ControlWithoutWindow, View.ControlWithoutWindow, false)] |
||||
|
public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File( |
||||
|
string expectation, |
||||
|
string markup, |
||||
|
bool devToolsMode) |
||||
|
{ |
||||
|
var excluded = devToolsMode ? null : "Avalonia.Diagnostics"; |
||||
|
var compilation = |
||||
|
View.CreateAvaloniaCompilation(excluded) |
||||
|
.WithCustomTextBox(); |
||||
|
|
||||
|
var types = new RoslynTypeSystem(compilation); |
||||
|
var classResolver = new XamlXViewResolver( |
||||
|
types, |
||||
|
MiniCompiler.CreateDefault( |
||||
|
new RoslynTypeSystem(compilation), |
||||
|
MiniCompiler.AvaloniaXmlnsDefinitionAttribute)); |
||||
|
|
||||
|
var xaml = await View.Load(markup); |
||||
|
var classInfo = classResolver.ResolveView(xaml); |
||||
|
var nameResolver = new XamlXNameResolver(); |
||||
|
var names = nameResolver.ResolveNames(classInfo.Xaml); |
||||
|
|
||||
|
var generator = new InitializeComponentCodeGenerator(types); |
||||
|
|
||||
|
var code = generator |
||||
|
.GenerateCode("SampleView", "Sample.App", classInfo.XamlType, names) |
||||
|
.Replace("\r", string.Empty); |
||||
|
|
||||
|
var expected = await InitializeComponentCode.Load(expectation); |
||||
|
|
||||
|
|
||||
|
CSharpSyntaxTree.ParseText(code); |
||||
|
Assert.Equal(expected.Replace("\r", string.Empty), code); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,59 @@ |
|||||
|
using System; |
||||
|
using System.ComponentModel; |
||||
|
using Avalonia.Generators.Compiler; |
||||
|
using Microsoft.CodeAnalysis; |
||||
|
using Microsoft.CodeAnalysis.CSharp; |
||||
|
using Avalonia.Generators.Tests.Views; |
||||
|
using XamlX; |
||||
|
using XamlX.Parsers; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Generators.Tests; |
||||
|
|
||||
|
public class MiniCompilerTests |
||||
|
{ |
||||
|
private const string AvaloniaXaml = "<TextBlock xmlns='clr-namespace:Avalonia.Controls;assembly=Avalonia' />"; |
||||
|
private const string MiniClass = "namespace Example { public class Valid { public int Foo() => 21; } }"; |
||||
|
private const string MiniInvalidXaml = "<Invalid xmlns='clr-namespace:Example;assembly=Example' />"; |
||||
|
private const string MiniValidXaml = "<Valid xmlns='clr-namespace:Example;assembly=Example' />"; |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Resolve_Types_From_Simple_Valid_Xaml_Markup() |
||||
|
{ |
||||
|
var xaml = XDocumentXamlParser.Parse(MiniValidXaml); |
||||
|
var compilation = CreateBasicCompilation(MiniClass); |
||||
|
MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)).Transform(xaml); |
||||
|
|
||||
|
Assert.NotNull(xaml.Root); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Throw_When_Unable_To_Resolve_Types_From_Simple_Invalid_Markup() |
||||
|
{ |
||||
|
var xaml = XDocumentXamlParser.Parse(MiniInvalidXaml); |
||||
|
var compilation = CreateBasicCompilation(MiniClass); |
||||
|
var compiler = MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)); |
||||
|
|
||||
|
Assert.Throws<XamlParseException>(() => compiler.Transform(xaml)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Resolve_Types_From_Simple_Avalonia_Markup() |
||||
|
{ |
||||
|
var xaml = XDocumentXamlParser.Parse(AvaloniaXaml); |
||||
|
var compilation = View.CreateAvaloniaCompilation(); |
||||
|
MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)).Transform(xaml); |
||||
|
|
||||
|
Assert.NotNull(xaml.Root); |
||||
|
} |
||||
|
|
||||
|
private static CSharpCompilation CreateBasicCompilation(string source) => |
||||
|
CSharpCompilation |
||||
|
.Create("BasicLib", options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) |
||||
|
.AddReferences(MetadataReference.CreateFromFile(typeof(string).Assembly.Location)) |
||||
|
.AddReferences(MetadataReference.CreateFromFile(typeof(IServiceProvider).Assembly.Location)) |
||||
|
.AddReferences(MetadataReference.CreateFromFile(typeof(ITypeDescriptorContext).Assembly.Location)) |
||||
|
.AddReferences(MetadataReference.CreateFromFile(typeof(ISupportInitialize).Assembly.Location)) |
||||
|
.AddReferences(MetadataReference.CreateFromFile(typeof(TypeConverterAttribute).Assembly.Location)) |
||||
|
.AddSyntaxTrees(CSharpSyntaxTree.ParseText(source)); |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Avalonia.ReactiveUI.RoutedViewHost ClrNamespaceRoutedViewHost => this.FindNameScope()?.Find<global::Avalonia.ReactiveUI.RoutedViewHost>("ClrNamespaceRoutedViewHost"); |
||||
|
internal global::Avalonia.ReactiveUI.RoutedViewHost UriRoutedViewHost => this.FindNameScope()?.Find<global::Avalonia.ReactiveUI.RoutedViewHost>("UriRoutedViewHost"); |
||||
|
internal global::Controls.CustomTextBox UserNameTextBox => this.FindNameScope()?.Find<global::Controls.CustomTextBox>("UserNameTextBox"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
||||
|
internal global::Avalonia.Controls.ListBox NamedListBox => this.FindNameScope()?.Find<global::Avalonia.Controls.ListBox>("NamedListBox"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
public global::Avalonia.Controls.TextBox FirstNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("FirstNameTextBox"); |
||||
|
public global::Avalonia.Controls.TextBox LastNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("LastNameTextBox"); |
||||
|
protected global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
||||
|
private global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox"); |
||||
|
internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
||||
|
internal global::Avalonia.Controls.Button RegisterButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("RegisterButton"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
||||
|
internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
||||
|
internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Reflection; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Avalonia.Generators.Tests.OnlyProperties.GeneratedCode; |
||||
|
|
||||
|
public static class OnlyPropertiesCode |
||||
|
{ |
||||
|
public const string NamedControl = "NamedControl.txt"; |
||||
|
public const string NamedControls = "NamedControls.txt"; |
||||
|
public const string XNamedControl = "xNamedControl.txt"; |
||||
|
public const string XNamedControls = "xNamedControls.txt"; |
||||
|
public const string NoNamedControls = "NoNamedControls.txt"; |
||||
|
public const string CustomControls = "CustomControls.txt"; |
||||
|
public const string DataTemplates = "DataTemplates.txt"; |
||||
|
public const string SignUpView = "SignUpView.txt"; |
||||
|
public const string AttachedProps = "AttachedProps.txt"; |
||||
|
public const string FieldModifier = "FieldModifier.txt"; |
||||
|
public const string ControlWithoutWindow = "ControlWithoutWindow.txt"; |
||||
|
|
||||
|
public static async Task<string> Load(string generatedCodeResourceName) |
||||
|
{ |
||||
|
var assembly = typeof(XamlXNameResolverTests).Assembly; |
||||
|
var fullResourceName = assembly |
||||
|
.GetManifestResourceNames() |
||||
|
.First(name => name.Contains("OnlyProperties") && name.EndsWith(generatedCodeResourceName)); |
||||
|
|
||||
|
await using var stream = assembly.GetManifestResourceStream(fullResourceName); |
||||
|
using var reader = new StreamReader(stream!); |
||||
|
return await reader.ReadToEndAsync(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Controls.CustomTextBox UserNameTextBox => this.FindNameScope()?.Find<global::Controls.CustomTextBox>("UserNameTextBox"); |
||||
|
internal global::Avalonia.Controls.TextBlock UserNameValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("UserNameValidation"); |
||||
|
internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
||||
|
internal global::Avalonia.Controls.TextBlock PasswordValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("PasswordValidation"); |
||||
|
internal global::Avalonia.Controls.ListBox AwesomeListView => this.FindNameScope()?.Find<global::Avalonia.Controls.ListBox>("AwesomeListView"); |
||||
|
internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox"); |
||||
|
internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("ConfirmPasswordValidation"); |
||||
|
internal global::Avalonia.Controls.Documents.Run SignUpButtonDescription => this.FindNameScope()?.Find<global::Avalonia.Controls.Documents.Run>("SignUpButtonDescription"); |
||||
|
internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
||||
|
internal global::Avalonia.Controls.TextBlock CompoundValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("CompoundValidation"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
// <auto-generated /> |
||||
|
|
||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace Sample.App |
||||
|
{ |
||||
|
partial class SampleView |
||||
|
{ |
||||
|
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
||||
|
internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
||||
|
internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Generators.Common; |
||||
|
using Avalonia.Generators.Compiler; |
||||
|
using Avalonia.Generators.NameGenerator; |
||||
|
using Avalonia.Generators.Tests.OnlyProperties.GeneratedCode; |
||||
|
using Avalonia.Generators.Tests.Views; |
||||
|
using Microsoft.CodeAnalysis.CSharp; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Generators.Tests.OnlyProperties; |
||||
|
|
||||
|
public class OnlyPropertiesTests |
||||
|
{ |
||||
|
[Theory] |
||||
|
[InlineData(OnlyPropertiesCode.NamedControl, View.NamedControl)] |
||||
|
[InlineData(OnlyPropertiesCode.NamedControls, View.NamedControls)] |
||||
|
[InlineData(OnlyPropertiesCode.XNamedControl, View.XNamedControl)] |
||||
|
[InlineData(OnlyPropertiesCode.XNamedControls, View.XNamedControls)] |
||||
|
[InlineData(OnlyPropertiesCode.NoNamedControls, View.NoNamedControls)] |
||||
|
[InlineData(OnlyPropertiesCode.CustomControls, View.CustomControls)] |
||||
|
[InlineData(OnlyPropertiesCode.DataTemplates, View.DataTemplates)] |
||||
|
[InlineData(OnlyPropertiesCode.SignUpView, View.SignUpView)] |
||||
|
[InlineData(OnlyPropertiesCode.AttachedProps, View.AttachedProps)] |
||||
|
[InlineData(OnlyPropertiesCode.FieldModifier, View.FieldModifier)] |
||||
|
[InlineData(OnlyPropertiesCode.ControlWithoutWindow, View.ControlWithoutWindow)] |
||||
|
public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File(string expectation, string markup) |
||||
|
{ |
||||
|
var compilation = |
||||
|
View.CreateAvaloniaCompilation() |
||||
|
.WithCustomTextBox(); |
||||
|
|
||||
|
var classResolver = new XamlXViewResolver( |
||||
|
new RoslynTypeSystem(compilation), |
||||
|
MiniCompiler.CreateDefault( |
||||
|
new RoslynTypeSystem(compilation), |
||||
|
MiniCompiler.AvaloniaXmlnsDefinitionAttribute)); |
||||
|
|
||||
|
var xaml = await View.Load(markup); |
||||
|
var classInfo = classResolver.ResolveView(xaml); |
||||
|
var nameResolver = new XamlXNameResolver(); |
||||
|
var names = nameResolver.ResolveNames(classInfo.Xaml); |
||||
|
|
||||
|
var generator = new OnlyPropertiesCodeGenerator(); |
||||
|
var code = generator |
||||
|
.GenerateCode("SampleView", "Sample.App", classInfo.XamlType, names) |
||||
|
.Replace("\r", string.Empty); |
||||
|
|
||||
|
var expected = await OnlyPropertiesCode.Load(expectation); |
||||
|
CSharpSyntaxTree.ParseText(code); |
||||
|
Assert.Equal(expected.Replace("\r", string.Empty), code); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:custom="clr-namespace:Avalonia.ReactiveUI;assembly=Avalonia.ReactiveUI" |
||||
|
xmlns:rxui="http://reactiveui.net" |
||||
|
x:Class="Sample.App.AttachedProps" |
||||
|
Design.Width="300"> |
||||
|
<TextBox Name="UserNameTextBox" |
||||
|
Watermark="Username input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
</Window> |
||||
@ -0,0 +1,10 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:custom="clr-namespace:Avalonia.ReactiveUI;assembly=Avalonia.ReactiveUI" |
||||
|
xmlns:rxui="http://reactiveui.net" |
||||
|
x:Class="Sample.App.ControlWithoutWindow" |
||||
|
Design.Width="300"> |
||||
|
<TextBox Name="UserNameTextBox" |
||||
|
Watermark="Username input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
</UserControl> |
||||
@ -0,0 +1,11 @@ |
|||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:custom="clr-namespace:Avalonia.ReactiveUI;assembly=Avalonia.ReactiveUI" |
||||
|
xmlns:controls="clr-namespace:Controls" |
||||
|
x:Class="Sample.App.CustomControls" |
||||
|
xmlns:rxui="http://reactiveui.net"> |
||||
|
<custom:RoutedViewHost Name="ClrNamespaceRoutedViewHost" /> |
||||
|
<rxui:RoutedViewHost Name="UriRoutedViewHost" /> |
||||
|
<controls:CustomTextBox Name="UserNameTextBox" /> |
||||
|
<controls:EvilControl Name="EvilName" /> |
||||
|
</Window> |
||||
@ -0,0 +1,18 @@ |
|||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="Sample.App.DataTemplates"> |
||||
|
<StackPanel> |
||||
|
<TextBox x:Name="UserNameTextBox" |
||||
|
Watermark="Username input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
<ListBox Name="NamedListBox"> |
||||
|
<ListBox.ItemTemplate> |
||||
|
<DataTemplate x:Name="NamedDataTemplate"> |
||||
|
<TextBox x:Name="TemplatedTextBox" |
||||
|
Watermark="Templated input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
</DataTemplate> |
||||
|
</ListBox.ItemTemplate> |
||||
|
</ListBox> |
||||
|
</StackPanel> |
||||
|
</Window> |
||||
@ -0,0 +1,28 @@ |
|||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="Sample.App.FieldModifier"> |
||||
|
<StackPanel> |
||||
|
<TextBox Name="FirstNameTextBox" |
||||
|
x:FieldModifier="Public" |
||||
|
Watermark="Username input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
<TextBox Name="LastNameTextBox" |
||||
|
x:FieldModifier="public" |
||||
|
Watermark="Username input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
<TextBox Name="PasswordTextBox" |
||||
|
x:FieldModifier="protected" |
||||
|
Watermark="Password input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
<TextBox Name="ConfirmPasswordTextBox" |
||||
|
x:FieldModifier="private" |
||||
|
Watermark="Password input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
<Button Name="SignUpButton" |
||||
|
x:FieldModifier="NotPublic" |
||||
|
Content="Sign up" /> |
||||
|
<Button Name="RegisterButton" |
||||
|
x:FieldModifier="Nonsense" |
||||
|
Content="Register" /> |
||||
|
</StackPanel> |
||||
|
</Window> |
||||
@ -0,0 +1,7 @@ |
|||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="Sample.App.NamedControl"> |
||||
|
<TextBox Name="UserNameTextBox" |
||||
|
Watermark="Username input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
</Window> |
||||
@ -0,0 +1,14 @@ |
|||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="Sample.App.NamedControls"> |
||||
|
<StackPanel> |
||||
|
<TextBox Name="UserNameTextBox" |
||||
|
Watermark="Username input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
<TextBox Name="PasswordTextBox" |
||||
|
Watermark="Password input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
<Button Name="SignUpButton" |
||||
|
Content="Sign up" /> |
||||
|
</StackPanel> |
||||
|
</Window> |
||||
@ -0,0 +1,6 @@ |
|||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="Sample.App.NoNamedControls"> |
||||
|
<TextBox Watermark="Username input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
</Window> |
||||
@ -0,0 +1,52 @@ |
|||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:controls="clr-namespace:Controls" |
||||
|
x:Class="Sample.App.SignUpView"> |
||||
|
<StackPanel> |
||||
|
<controls:CustomTextBox Margin="0 10 0 0" |
||||
|
Name="UserNameTextBox" |
||||
|
Watermark="Please, enter user name..." |
||||
|
UseFloatingWatermark="True" /> |
||||
|
<TextBlock Name="UserNameValidation" |
||||
|
Foreground="Red" |
||||
|
FontSize="12" /> |
||||
|
<TextBox Margin="0 10 0 0" |
||||
|
Name="PasswordTextBox" |
||||
|
Watermark="Please, enter your password..." |
||||
|
UseFloatingWatermark="True" |
||||
|
PasswordChar="*" /> |
||||
|
<TextBlock Name="PasswordValidation" |
||||
|
Foreground="Red" |
||||
|
FontSize="12" /> |
||||
|
<ListBox x:Name="AwesomeListView"> |
||||
|
<ListBox.ItemTemplate> |
||||
|
<DataTemplate> |
||||
|
<TextBlock x:Name="MeaningLessName" Text="{Binding}" /> |
||||
|
</DataTemplate> |
||||
|
</ListBox.ItemTemplate> |
||||
|
</ListBox> |
||||
|
<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" /> |
||||
|
<TextBlock> |
||||
|
<TextBlock.Inlines> |
||||
|
<InlineCollection> |
||||
|
<Run x:Name="SignUpButtonDescription" /> |
||||
|
</InlineCollection> |
||||
|
</TextBlock.Inlines> |
||||
|
</TextBlock> |
||||
|
<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,76 @@ |
|||||
|
using System; |
||||
|
using System.ComponentModel; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Controls; |
||||
|
using Microsoft.CodeAnalysis; |
||||
|
using Microsoft.CodeAnalysis.CSharp; |
||||
|
|
||||
|
namespace Avalonia.Generators.Tests.Views; |
||||
|
|
||||
|
public static class View |
||||
|
{ |
||||
|
public const string NamedControl = "NamedControl.xml"; |
||||
|
public const string NamedControls = "NamedControls.xml"; |
||||
|
public const string XNamedControl = "xNamedControl.xml"; |
||||
|
public const string XNamedControls = "xNamedControls.xml"; |
||||
|
public const string NoNamedControls = "NoNamedControls.xml"; |
||||
|
public const string CustomControls = "CustomControls.xml"; |
||||
|
public const string DataTemplates = "DataTemplates.xml"; |
||||
|
public const string SignUpView = "SignUpView.xml"; |
||||
|
public const string AttachedProps = "AttachedProps.xml"; |
||||
|
public const string FieldModifier = "FieldModifier.xml"; |
||||
|
public const string ControlWithoutWindow = "ControlWithoutWindow.xml"; |
||||
|
public const string ViewWithGenericBaseView = "ViewWithGenericBaseView.xml"; |
||||
|
|
||||
|
public static async Task<string> Load(string viewName) |
||||
|
{ |
||||
|
var assembly = typeof(XamlXNameResolverTests).Assembly; |
||||
|
var fullResourceName = assembly |
||||
|
.GetManifestResourceNames() |
||||
|
.First(name => name.EndsWith(viewName)); |
||||
|
|
||||
|
await using var stream = assembly.GetManifestResourceStream(fullResourceName); |
||||
|
using var reader = new StreamReader(stream!); |
||||
|
return await reader.ReadToEndAsync(); |
||||
|
} |
||||
|
|
||||
|
public static CSharpCompilation CreateAvaloniaCompilation(string excludedPattern = null) |
||||
|
{ |
||||
|
var compilation = CSharpCompilation |
||||
|
.Create("AvaloniaLib", options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) |
||||
|
.AddReferences(MetadataReference.CreateFromFile(typeof(string).Assembly.Location)) |
||||
|
.AddReferences(MetadataReference.CreateFromFile(typeof(IServiceProvider).Assembly.Location)) |
||||
|
.AddReferences(MetadataReference.CreateFromFile(typeof(ITypeDescriptorContext).Assembly.Location)) |
||||
|
.AddReferences(MetadataReference.CreateFromFile(typeof(ISupportInitialize).Assembly.Location)) |
||||
|
.AddReferences(MetadataReference.CreateFromFile(typeof(TypeConverterAttribute).Assembly.Location)); |
||||
|
|
||||
|
var avaloniaAssemblyLocation = typeof(TextBlock).Assembly.Location; |
||||
|
var avaloniaAssemblyDirectory = Path.GetDirectoryName(avaloniaAssemblyLocation); |
||||
|
var avaloniaAssemblyReferences = Directory |
||||
|
.EnumerateFiles(avaloniaAssemblyDirectory!) |
||||
|
.Where(file => file.EndsWith(".dll") && |
||||
|
file.Contains("Avalonia") && |
||||
|
(string.IsNullOrWhiteSpace(excludedPattern) || !file.Contains(excludedPattern))) |
||||
|
.Select(file => MetadataReference.CreateFromFile(file)) |
||||
|
.ToList(); |
||||
|
|
||||
|
return compilation.AddReferences(avaloniaAssemblyReferences); |
||||
|
} |
||||
|
|
||||
|
public static CSharpCompilation WithCustomTextBox(this CSharpCompilation compilation) => |
||||
|
compilation.AddSyntaxTrees( |
||||
|
CSharpSyntaxTree.ParseText( |
||||
|
"using Avalonia.Controls;" + |
||||
|
"namespace Controls {" + |
||||
|
" public class CustomTextBox : TextBox { }" + |
||||
|
" public class EvilControl { }" + |
||||
|
"}")); |
||||
|
|
||||
|
public static CSharpCompilation WithBaseView(this CSharpCompilation compilation) => |
||||
|
compilation.AddSyntaxTrees( |
||||
|
CSharpSyntaxTree.ParseText( |
||||
|
"using Avalonia.Controls;" + |
||||
|
"namespace Sample.App { public class BaseView<TViewModel> : UserControl { } }")); |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
<local:BaseView |
||||
|
xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="Sample.App.ViewWithGenericBaseView" |
||||
|
Design.Width="300" |
||||
|
xmlns:sys="clr-namespace:System" |
||||
|
x:TypeArguments="sys:String" |
||||
|
x:Name="Root" |
||||
|
xmlns:local="clr-namespace:Sample.App"> |
||||
|
<Grid> |
||||
|
<local:BaseView x:Name="NotAsRootNode" x:TypeArguments="sys:Int32" /> |
||||
|
</Grid> |
||||
|
</local:BaseView> |
||||
@ -0,0 +1,7 @@ |
|||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="Sample.App.xNamedControl"> |
||||
|
<TextBox x:Name="UserNameTextBox" |
||||
|
Watermark="Username input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
</Window> |
||||
@ -0,0 +1,14 @@ |
|||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="Sample.App.xNamedControls"> |
||||
|
<StackPanel> |
||||
|
<TextBox x:Name="UserNameTextBox" |
||||
|
Watermark="Username input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
<TextBox x:Name="PasswordTextBox" |
||||
|
Watermark="Password input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
<Button x:Name="SignUpButton" |
||||
|
Content="Sign up" /> |
||||
|
</StackPanel> |
||||
|
</Window> |
||||
@ -0,0 +1,40 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Generators.Common; |
||||
|
using Avalonia.Generators.Compiler; |
||||
|
using Avalonia.Generators.Tests.Views; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Generators.Tests; |
||||
|
|
||||
|
public class XamlXClassResolverTests |
||||
|
{ |
||||
|
[Theory] |
||||
|
[InlineData("Sample.App", "NamedControl", View.NamedControl)] |
||||
|
[InlineData("Sample.App", "AttachedProps", View.AttachedProps)] |
||||
|
[InlineData("Sample.App", "CustomControls", View.CustomControls)] |
||||
|
[InlineData("Sample.App", "DataTemplates", View.DataTemplates)] |
||||
|
[InlineData("Sample.App", "FieldModifier", View.FieldModifier)] |
||||
|
[InlineData("Sample.App", "NamedControls", View.NamedControls)] |
||||
|
[InlineData("Sample.App", "NoNamedControls", View.NoNamedControls)] |
||||
|
[InlineData("Sample.App", "SignUpView", View.SignUpView)] |
||||
|
[InlineData("Sample.App", "xNamedControl", View.XNamedControl)] |
||||
|
[InlineData("Sample.App", "xNamedControls", View.XNamedControls)] |
||||
|
[InlineData("Sample.App", "ViewWithGenericBaseView", View.ViewWithGenericBaseView)] |
||||
|
public async Task Should_Resolve_Base_Class_From_Xaml_File(string nameSpace, string className, string markup) |
||||
|
{ |
||||
|
var xaml = await View.Load(markup); |
||||
|
var compilation = View |
||||
|
.CreateAvaloniaCompilation() |
||||
|
.WithCustomTextBox() |
||||
|
.WithBaseView(); |
||||
|
|
||||
|
var types = new RoslynTypeSystem(compilation); |
||||
|
var resolver = new XamlXViewResolver( |
||||
|
types, |
||||
|
MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute)); |
||||
|
|
||||
|
var resolvedClass = resolver.ResolveView(xaml); |
||||
|
Assert.Equal(className, resolvedClass.ClassName); |
||||
|
Assert.Equal(nameSpace, resolvedClass.Namespace); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,141 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Generators.Common; |
||||
|
using Avalonia.Generators.Common.Domain; |
||||
|
using Avalonia.Generators.Compiler; |
||||
|
using Avalonia.ReactiveUI; |
||||
|
using Avalonia.Generators.Tests.Views; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Generators.Tests; |
||||
|
|
||||
|
public class XamlXNameResolverTests |
||||
|
{ |
||||
|
[Theory] |
||||
|
[InlineData(View.NamedControl)] |
||||
|
[InlineData(View.XNamedControl)] |
||||
|
[InlineData(View.AttachedProps)] |
||||
|
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Named_Control(string resource) |
||||
|
{ |
||||
|
var xaml = await View.Load(resource); |
||||
|
var controls = ResolveNames(xaml); |
||||
|
|
||||
|
Assert.NotEmpty(controls); |
||||
|
Assert.Equal(1, controls.Count); |
||||
|
Assert.Equal("UserNameTextBox", controls[0].Name); |
||||
|
Assert.Contains(typeof(TextBox).FullName!, controls[0].TypeName); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(View.NamedControls)] |
||||
|
[InlineData(View.XNamedControls)] |
||||
|
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Named_Controls(string resource) |
||||
|
{ |
||||
|
var xaml = await View.Load(resource); |
||||
|
var controls = ResolveNames(xaml); |
||||
|
|
||||
|
Assert.NotEmpty(controls); |
||||
|
Assert.Equal(3, controls.Count); |
||||
|
Assert.Equal("UserNameTextBox", controls[0].Name); |
||||
|
Assert.Equal("PasswordTextBox", controls[1].Name); |
||||
|
Assert.Equal("SignUpButton", controls[2].Name); |
||||
|
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] |
||||
|
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Custom_Controls() |
||||
|
{ |
||||
|
var xaml = await View.Load(View.CustomControls); |
||||
|
var controls = ResolveNames(xaml); |
||||
|
|
||||
|
Assert.NotEmpty(controls); |
||||
|
Assert.Equal(3, controls.Count); |
||||
|
Assert.Equal("ClrNamespaceRoutedViewHost", controls[0].Name); |
||||
|
Assert.Equal("UriRoutedViewHost", controls[1].Name); |
||||
|
Assert.Equal("UserNameTextBox", controls[2].Name); |
||||
|
Assert.Contains(typeof(RoutedViewHost).FullName!, controls[0].TypeName); |
||||
|
Assert.Contains(typeof(RoutedViewHost).FullName!, controls[1].TypeName); |
||||
|
Assert.Contains("Controls.CustomTextBox", controls[2].TypeName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_When_Types_Contains_Generic_Arguments() |
||||
|
{ |
||||
|
var xaml = await View.Load(View.ViewWithGenericBaseView); |
||||
|
var controls = ResolveNames(xaml); |
||||
|
Assert.Equal(2, controls.Count); |
||||
|
|
||||
|
var currentControl = controls[0]; |
||||
|
Assert.Equal("Root", currentControl.Name); |
||||
|
Assert.Equal("global::Sample.App.BaseView<global::System.String>", currentControl.TypeName); |
||||
|
|
||||
|
currentControl = controls[1]; |
||||
|
Assert.Equal("NotAsRootNode", currentControl.Name); |
||||
|
Assert.Contains("Sample.App.BaseView", currentControl.TypeName); |
||||
|
Assert.Equal("global::Sample.App.BaseView<global::System.Int32>", currentControl.TypeName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Not_Resolve_Named_Controls_From_Avalonia_Markup_File_Without_Named_Controls() |
||||
|
{ |
||||
|
var xaml = await View.Load(View.NoNamedControls); |
||||
|
var controls = ResolveNames(xaml); |
||||
|
|
||||
|
Assert.Empty(controls); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Not_Resolve_Elements_From_DataTemplates() |
||||
|
{ |
||||
|
var xaml = await View.Load(View.DataTemplates); |
||||
|
var controls = ResolveNames(xaml); |
||||
|
|
||||
|
Assert.NotEmpty(controls); |
||||
|
Assert.Equal(2, controls.Count); |
||||
|
Assert.Equal("UserNameTextBox", controls[0].Name); |
||||
|
Assert.Equal("NamedListBox", controls[1].Name); |
||||
|
Assert.Contains(typeof(TextBox).FullName!, controls[0].TypeName); |
||||
|
Assert.Contains(typeof(ListBox).FullName!, controls[1].TypeName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Resolve_Names_From_Complex_Views() |
||||
|
{ |
||||
|
var xaml = await View.Load(View.SignUpView); |
||||
|
var controls = ResolveNames(xaml); |
||||
|
|
||||
|
Assert.NotEmpty(controls); |
||||
|
Assert.Equal(10, controls.Count); |
||||
|
Assert.Equal("UserNameTextBox", controls[0].Name); |
||||
|
Assert.Equal("UserNameValidation", controls[1].Name); |
||||
|
Assert.Equal("PasswordTextBox", controls[2].Name); |
||||
|
Assert.Equal("PasswordValidation", controls[3].Name); |
||||
|
Assert.Equal("AwesomeListView", controls[4].Name); |
||||
|
Assert.Equal("ConfirmPasswordTextBox", controls[5].Name); |
||||
|
Assert.Equal("ConfirmPasswordValidation", controls[6].Name); |
||||
|
Assert.Equal("SignUpButtonDescription", controls[7].Name); |
||||
|
Assert.Equal("SignUpButton", controls[8].Name); |
||||
|
Assert.Equal("CompoundValidation", controls[9].Name); |
||||
|
} |
||||
|
|
||||
|
private static IReadOnlyList<ResolvedName> ResolveNames(string xaml) |
||||
|
{ |
||||
|
var compilation = |
||||
|
View.CreateAvaloniaCompilation() |
||||
|
.WithCustomTextBox() |
||||
|
.WithBaseView(); |
||||
|
|
||||
|
var classResolver = new XamlXViewResolver( |
||||
|
new RoslynTypeSystem(compilation), |
||||
|
MiniCompiler.CreateDefault( |
||||
|
new RoslynTypeSystem(compilation), |
||||
|
MiniCompiler.AvaloniaXmlnsDefinitionAttribute)); |
||||
|
|
||||
|
var classInfo = classResolver.ResolveView(xaml); |
||||
|
var nameResolver = new XamlXNameResolver(); |
||||
|
return nameResolver.ResolveNames(classInfo.Xaml); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue