diff --git a/samples/Generators.Sandbox/App.xaml.cs b/samples/Generators.Sandbox/App.xaml.cs index 6118b3f177..e592923eea 100644 --- a/samples/Generators.Sandbox/App.xaml.cs +++ b/samples/Generators.Sandbox/App.xaml.cs @@ -4,9 +4,13 @@ using Generators.Sandbox.ViewModels; namespace Generators.Sandbox; -public class App : Application +public partial class App : Application { - public override void Initialize() => AvaloniaXamlLoader.Load(this); + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + Resources.MergedDictionaries.Add(new global::Sandbox.MyResources()); + } public override void OnFrameworkInitializationCompleted() { @@ -17,4 +21,4 @@ public class App : Application view.Show(); base.OnFrameworkInitializationCompleted(); } -} \ No newline at end of file +} diff --git a/samples/Generators.Sandbox/MyResources.axaml b/samples/Generators.Sandbox/MyResources.axaml new file mode 100644 index 0000000000..70d192a299 --- /dev/null +++ b/samples/Generators.Sandbox/MyResources.axaml @@ -0,0 +1,6 @@ + + + + diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 4b9e608f5c..ce42224f38 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/IComponentConnector.cs b/src/Markup/Avalonia.Markup.Xaml/IComponentConnector.cs new file mode 100644 index 0000000000..84cf14a0e2 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/IComponentConnector.cs @@ -0,0 +1,16 @@ +namespace Avalonia.Markup.Xaml; + +public interface IComponentConnector +{ + /// + /// Loads the compiled page of a component. + /// + void InitializeComponent(); + + /// + /// Attaches names to compiled content. + /// + /// An identifier token to distinguish calls. + /// The target to connect names to. + void Connect (int connectionId, object target); +} diff --git a/src/tools/Avalonia.Generators/Common/Domain/IClassResolver.cs b/src/tools/Avalonia.Generators/Common/Domain/IClassResolver.cs new file mode 100644 index 0000000000..e08ab46027 --- /dev/null +++ b/src/tools/Avalonia.Generators/Common/Domain/IClassResolver.cs @@ -0,0 +1,10 @@ +using XamlX.Ast; + +namespace Avalonia.Generators.Common.Domain; + +internal interface IClassResolver +{ + ResolvedClass? ResolveClass(XamlDocument xaml); +} + +internal record ResolvedClass(string TypeName, string ClassModifier); diff --git a/src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs b/src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs index 4b426172f8..c1ebdc3e45 100644 --- a/src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs +++ b/src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs @@ -5,5 +5,5 @@ namespace Avalonia.Generators.Common.Domain; internal interface ICodeGenerator { - string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable names); + string GenerateCode(ResolvedView view, IEnumerable names); } diff --git a/src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs b/src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs index 49ceb6f69e..1f4677aa23 100644 --- a/src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs +++ b/src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs @@ -8,4 +8,8 @@ internal interface IViewResolver ResolvedView? ResolveView(string xaml); } -internal record ResolvedView(string ClassName, IXamlType XamlType, string Namespace, XamlDocument Xaml); +internal record ResolvedView(string ClassName, IXamlType XamlType, string Namespace, XamlDocument Xaml) +{ + public bool IsStyledElement { get; init; } + public bool HasClass { get; init; } +} diff --git a/src/tools/Avalonia.Generators/Common/XamlXClassResolver.cs b/src/tools/Avalonia.Generators/Common/XamlXClassResolver.cs new file mode 100644 index 0000000000..9e9355687d --- /dev/null +++ b/src/tools/Avalonia.Generators/Common/XamlXClassResolver.cs @@ -0,0 +1,27 @@ +using Avalonia.Generators.Common.Domain; +using XamlX.Ast; + +namespace Avalonia.Generators.Common; + +internal class XamlXClassResolver : IClassResolver, IXamlAstVisitor +{ + public ResolvedClass? ResolveClass(XamlDocument xaml) + { + return null; + } + + public IXamlAstNode Visit(IXamlAstNode node) + { + throw new System.NotImplementedException(); + } + + public void Push(IXamlAstNode node) + { + throw new System.NotImplementedException(); + } + + public void Pop() + { + throw new System.NotImplementedException(); + } +} diff --git a/src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs b/src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs index b0495b2840..2e26460a34 100644 --- a/src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs +++ b/src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs @@ -13,8 +13,6 @@ internal class XamlXViewResolver : IViewResolver, IXamlAstVisitor { private readonly RoslynTypeSystem _typeSystem; private readonly MiniCompiler _compiler; - private readonly bool _checkTypeValidity; - private readonly Action? _onTypeInvalid; private readonly Action? _onUnhandledError; private ResolvedView? _resolvedClass; @@ -23,12 +21,8 @@ internal class XamlXViewResolver : IViewResolver, IXamlAstVisitor public XamlXViewResolver( RoslynTypeSystem typeSystem, MiniCompiler compiler, - bool checkTypeValidity = false, - Action? onTypeInvalid = null, Action? onUnhandledError = null) { - _checkTypeValidity = checkTypeValidity; - _onTypeInvalid = onTypeInvalid; _onUnhandledError = onUnhandledError; _typeSystem = typeSystem; _compiler = compiler; @@ -61,10 +55,6 @@ internal class XamlXViewResolver : IViewResolver, IXamlAstVisitor 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 && @@ -72,21 +62,16 @@ internal class XamlXViewResolver : IViewResolver, IXamlAstVisitor 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!); + var clrType = objectNode.Type.GetClrType(); + _resolvedClass = new ResolvedView(className, clrType, nameSpace, _xaml!) + { + IsStyledElement = clrType.IsAvaloniaStyledElement(), + HasClass = _typeSystem.FindType(text.Text) is not null + }; return node; } } diff --git a/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs b/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs index b1f7738a8a..170c761edd 100644 --- a/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs +++ b/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs @@ -6,7 +6,6 @@ namespace Avalonia.Generators; internal static class GeneratorContextExtensions { private const string UnhandledErrorDescriptorId = "AXN0002"; - private const string InvalidTypeDescriptorId = "AXN0001"; public static string GetMsBuildProperty( this GeneratorExecutionContext context, @@ -24,11 +23,6 @@ internal static class GeneratorContextExtensions error.Message, 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, string? description = null) => context.ReportDiagnostic( Diagnostic.Create( diff --git a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs index 67389ef826..e90c9b8f73 100644 --- a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs +++ b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs @@ -12,49 +12,52 @@ internal class AvaloniaNameGenerator : INameGenerator private readonly ViewFileNamingStrategy _naming; private readonly IGlobPattern _pathPattern; private readonly IGlobPattern _namespacePattern; - private readonly IViewResolver _classes; + private readonly IViewResolver _views; private readonly INameResolver _names; + private readonly IClassResolver _classes; private readonly ICodeGenerator _code; public AvaloniaNameGenerator( ViewFileNamingStrategy naming, IGlobPattern pathPattern, IGlobPattern namespacePattern, - IViewResolver classes, + IViewResolver views, INameResolver names, + IClassResolver classes, ICodeGenerator code) { _naming = naming; _pathPattern = pathPattern; _namespacePattern = namespacePattern; - _classes = classes; + _views = views; _names = names; + _classes = classes; _code = code; } - public IEnumerable GenerateNameReferences(IEnumerable additionalFiles, CancellationToken cancellationToken) + public IEnumerable GenerateSupportClasses(IEnumerable additionalFiles, CancellationToken cancellationToken) { - var resolveViews = - from file in additionalFiles - let filePath = file.Path - where (filePath.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase) || - filePath.EndsWith(".paml", StringComparison.OrdinalIgnoreCase) || - filePath.EndsWith(".axaml", StringComparison.OrdinalIgnoreCase)) && - _pathPattern.Matches(filePath) - let xaml = file.GetText(cancellationToken)?.ToString() - where xaml != null - let view = _classes.ResolveView(xaml) - where view != null && _namespacePattern.Matches(view.Namespace) - select view; + var resolveViews = additionalFiles.Select(file => (file, filePath: file.Path)) + .Where(t => + (t.filePath.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase) || + t.filePath.EndsWith(".paml", StringComparison.OrdinalIgnoreCase) || + t.filePath.EndsWith(".axaml", StringComparison.OrdinalIgnoreCase)) && + _pathPattern.Matches(t.filePath)) + .Select(t => t.file.GetText(cancellationToken)?.ToString()) + .Where(xaml => xaml != null) + .Select(xaml => _views.ResolveView(xaml!)) + .Where(view => view != null && _namespacePattern.Matches(view.Namespace))! + .ToArray(); - 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); + var xNameFiles = resolveViews + .Select(view => (view, names: _names.ResolveNames(view.Xaml))) + .Where(t => !t.view.HasClass || (t.view.IsStyledElement && t.names.Any())) + .Select(t => ( + fileName: ResolveViewFileName(t.view, _naming), + code: _code.GenerateCode(t.view, t.names))) + .Select(t => new GeneratedPartialClass(t.fileName, t.code)); - return query; + return xNameFiles; } private static string ResolveViewFileName(ResolvedView view, ViewFileNamingStrategy strategy) => strategy switch diff --git a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs index e93895db2e..8a3e051626 100644 --- a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs +++ b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs @@ -27,7 +27,7 @@ public class AvaloniaNameSourceGenerator : ISourceGenerator return; } - var partials = generator.GenerateNameReferences(ResolveAdditionalFiles(context), context.CancellationToken); + var partials = generator.GenerateSupportClasses(ResolveAdditionalFiles(context), context.CancellationToken); foreach (var (fileName, content) in partials) { if(context.CancellationToken.IsCancellationRequested) @@ -77,10 +77,10 @@ public class AvaloniaNameSourceGenerator : ISourceGenerator options.AvaloniaNameGeneratorViewFileNamingStrategy, new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByPath), new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByNamespace), - new XamlXViewResolver(types, compiler, true, - type => context.ReportNameGeneratorInvalidType(type), + new XamlXViewResolver(types, compiler, error => context.ReportNameGeneratorUnhandledError(error)), new XamlXNameResolver(options.AvaloniaNameGeneratorClassFieldModifier), + new XamlXClassResolver(), generator); } } diff --git a/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs index 5b44de43c1..15e501128b 100644 --- a/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs +++ b/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs @@ -6,7 +6,7 @@ namespace Avalonia.Generators.NameGenerator; internal interface INameGenerator { - IEnumerable GenerateNameReferences(IEnumerable additionalFiles, CancellationToken cancellationToken); + IEnumerable GenerateSupportClasses(IEnumerable additionalFiles, CancellationToken cancellationToken); } internal record GeneratedPartialClass(string FileName, string Content); diff --git a/src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs index 3dd058af0b..06fdffa13c 100644 --- a/src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs +++ b/src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs @@ -27,7 +27,7 @@ internal class InitializeComponentCodeGenerator : ICodeGenerator _diagnosticsAreConnected = avaloniaNameGeneratorAttachDevTools && types.FindAssembly("Avalonia.Diagnostics") != null; } - public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable names) + public string GenerateCode(ResolvedView view, IEnumerable names) { var properties = new List(); var initializations = new List(); @@ -53,7 +53,9 @@ internal class InitializeComponentCodeGenerator : ICodeGenerator hasNames = true; } - var attachDevTools = _diagnosticsAreConnected && IsWindow(xamlType); + var attachDevTools = _diagnosticsAreConnected && IsWindow(view.XamlType); + + var baseClassPart = view.HasClass ? "" : $": global::{view.XamlType.FullName}"; return $@"// @@ -61,9 +63,9 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace {nameSpace} +namespace {view.Namespace} {{ - partial class {className} + partial class {view.ClassName} {baseClassPart} {{ {string.Join("\n", properties)} diff --git a/src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs index 8b295acd6b..4754839b3b 100644 --- a/src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs +++ b/src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs @@ -10,7 +10,7 @@ internal class OnlyPropertiesCodeGenerator : ICodeGenerator private string _generatorName = typeof(OnlyPropertiesCodeGenerator).FullName; private string _generatorVersion = typeof(OnlyPropertiesCodeGenerator).Assembly.GetName().Version.ToString(); - public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable names) + public string GenerateCode(ResolvedView view, IEnumerable names) { var namedControls = names .Select(info => " " + @@ -24,9 +24,9 @@ internal class OnlyPropertiesCodeGenerator : ICodeGenerator using Avalonia.Controls; -namespace {nameSpace} +namespace {view.Namespace} {{ - partial class {className} + partial class {view.Xaml} : global::{view.XamlType.FullName} {{ {lines} }} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs b/tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs index 15fb282ed9..db1b78e60f 100644 --- a/tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs @@ -52,7 +52,7 @@ public class InitializeComponentTests var generatorVersion = typeof(InitializeComponentCodeGenerator).Assembly.GetName().Version?.ToString(); var code = generator - .GenerateCode("SampleView", "Sample.App", classInfo.XamlType, names) + .GenerateNamesSupportCode("SampleView", "Sample.App", classInfo.XamlType, names) .Replace("\r", string.Empty); var expected = (await InitializeComponentCode.Load(expectation)) diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs b/tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs index 3f498c2be2..93bec19325 100644 --- a/tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs @@ -45,7 +45,7 @@ public class OnlyPropertiesTests var generatorVersion = typeof(OnlyPropertiesCodeGenerator).Assembly.GetName().Version?.ToString(); var code = generator - .GenerateCode("SampleView", "Sample.App", classInfo.XamlType, names) + .GenerateNamesSupportCode("SampleView", "Sample.App", classInfo.XamlType, names) .Replace("\r", string.Empty); var expected = (await OnlyPropertiesCode.Load(expectation))