diff --git a/src/Controls/src/Core/MauiServiceAttribute.cs b/src/Controls/src/Core/MauiServiceAttribute.cs
new file mode 100644
index 000000000000..c78ee17a73f6
--- /dev/null
+++ b/src/Controls/src/Core/MauiServiceAttribute.cs
@@ -0,0 +1,30 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.Maui.Controls
+{
+ [AttributeUsage(AttributeTargets.Class)]
+ public class MauiServiceAttribute : Attribute
+ {
+ ///
+ /// Type gets registered in the DI container, defaults to Singleton
+ ///
+ public MauiServiceAttribute(ServiceLifetime lifetime = ServiceLifetime.Singleton) => Lifetime = lifetime;
+
+ ///
+ /// Lifetime of the Service type on which this attribute is defined.
+ ///
+ public ServiceLifetime Lifetime { get; }
+
+ ///
+ /// The type for which this service would be resolved for.
+ /// builder.Services.AddSingleton<INavigationService, NavigationService>();
+ ///
+ public Type? RegisterFor { get; set; }
+
+ ///
+ /// If set to true, makes use of the TryAdd method construct while registering the service.
+ ///
+ public bool UseTryAdd { get; set; }
+ }
+}
diff --git a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt
index 328746133294..1811b0618d88 100644
--- a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt
@@ -1,7 +1,26 @@
#nullable enable
*REMOVED*override Microsoft.Maui.Controls.RefreshView.MeasureOverride(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size
Microsoft.Maui.Controls.LayoutOptions.Equals(Microsoft.Maui.Controls.LayoutOptions other) -> bool
+Microsoft.Maui.Controls.MauiServiceAttribute
+Microsoft.Maui.Controls.MauiServiceAttribute.MauiServiceAttribute(Microsoft.Extensions.DependencyInjection.ServiceLifetime lifetime = Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton) -> void
+Microsoft.Maui.Controls.MauiServiceAttribute.RegisterFor.get -> System.Type?
+Microsoft.Maui.Controls.MauiServiceAttribute.RegisterFor.set -> void
+Microsoft.Maui.Controls.MauiServiceAttribute.Lifetime.get -> Microsoft.Extensions.DependencyInjection.ServiceLifetime
+Microsoft.Maui.Controls.MauiServiceAttribute.UseTryAdd.get -> bool
+Microsoft.Maui.Controls.MauiServiceAttribute.UseTryAdd.set -> void
Microsoft.Maui.Controls.Region.Equals(Microsoft.Maui.Controls.Region other) -> bool
+Microsoft.Maui.Controls.RouteAttribute
+Microsoft.Maui.Controls.RouteAttribute.ImplicitViewModel.get -> bool
+Microsoft.Maui.Controls.RouteAttribute.ImplicitViewModel.set -> void
+Microsoft.Maui.Controls.RouteAttribute.Route.get -> string?
+Microsoft.Maui.Controls.RouteAttribute.RouteAttribute() -> void
+Microsoft.Maui.Controls.RouteAttribute.RouteAttribute(string! route, Microsoft.Extensions.DependencyInjection.ServiceLifetime lifetime = Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton) -> void
+Microsoft.Maui.Controls.RouteAttribute.Routes.get -> string![]?
+Microsoft.Maui.Controls.RouteAttribute.Routes.set -> void
+Microsoft.Maui.Controls.RouteAttribute.Lifetime.get -> Microsoft.Extensions.DependencyInjection.ServiceLifetime
+Microsoft.Maui.Controls.RouteAttribute.Lifetime.set -> void
+Microsoft.Maui.Controls.RouteAttribute.ViewModelType.get -> System.Type?
+Microsoft.Maui.Controls.RouteAttribute.ViewModelType.set -> void
Microsoft.Maui.Controls.Shapes.Matrix.Equals(Microsoft.Maui.Controls.Shapes.Matrix other) -> bool
Microsoft.Maui.Controls.VisualElement.~VisualElement() -> void
override Microsoft.Maui.Controls.LayoutOptions.GetHashCode() -> int
diff --git a/src/Controls/src/Core/RouteAttribute.cs b/src/Controls/src/Core/RouteAttribute.cs
new file mode 100644
index 000000000000..e92084cdd030
--- /dev/null
+++ b/src/Controls/src/Core/RouteAttribute.cs
@@ -0,0 +1,38 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.Maui.Controls
+{
+ [AttributeUsage(AttributeTargets.Class)]
+ public class RouteAttribute : Attribute
+ {
+ public RouteAttribute() { }
+
+ public RouteAttribute(string route, ServiceLifetime lifetime = ServiceLifetime.Singleton)
+ => (Route, Lifetime) = (route, lifetime);
+
+ public string? Route { get; }
+
+ ///
+ /// Routes of the View (page) type on which this attribute is defined.
+ /// When both Routes and Route properties are defined, Routes takes precedence.
+ ///
+ public string[]? Routes { get; set; }
+
+ ///
+ /// Lifetime of the View (page) / ViewModel type on which this attribute is defined.
+ ///
+ public ServiceLifetime Lifetime { get; set; }
+
+ ///
+ /// Indicates whether the ViewModel should be associated automatically.
+ ///
+ public bool ImplicitViewModel { get; set; }
+
+ ///
+ /// Type of the ViewModel associated with the View (page) on which this attribute is defined.
+ /// When both ImplicitViewModel and ViewModelType properties are defined, ViewModelType takes precedence.
+ ///
+ public Type? ViewModelType { get; set; }
+ }
+}
diff --git a/src/Controls/src/SourceGen/Helper.cs b/src/Controls/src/SourceGen/Helper.cs
new file mode 100644
index 000000000000..aa4e92519d35
--- /dev/null
+++ b/src/Controls/src/SourceGen/Helper.cs
@@ -0,0 +1,52 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Microsoft.Maui.Controls.SourceGen
+{
+ internal static class Helper
+ {
+ internal const string Singleton = nameof(Singleton);
+ internal const string Scoped = nameof(Scoped);
+ internal const string Transient = nameof(Transient);
+
+ // Determine the namespace the class/enum/struct/record is declared in, if any
+ internal static string GetNamespace(BaseTypeDeclarationSyntax syntax)
+ {
+ // If we don't have a namespace at all we'll return an empty string
+ // This accounts for the "default namespace" case
+ string @namespace = string.Empty;
+
+ // Get the containing syntax node for the type declaration
+ // (could be a nested type, for example)
+ SyntaxNode? potentialNamespaceParent = syntax.Parent;
+
+ // Keep moving "out" of nested classes etc until we get to a namespace
+ // or until we run out of parents
+ while (potentialNamespaceParent is not null
+ && potentialNamespaceParent is not NamespaceDeclarationSyntax
+ && potentialNamespaceParent is not FileScopedNamespaceDeclarationSyntax)
+ {
+ potentialNamespaceParent = potentialNamespaceParent.Parent;
+ }
+
+ // Build up the final namespace by looping until we no longer have a namespace declaration
+ if (potentialNamespaceParent is BaseNamespaceDeclarationSyntax namespaceParent)
+ {
+ // We have a namespace. Use that as the type
+ @namespace = namespaceParent.Name.ToString();
+
+ // Keep moving "out" of the namespace declarations until we
+ // run out of nested namespace declarations
+ while (namespaceParent.Parent is NamespaceDeclarationSyntax parent)
+ {
+ // Add the outer namespace as a prefix to the final namespace
+ @namespace = $"{parent.Name}.{@namespace}";
+ namespaceParent = parent;
+ }
+ }
+
+ // return the final namespace
+ return @namespace;
+ }
+ }
+}
diff --git a/src/Controls/src/SourceGen/RouteIncrementalGenerator.cs b/src/Controls/src/SourceGen/RouteIncrementalGenerator.cs
new file mode 100644
index 000000000000..111bbc288b71
--- /dev/null
+++ b/src/Controls/src/SourceGen/RouteIncrementalGenerator.cs
@@ -0,0 +1,564 @@
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+using static Microsoft.Maui.Controls.SourceGen.Helper;
+
+namespace Microsoft.Maui.Controls.SourceGen
+{
+ [Generator(LanguageNames.CSharp)]
+ public partial class RouteIncrementalGenerator : IIncrementalGenerator
+ {
+ static readonly string Indent = new string('\x20', 8);
+ const string AutoGeneratedHeaderText = """
+ //------------------------------------------------------------------------------
+ //
+ // This code was generated by a .NET MAUI source generator.
+ //
+ // Changes to this file may cause incorrect behavior and will be lost if
+ // the code is regenerated.
+ //
+ //------------------------------------------------------------------------------
+ """;
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var classDeclarations = context.CompilationProvider.Combine(context.SyntaxProvider.CreateSyntaxProvider(
+ static (node, _) => node is ClassDeclarationSyntax,
+ static (ctx, _) => GetSemanticTargetForGeneration(ctx))
+ .Where(static x => x is not null)
+ .Collect());
+
+ context.RegisterSourceOutput(classDeclarations, (spc, source) => Execute(spc, source.Left, source.Right));
+ }
+
+ private static ClassDeclarationSyntax? GetSemanticTargetForGeneration(GeneratorSyntaxContext context)
+ {
+ if (context.Node is not ClassDeclarationSyntax cds)
+ {
+ return null;
+ }
+
+ if (cds.Identifier.ValueText == "MauiProgram")
+ {
+ foreach (var member in cds.Members)
+ {
+ if (member is MethodDeclarationSyntax { Identifier.ValueText: "CreateMauiApp" })
+ {
+ return cds;
+ }
+ }
+ }
+
+ var routeAttribute = cds.AttributeLists.SelectMany(x => x.Attributes)
+ .Where(a => a.Name.NormalizeWhitespace().ToFullString() == "Route")
+ .FirstOrDefault();
+
+ if (routeAttribute is not null)
+ {
+ return cds;
+ }
+
+ var serviceAttribute = cds.AttributeLists.SelectMany(x => x.Attributes)
+ .Where(a => a.Name.NormalizeWhitespace().ToFullString() == "MauiService")
+ .FirstOrDefault();
+
+ if (serviceAttribute is not null)
+ {
+ return cds;
+ }
+
+ if (cds.BaseList is not null)
+ {
+ foreach (var baseType in cds.BaseList.Types)
+ {
+ if (baseType.Type is IdentifierNameSyntax identifierName)
+ {
+ return identifierName.Identifier.ValueText switch
+ {
+ "Application" => cds,
+ "Shell" => cds,
+ _ => null,
+ };
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private static void Execute(SourceProductionContext context, Compilation compilation, ImmutableArray classes)
+ {
+ if (classes.IsDefaultOrEmpty)
+ {
+ return;
+ }
+
+ var mauiApp = GetMauiApp(classes);
+ var mauiStartup = GetMauiStartup(classes);
+ var mauiServices = GetMauiServices(classes);
+ var routedPages = GetRoutedPages(classes);
+ var shellPages = GetShellPages(classes);
+
+ var routes = new StringBuilder();
+ var services = new StringBuilder();
+
+ GenerateCode(compilation, routedPages, mauiServices, ref routes, ref services, Singleton);
+ GenerateCode(compilation, routedPages, mauiServices, ref routes, ref services, Scoped);
+ GenerateCode(compilation, routedPages, mauiServices, ref routes, ref services, Transient);
+
+ string shellRoutes;
+
+ foreach (var shellPage in shellPages)
+ {
+ shellRoutes = $$"""
+ {{AutoGeneratedHeaderText}}
+
+ namespace {{GetNamespace(shellPage)}};
+
+ partial class {{shellPage.Identifier.ValueText}}
+ {
+ static partial void RegisterRoutes();
+
+ static partial void RegisterRoutes()
+ {
+ {{routes.ToString().Trim()}}
+ }
+ }
+
+ """;
+
+ context.AddSource($"{shellPage.Identifier.ValueText}.sg.cs", shellRoutes);
+ }
+
+ if (mauiStartup is not null)
+ {
+ context.AddSource("MauiProgram.sg.cs", $$"""
+ {{AutoGeneratedHeaderText}}
+
+ using Microsoft.Extensions.DependencyInjection;
+
+ namespace {{GetNamespace(mauiStartup)}};
+
+ static partial class MauiProgram
+ {
+ static partial void ConfigureDependencies(this global::Microsoft.Maui.Hosting.MauiAppBuilder builder);
+
+ static partial void ConfigureDependencies(this global::Microsoft.Maui.Hosting.MauiAppBuilder builder)
+ {
+ {{services.ToString().Trim()}}
+ }
+ }
+
+ """);
+ }
+
+ static void GenerateCode(Compilation compilation, IEnumerable routedPages, IEnumerable mauiServices, ref StringBuilder routes, ref StringBuilder services, string lifetime)
+ {
+ if (lifetime is null)
+ {
+ return;
+ }
+
+ var addComment = true;
+ SymbolInfo symbolInfo;
+
+ foreach (var routedPage in routedPages.Where(x => x.Lifetime == lifetime))
+ {
+ foreach (var route in routedPage.Routes)
+ {
+ if (string.IsNullOrEmpty(route))
+ {
+ routes.AppendLine($"{Indent}Routing.RegisterRoute(nameof(global::{GetNamespace(routedPage.Type)}.{routedPage.Type.Identifier}), typeof(global::{GetNamespace(routedPage.Type)}.{routedPage.Type.Identifier}));");
+ }
+ else
+ {
+ routes.AppendLine($"{Indent}Routing.RegisterRoute({route}, typeof(global::{GetNamespace(routedPage.Type)}.{routedPage.Type.Identifier}));");
+ }
+ }
+
+ if (addComment)
+ {
+ addComment = false;
+ services.AppendLine($"{Indent}// {lifetime} Services");
+ }
+
+ switch (routedPage.ViewModelType)
+ {
+ case not null:
+ symbolInfo = compilation.GetSemanticModel(routedPage.ViewModelType.SyntaxTree).GetSymbolInfo(routedPage.ViewModelType);
+ services.AppendLine($"{Indent}builder.Services.{ServiceMethod(lifetime)}();");
+ break;
+ default:
+ if (routedPage.ImplicitViewModel)
+ {
+ services.AppendLine($"{Indent}builder.Services.{ServiceMethod(lifetime)}<{routedPage.Type.Identifier.ValueText.Replace("Page", "ViewModel")}>();");
+ }
+ break;
+ }
+
+ services.AppendLine($"{Indent}builder.Services.{ServiceMethod(lifetime)}();");
+ }
+
+ foreach (var service in mauiServices.Where(x => x.Lifetime == lifetime))
+ {
+ if (addComment)
+ {
+ addComment = false;
+ services.AppendLine($"{Indent}// {lifetime} Services");
+ }
+
+ switch (service.RegisterFor)
+ {
+ case not null:
+ symbolInfo = compilation.GetSemanticModel(service.RegisterFor.SyntaxTree).GetSymbolInfo(service.RegisterFor);
+ services.AppendLine($"{Indent}builder.Services.{ServiceMethod(lifetime, service.UseTryAdd)}();");
+ break;
+ default:
+ services.AppendLine($"{Indent}builder.Services.{ServiceMethod(lifetime, service.UseTryAdd)}();");
+ break;
+ }
+ }
+
+ if (!addComment)
+ {
+ services.AppendLine();
+ }
+ }
+
+ static string ServiceMethod(string lifetime, bool useTryAdd = false)
+ => useTryAdd ? $"TryAdd{lifetime}" : $"Add{lifetime}";
+ }
+
+ private static IEnumerable GetRoutedPages(ImmutableArray classDeclarations)
+ {
+ var routedPages = new List();
+
+ foreach (var cds in classDeclarations)
+ {
+ if (cds is null)
+ {
+ continue;
+ }
+
+ var routeAttribute = cds.AttributeLists.SelectMany(x => x.Attributes)
+ .Where(a => a.Name.NormalizeWhitespace().ToFullString() == "Route")
+ .FirstOrDefault();
+
+ if (routeAttribute is not null)
+ {
+ TypeSyntax? viewModelType = null;
+ var implicitViewModel = false;
+ var route = string.Empty;
+ var lifetime = Singleton;
+ var shellRoutes = new List();
+
+ var routeArgument = true;
+
+ IEnumerable? arguments = routeAttribute.ArgumentList?.Arguments;
+
+ if (arguments is not null)
+ {
+ lifetime = Singleton;
+
+ var argWithoutName = arguments.Count(a => a.NameColon is null && a.NameEquals is null);
+
+ foreach (var argument in arguments)
+ {
+ if (argument.NameColon is not null)
+ {
+ if (argument.NameColon.Name.Identifier.Text == "route")
+ {
+ route = GetRoute(cds, argument.Expression);
+ }
+ else if (argument.NameColon.Name.Identifier.Text == "lifetime")
+ {
+ lifetime = GetLifetime(argument.Expression);
+ }
+ }
+ else if (argument.NameEquals is not null)
+ {
+ if (argument.NameEquals.Name.Identifier.Text == "Routes")
+ {
+ if (argument.Expression is ImplicitArrayCreationExpressionSyntax implicitArrayCreationExpression)
+ {
+ if (implicitArrayCreationExpression.Initializer.Kind() == SyntaxKind.ArrayInitializerExpression)
+ {
+ foreach (var expression in implicitArrayCreationExpression.Initializer.Expressions)
+ {
+ shellRoutes.Add(GetRoute(cds, expression));
+ }
+ }
+ }
+ else if (argument.Expression is ArrayCreationExpressionSyntax arrayCreationExpression)
+ {
+ if (arrayCreationExpression?.Initializer?.Kind() == SyntaxKind.ArrayInitializerExpression)
+ {
+ foreach (var expression in arrayCreationExpression.Initializer.Expressions)
+ {
+ shellRoutes.Add(GetRoute(cds, expression));
+ }
+ }
+ }
+ }
+ else if (argument.NameEquals.Name.Identifier.Text == "Lifetime")
+ {
+ lifetime = GetLifetime(argument.Expression);
+ }
+ else if (argument.NameEquals.Name.Identifier.Text == "ImplicitViewModel")
+ {
+ if (argument.Expression is LiteralExpressionSyntax literalExpression)
+ {
+ _ = bool.TryParse(literalExpression.Token.ValueText, out implicitViewModel);
+ }
+ }
+ else if (argument.NameEquals.Name.Identifier.Text == "ViewModelType")
+ {
+ if (argument.Expression is TypeOfExpressionSyntax typeofExpression)
+ {
+ //viewModelType = $"global::{GetNamespace(cds)}.{((IdentifierNameSyntax)typeofExpression.Type).Identifier.Text}";
+ //viewModelType = ((IdentifierNameSyntax)typeofExpression.Type).Identifier.Text;
+ viewModelType = typeofExpression.Type;
+ }
+ }
+ }
+ else if (argument.Expression is not null)
+ {
+ if (argWithoutName == 1)
+ {
+ if (argument.Expression.IsKind(SyntaxKind.StringLiteralExpression)
+ || argument.Expression.IsKind(SyntaxKind.SimpleMemberAccessExpression))
+ {
+ route = GetRoute(cds, argument.Expression);
+ }
+ else
+ {
+ lifetime = GetLifetime(argument.Expression);
+ }
+ }
+ else
+ {
+ if (routeArgument)
+ {
+ routeArgument = false;
+ route = GetRoute(cds, argument.Expression);
+ }
+ else
+ {
+ lifetime = GetLifetime(argument.Expression);
+ }
+ }
+ }
+ }
+ }
+
+ if (shellRoutes.Count == 0)
+ {
+ shellRoutes.Add(route);
+ }
+
+ routedPages.Add(new RoutedPage(cds, shellRoutes, lifetime, implicitViewModel, viewModelType));
+ }
+ }
+
+ return routedPages;
+ }
+
+ private static IEnumerable GetShellPages(ImmutableArray classDeclarations)
+ {
+ var shellPages = new List();
+
+ foreach (var cds in classDeclarations)
+ {
+ if (cds is null)
+ {
+ continue;
+ }
+
+ if (cds.BaseList is not null)
+ {
+ foreach (var baseType in cds.BaseList.Types)
+ {
+ if (baseType.Type is IdentifierNameSyntax { Identifier.ValueText: "Shell" })
+ {
+ shellPages.Add(cds);
+ break;
+ }
+ }
+ }
+ }
+
+ return shellPages;
+ }
+
+ private static IEnumerable GetMauiServices(ImmutableArray classDeclarations)
+ {
+ var services = new List();
+
+ foreach (var cds in classDeclarations)
+ {
+ if (cds is null)
+ {
+ continue;
+ }
+
+ IEnumerable? arguments;
+
+ var serviceAttribute = cds.AttributeLists.SelectMany(al => al.Attributes)
+ .Where(a => a.Name.NormalizeWhitespace().ToFullString().Replace("Attribute", string.Empty) == "MauiService")
+ .FirstOrDefault();
+
+ if (serviceAttribute is not null)
+ {
+ arguments = serviceAttribute.ArgumentList?.Arguments;
+
+ var lifetime = Singleton;
+ TypeSyntax? registerFor = null;
+ var useTryAdd = false;
+
+ if (arguments is not null)
+ {
+ foreach (var argument in arguments)
+ {
+ if (argument.NameColon?.Name.Identifier.Text == "lifetime")
+ {
+ lifetime = GetLifetime(argument.Expression);
+ }
+
+ switch (argument.NameEquals?.Name.Identifier.Text)
+ {
+ case "RegisterFor" when argument.Expression is TypeOfExpressionSyntax typeOfExpression:
+ registerFor = typeOfExpression.Type;
+ break;
+ case "UseTryAdd" when argument.Expression is LiteralExpressionSyntax literalExpression:
+ _ = bool.TryParse(literalExpression.Token.ValueText, out useTryAdd);
+ break;
+ }
+
+ if (argument.Expression is not null)
+ {
+ lifetime = GetLifetime(argument.Expression);
+ }
+ }
+ }
+
+ services.Add(new Service(cds, lifetime, registerFor, useTryAdd));
+ }
+ }
+
+ return services;
+ }
+
+ private static ClassDeclarationSyntax? GetMauiApp(ImmutableArray classDeclarations)
+ {
+ foreach (var cds in classDeclarations)
+ {
+ if (cds is null)
+ {
+ continue;
+ }
+
+ if (cds.BaseList is not null)
+ {
+ foreach (var baseType in cds.BaseList.Types)
+ {
+ if (baseType.Type is IdentifierNameSyntax { Identifier.ValueText: "Application" })
+ {
+ return cds;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private static ClassDeclarationSyntax? GetMauiStartup(ImmutableArray classDeclarations)
+ {
+ foreach (var cds in classDeclarations)
+ {
+ if (cds is null)
+ {
+ continue;
+ }
+
+ if (cds.Identifier.ValueText == "MauiProgram")
+ {
+ foreach (var member in cds.Members)
+ {
+ if (member is MethodDeclarationSyntax { Identifier.ValueText: "CreateMauiApp" })
+ {
+ return cds;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private static string GetRoute(ClassDeclarationSyntax classDeclaration, ExpressionSyntax? expression) => expression switch
+ {
+ LiteralExpressionSyntax literalExpression => $"\"{literalExpression.Token.ValueText}\"",
+ InvocationExpressionSyntax invocationExpression => ((IdentifierNameSyntax)invocationExpression.Expression).Identifier.Text switch
+ {
+ "nameof" => $"nameof(global::{GetNamespace(classDeclaration)}.{invocationExpression.ArgumentList.Arguments})",
+ _ => string.Empty,// Default route
+ },
+ MemberAccessExpressionSyntax memberAccessExpression => $"{((IdentifierNameSyntax)memberAccessExpression.Expression).Identifier.Text}.{memberAccessExpression.Name.Identifier.Text}",
+ _ => string.Empty,// Default route
+ };
+
+ private static string GetLifetime(ExpressionSyntax? expression) => expression switch
+ {
+ IdentifierNameSyntax identifierName => identifierName.Identifier.ValueText,
+ MemberAccessExpressionSyntax memberAccessExpression => memberAccessExpression.Name.Identifier.Text,
+ _ => Singleton
+ };
+ }
+
+ internal class RoutedPage
+ {
+ public RoutedPage(
+ ClassDeclarationSyntax type,
+ IEnumerable routes,
+ string lifetime,
+ bool implicitViewModel = false,
+ TypeSyntax? viewModelType = null)
+ {
+ Type = type;
+ Routes = routes;
+ Lifetime = lifetime;
+ ImplicitViewModel = implicitViewModel;
+ ViewModelType = viewModelType;
+ }
+
+ public ClassDeclarationSyntax Type { get; }
+
+ public IEnumerable Routes { get; }
+
+ public string Lifetime { get; }
+
+ public bool ImplicitViewModel { get; }
+
+ public TypeSyntax? ViewModelType { get; }
+ }
+
+ internal class Service
+ {
+ public Service(ClassDeclarationSyntax type, string lifetime, TypeSyntax? registerFor, bool useTryAdd)
+ => (Type, Lifetime, RegisterFor, UseTryAdd) = (type, lifetime, registerFor, useTryAdd);
+
+ public ClassDeclarationSyntax Type { get; }
+
+ public string Lifetime { get; }
+
+ public TypeSyntax? RegisterFor { get; }
+
+ public bool UseTryAdd { get; }
+ }
+}
diff --git a/src/Controls/src/SourceGen/RouteSourceGenerator.cs b/src/Controls/src/SourceGen/RouteSourceGenerator.cs
new file mode 100644
index 000000000000..0b9bebf75ad2
--- /dev/null
+++ b/src/Controls/src/SourceGen/RouteSourceGenerator.cs
@@ -0,0 +1,178 @@
+using System.Linq;
+using System.Text;
+
+using Microsoft.CodeAnalysis;
+
+using static Microsoft.Maui.Controls.SourceGen.Helper;
+
+namespace Microsoft.Maui.Controls.SourceGen
+{
+ //[Generator(LanguageNames.CSharp)]
+ public class RouteSourceGenerator : ISourceGenerator
+ {
+ static readonly string Indent = new string('\x20', 8);
+ const string AutoGeneratedHeaderText = """
+ //------------------------------------------------------------------------------
+ //
+ // This code was generated by a .NET MAUI source generator.
+ //
+ // Changes to this file may cause incorrect behavior and will be lost if
+ // the code is regenerated.
+ //
+ //------------------------------------------------------------------------------
+ """;
+
+ public void Initialize(GeneratorInitializationContext context)
+ {
+ //#if DEBUG
+ // if (!System.Diagnostics.Debugger.IsAttached)
+ // {
+ // System.Diagnostics.Debugger.Launch();
+ // }
+ //#endif
+ context.RegisterForSyntaxNotifications(() => new RouteSyntaxReceiver());
+ }
+
+ public void Execute(GeneratorExecutionContext context)
+ {
+ if (context.SyntaxReceiver is not RouteSyntaxReceiver syntaxReceiver)
+ {
+ return;
+ }
+
+ var mauiApp = syntaxReceiver.MauiApp;
+ var mauiStartup = syntaxReceiver.MauiStartup;
+ var routedPages = syntaxReceiver.RoutedPages;
+ var shellPages = syntaxReceiver.ShellPages;
+
+ var routes = new StringBuilder();
+ var services = new StringBuilder();
+
+ GenerateCode(context, syntaxReceiver, ref routes, ref services, Singleton);
+ GenerateCode(context, syntaxReceiver, ref routes, ref services, Scoped);
+ GenerateCode(context, syntaxReceiver, ref routes, ref services, Transient);
+
+ string shellRoutes;
+
+ foreach (var shellPage in shellPages)
+ {
+ shellRoutes = $$"""
+ {{AutoGeneratedHeaderText}}
+
+ namespace {{GetNamespace(shellPage)}};
+
+ partial class {{shellPage.Identifier.ValueText}}
+ {
+ static partial void RegisterRoutes();
+
+ static partial void RegisterRoutes()
+ {
+ {{routes.ToString().Trim()}}
+ }
+ }
+
+ """;
+
+ context.AddSource($"{shellPage.Identifier.ValueText}.sg.cs", shellRoutes);
+ }
+
+ if (mauiStartup is not null)
+ {
+ context.AddSource("MauiProgram.sg.cs", $$"""
+ {{AutoGeneratedHeaderText}}
+
+ using Microsoft.Extensions.DependencyInjection;
+
+ namespace {{GetNamespace(mauiStartup)}};
+
+ static partial class MauiProgram
+ {
+ static partial void ConfigureDependencies(this global::Microsoft.Maui.Hosting.MauiAppBuilder builder);
+
+ static partial void ConfigureDependencies(this global::Microsoft.Maui.Hosting.MauiAppBuilder builder)
+ {
+ {{services.ToString().Trim()}}
+ }
+ }
+
+ """);
+ }
+
+ static void GenerateCode(GeneratorExecutionContext context, RouteSyntaxReceiver syntaxReceiver, ref StringBuilder routes, ref StringBuilder services, string lifetime)
+ {
+ if (lifetime is null)
+ {
+ return;
+ }
+
+ var addComment = true;
+ SymbolInfo symbolInfo;
+
+ foreach (var routedPage in syntaxReceiver.RoutedPages.Where(x => x.Lifetime == lifetime))
+ {
+ foreach (var route in routedPage.Routes)
+ {
+ if (string.IsNullOrEmpty(route))
+ {
+ routes.AppendLine($"{Indent}Routing.RegisterRoute(nameof(global::{GetNamespace(routedPage.Type)}.{routedPage.Type.Identifier}), typeof(global::{GetNamespace(routedPage.Type)}.{routedPage.Type.Identifier}));");
+ }
+ else
+ {
+ routes.AppendLine($"{Indent}Routing.RegisterRoute({route}, typeof(global::{GetNamespace(routedPage.Type)}.{routedPage.Type.Identifier}));");
+ }
+ }
+
+ if (addComment)
+ {
+ addComment = false;
+ services.AppendLine($"{Indent}// {lifetime} Services");
+ }
+
+ switch (routedPage.ViewModelType)
+ {
+ case not null:
+ symbolInfo = context.Compilation.GetSemanticModel(routedPage.ViewModelType.SyntaxTree).GetSymbolInfo(routedPage.ViewModelType);
+ services.AppendLine($"{Indent}builder.Services.{ServiceMethod(lifetime)}();");
+ break;
+ default:
+ if (routedPage.ImplicitViewModel)
+ {
+ services.AppendLine($"{Indent}builder.Services.{ServiceMethod(lifetime)}<{routedPage.Type.Identifier.ValueText.Replace("Page", "ViewModel")}>();");
+ }
+ break;
+ }
+
+ services.AppendLine($"{Indent}builder.Services.{ServiceMethod(lifetime)}();");
+ }
+
+ foreach (var service in syntaxReceiver.Services.Where(x => x.Lifetime == lifetime))
+ {
+ if (addComment)
+ {
+ addComment = false;
+ services.AppendLine($"{Indent}// {lifetime} Services");
+ }
+
+ switch (service.RegisterFor)
+ {
+ case not null:
+ symbolInfo = context.Compilation.GetSemanticModel(service.RegisterFor.SyntaxTree).GetSymbolInfo(service.RegisterFor);
+ services.AppendLine($"{Indent}builder.Services.{ServiceMethod(lifetime, service.UseTryAdd)}();");
+ break;
+ default:
+ services.AppendLine($"{Indent}builder.Services.{ServiceMethod(lifetime, service.UseTryAdd)}();");
+ break;
+ }
+ }
+
+ if (!addComment)
+ {
+ services.AppendLine();
+ }
+ }
+
+ static string ServiceMethod(string lifetime, bool useTryAdd = false)
+ => useTryAdd ? $"TryAdd{lifetime}" : $"Add{lifetime}";
+ }
+ }
+}
diff --git a/src/Controls/src/SourceGen/RouteSyntaxReceiver.cs b/src/Controls/src/SourceGen/RouteSyntaxReceiver.cs
new file mode 100644
index 000000000000..3007891551b5
--- /dev/null
+++ b/src/Controls/src/SourceGen/RouteSyntaxReceiver.cs
@@ -0,0 +1,219 @@
+using System.Collections.Generic;
+using System.Linq;
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+using static Microsoft.Maui.Controls.SourceGen.Helper;
+
+namespace Microsoft.Maui.Controls.SourceGen
+{
+ public class RouteSyntaxReceiver : ISyntaxReceiver
+ {
+ const string RouteAttribute = "Microsoft.Maui.Controls.RouteAttribute";
+ const string MauiServiceAttribute = "Microsoft.Maui.Controls.MauiServiceAttribute";
+
+ private readonly List _services = new();
+ private readonly List _routedPages = new();
+ private readonly List _shellPages = new();
+
+ internal IEnumerable Services => _services;
+
+ internal IEnumerable RoutedPages => _routedPages;
+
+ internal IEnumerable ShellPages => _shellPages;
+
+ internal ClassDeclarationSyntax? MauiApp { get; private set; }
+
+ internal ClassDeclarationSyntax? MauiStartup { get; private set; }
+
+ public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
+ {
+ if (syntaxNode is not ClassDeclarationSyntax cds)
+ {
+ return;
+ }
+
+ if (cds.BaseList is not null)
+ {
+ foreach (var baseType in cds.BaseList.Types)
+ {
+ if (baseType.Type is not IdentifierNameSyntax identifierName)
+ {
+ continue;
+ }
+
+ if (identifierName.Identifier.ValueText == "Application")
+ {
+ MauiApp = cds;
+ break;
+ }
+
+ if (identifierName.Identifier.ValueText == "Shell")
+ {
+ _shellPages.Add(cds);
+ break;
+ }
+ }
+ }
+
+ if (cds.Identifier.ValueText == "MauiProgram")
+ {
+ foreach (var member in cds.Members)
+ {
+ if (member is MethodDeclarationSyntax { Identifier.ValueText: "CreateMauiApp" })
+ {
+ MauiStartup = cds;
+ break;
+ }
+ }
+ }
+ else if (cds.AttributeLists.Count > 0)
+ {
+ IEnumerable? arguments;
+
+ var routeAttributes = cds.AttributeLists.SelectMany(al => al.Attributes)
+ .Where(a => a.Name.NormalizeWhitespace().ToFullString().Replace("Attribute", string.Empty) == "Route");
+
+ var routeAttribute = routeAttributes.FirstOrDefault();
+
+ var serviceAttributes = cds.AttributeLists.SelectMany(al => al.Attributes)
+ .Where(a => a.Name.NormalizeWhitespace().ToFullString().Replace("Attribute", string.Empty) == "MauiService");
+
+ var serviceAttribute = serviceAttributes.FirstOrDefault();
+
+ string lifetime;
+
+ if (routeAttribute is not null)
+ {
+ TypeSyntax? viewModelType = null;
+ var implicitViewModel = false;
+ var route = string.Empty;
+ var routes = new List();
+
+ var routeArgument = true;
+ lifetime = Singleton;
+ arguments = routeAttribute.ArgumentList?.Arguments;
+
+ if (arguments is not null)
+ {
+ foreach (var argument in arguments)
+ {
+ switch (argument.NameColon?.Name.Identifier.Text)
+ {
+ case "route":
+ route = GetRoute(cds, argument.Expression);
+ break;
+ case "lifetime":
+ lifetime = GetLifetime(argument.Expression);
+ break;
+ }
+
+ switch (argument.NameEquals?.Name.Identifier.Text)
+ {
+ case "Routes" when argument.Expression is ArrayCreationExpressionSyntax arrayExpression:
+ if (arrayExpression?.Initializer?.Kind() == SyntaxKind.ArrayInitializerExpression)
+ {
+ routes.AddRange(arrayExpression.Initializer.Expressions.Select(expr => GetRoute(cds, expr)));
+ }
+ break;
+ case "Routes" when argument.Expression is ImplicitArrayCreationExpressionSyntax implicitArrayExpression:
+ if (implicitArrayExpression.Initializer.Kind() == SyntaxKind.ArrayInitializerExpression)
+ {
+ routes.AddRange(implicitArrayExpression.Initializer.Expressions.Select(expr => GetRoute(cds, expr)));
+ }
+ break;
+ case "Lifetime":
+ lifetime = GetLifetime(argument.Expression);
+ break;
+ case "ImplicitViewModel" when argument.Expression is LiteralExpressionSyntax literalExpression:
+ _ = bool.TryParse(literalExpression.Token.ValueText, out implicitViewModel);
+ break;
+ case "ViewModelType" when argument.Expression is TypeOfExpressionSyntax typeOfExpression:
+ viewModelType = typeOfExpression.Type;
+ break;
+ }
+
+ if (argument.NameColon is null && argument.NameEquals is null)
+ {
+ if (routeArgument)
+ {
+ routeArgument = false;
+ route = GetRoute(cds, argument.Expression);
+ }
+ else
+ {
+ lifetime = GetLifetime(argument.Expression);
+ }
+ }
+ }
+ }
+
+ if (routes.Count == 0)
+ {
+ routes.Add(route);
+ }
+
+ _routedPages.Add(new RoutedPage(cds, routes, lifetime, implicitViewModel, viewModelType));
+ }
+
+ if (serviceAttribute is not null)
+ {
+ arguments = serviceAttribute.ArgumentList?.Arguments;
+
+ lifetime = Singleton;
+ TypeSyntax? registerFor = null;
+ var useTryAdd = false;
+
+ if (arguments is not null)
+ {
+ foreach (var argument in arguments)
+ {
+ if (argument.NameColon?.Name.Identifier.Text == "lifetime")
+ {
+ lifetime = GetLifetime(argument.Expression);
+ }
+
+ switch (argument.NameEquals?.Name.Identifier.Text)
+ {
+ case "RegisterFor" when argument.Expression is TypeOfExpressionSyntax typeOfExpression:
+ registerFor = typeOfExpression.Type;
+ break;
+ case "UseTryAdd" when argument.Expression is LiteralExpressionSyntax literalExpression:
+ _ = bool.TryParse(literalExpression.Token.ValueText, out useTryAdd);
+ break;
+ }
+
+ if (argument.Expression is not null)
+ {
+ lifetime = GetLifetime(argument.Expression);
+ }
+ }
+ }
+
+ _services.Add(new Service(cds, lifetime, registerFor, useTryAdd));
+ }
+ }
+ }
+
+ private static string GetRoute(ClassDeclarationSyntax classDeclaration, ExpressionSyntax? expression) => expression switch
+ {
+ LiteralExpressionSyntax literalExpression => $"\"{literalExpression.Token.ValueText}\"",
+ InvocationExpressionSyntax invocationExpression => ((IdentifierNameSyntax)invocationExpression.Expression).Identifier.Text switch
+ {
+ "nameof" => $"nameof(global::{GetNamespace(classDeclaration)}.{invocationExpression.ArgumentList.Arguments})",
+ _ => string.Empty,// Default route
+ },
+ MemberAccessExpressionSyntax memberAccessExpression => $"{((IdentifierNameSyntax)memberAccessExpression.Expression).Identifier.Text}.{memberAccessExpression.Name.Identifier.Text}",
+ _ => string.Empty,// Default route
+ };
+
+ private static string GetLifetime(ExpressionSyntax? expression) => expression switch
+ {
+ IdentifierNameSyntax identifierName => identifierName.Identifier.ValueText,
+ MemberAccessExpressionSyntax memberAccessExpression => memberAccessExpression.Name.Identifier.Text,
+ _ => Singleton
+ };
+ }
+}