Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shell routing and Service registration automation #13402

Closed

Conversation

egvijayanand
Copy link
Contributor

@egvijayanand egvijayanand commented Feb 16, 2023

Description of Change

Attribute-based shell routing and service registration in the DI container. Automated with source generators.

Below are the two attributes that are defined to generate the source that automates the tasks while defining a shell route or to add a DI service into the .NET MAUI startup pipeline.

  • Route
  • MauiService

Route attribute

This can be defined on any Page inherited type so that it can be added to the Routes collection. In addition, the type will also be added to the DI service in the startup.

[Route]
public class SettingsPage : ContentPage
{
}

By default, the route name generated would be the name of the type itself. But can be modified with its properties.

Defines a constructor to help in the generalized case with two parameters for route and lifetime.

public class RouteAttribute : Attribute
{
  public RouteAttribute(string route, ServiceLifetime lifetime = ServiceLifetime.Singleton)
    => (Route, Lifetime) = (route, lifetime);
}

The Route attribute defines the following properties:

Property Type Remarks
Route string Defines a single route definition for the page.
Routes string[] Defines multiple route definitions for the same page and takes precedence over the Route property.
Lifetime ServiceLifetime Defines the lifetime of the page in the DI container. Defaults to Singleton if not defined.
ImplicitViewModel bool Usually, the ViewModel and the View (page here) are associated by names. In such cases, the name of the ViewModel is derived from the View, when this property is set to true.
ViewModelType Type Explicit definition of the ViewModel type and takes precedence over the ImplicitViewModel property.
[Route("settings")]
public class SettingsPage : ContentPage
{
}
[Route("search", ServiceLifetime.Transient, ImplicitViewModel = true)]
public class SearchPage : ContentPage
{
}

MauiService attribute

This can be defined on any type so that it can be added to the Services collection. of the MauiAppBuilder instance as a DI service in the .NET MAUI startup.

[MauiService]
public class DialogService
{
}

Defines a constructor to help in the generalized case that takes lifetime as a parameter.

[MauiService(ServiceLifetime.Transient)]
public class AppThemePopup : Popup
{
}

The MauiService attribute defines the following properties.

Property Type Remarks
Lifetime ServiceLifetime Defines the lifetime of the page in the DI container. Defaults to Singleton if not defined.
UseTryAdd bool Makes use of the TryAdd method construct while adding the type in the Services collection when this property is set to true.
RegisterFor Type Register the type for the type defined in this property, helpful for the type to be registered against an interface.
[MauiService(RegisterFor = typeof(INavigationService))]
public class NavigationService : INavigationService
{
}
[MauiService(UseTryAdd = true)]
public class MediaService
{
}

Output:

For each of the Shell pages defined, a partial method of the name RegisterRoutes with all those routes added as its method definition will get auto-generated. This method needs to be invoked in the constructor of the shell definition.

Assuming MauiApp1 as the project root namespace for the sample code.

User definition:

namespace MauiApp1;

public partial class AppShell : Shell
{
  public AppShell()
  {
    // Only relevant code is shown here. If defined as XAML, InitializeComponent(); would be here.
    RegisterRoutes();
  }
}

Source generated:

namespace MauiApp1;

partial class AppShell : Shell
{
  static partial void RegisterRoutes();

  static partial void RegisterRoutes()
  {
    Routing.RegisterRoute(nameof(global::MauiApp1.Views.SettingsPage), typeof(global::MauiApp1.Views.SettingsPage));
    Routing.RegisterRoute("search", typeof(global::MauiApp1.Views.SearchPage));
  }
}

For the services, a partial method of the name ConfigureDependencies with all those types in the Services collection as its method definition will get auto-generated. This method needs to be invoked within the CreateMauiApp() method.

User definition:

namespace MauiApp1;

public static partial class MauiProgram
{
  public static MauiApp CreateMauiApp()
  {
    // Only relevant code is shown here.
    var builder = MauiApp.CreateBuilder();
    // All other definitions
    builder.ConfigureDependencies();
    return builder;
  }
}

Source generated:

namespace MauiApp1;

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)
  {
    builder.Services.AddSingleton<global::MauiApp1.Services.DialogService>();
    builder.Services.AddSingleton<global::MauiApp1.Services.INavigationService, global::MauiApp1.Services.NavigationService>();
    builder.Services.TryAddSingleton<MediaService>();

    builder.Services.AddTransient<global::MauiApp1.Views.AppThemePopup>();
  }
}

Issues Fixed

Fixes #5312

@ghost ghost added the community ✨ Community Contribution label Feb 16, 2023
@ghost
Copy link

ghost commented Feb 16, 2023

Hey there @egvijayanand! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@dnfadmin
Copy link

dnfadmin commented Feb 16, 2023

CLA assistant check
All CLA requirements met.

@Eilon Eilon added the area-controls-shell Shell Navigation, Routes, Tabs, Flyout label Feb 16, 2023
@github-actions
Copy link
Contributor

Thank you for your pull request. We are auto-formating your source code to follow our code guidelines.

src/Controls/src/SourceGen/RouteSyntaxReceiver.cs Outdated Show resolved Hide resolved
src/Controls/src/SourceGen/RouteSyntaxReceiver.cs Outdated Show resolved Hide resolved
src/Controls/src/SourceGen/RouteSyntaxReceiver.cs Outdated Show resolved Hide resolved
src/Controls/src/SourceGen/RouteSyntaxReceiver.cs Outdated Show resolved Hide resolved
src/Controls/src/SourceGen/RouteSyntaxReceiver.cs Outdated Show resolved Hide resolved
Copy link
Member

@mandel-macaque mandel-macaque left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking much better, thanks a lot for the effort. I think we can improve the code a little more taking into account that some else clauses are not needed and that we can switch on a string?

src/Controls/src/SourceGen/Helper.cs Outdated Show resolved Hide resolved
src/Controls/src/SourceGen/RouteSyntaxReceiver.cs Outdated Show resolved Hide resolved
src/Controls/src/SourceGen/RouteSyntaxReceiver.cs Outdated Show resolved Hide resolved
src/Controls/src/SourceGen/RouteSyntaxReceiver.cs Outdated Show resolved Hide resolved
src/Controls/src/SourceGen/RouteSyntaxReceiver.cs Outdated Show resolved Hide resolved
src/Controls/src/SourceGen/RouteSourceGenerator.cs Outdated Show resolved Hide resolved
src/Controls/src/SourceGen/RouteSourceGenerator.cs Outdated Show resolved Hide resolved
src/Controls/src/SourceGen/RouteSourceGenerator.cs Outdated Show resolved Hide resolved
src/Controls/src/SourceGen/RouteSyntaxReceiver.cs Outdated Show resolved Hide resolved
src/Controls/src/SourceGen/RouteSyntaxReceiver.cs Outdated Show resolved Hide resolved
@egvijayanand
Copy link
Contributor Author

@dotnet-policy-service agree

@pictos
Copy link
Contributor

pictos commented Feb 24, 2023

@egvijayanand FYI, @PureWeen opened a discussion around this feature on MCT repo

Not sure if this will be approved here or there, but in any case, you should use IIncrementalSourceGenerator instead, for better performance.

@egvijayanand
Copy link
Contributor Author

Not sure if this will be approved here or there, but in any case, you should use IIncrementalSourceGenerator instead, for better performance.

Even I've no clarity on whether this would be part of .NET MAUI / Toolkit.

Still, reviews are being done. Will check on the Incremental part of SG.

Copy link
Member

@mandel-macaque mandel-macaque left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not my area of expertise so we should have another review (I'll ping the right person). Love the code changes, code is much nicer to read THANKS a lot for the hard work.

I left two nits, definitely not blockers but nice to have for nullability.

}

var addComment = true;
SymbolInfo symbolInfo;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: (and I'm not sure)

Suggested change
SymbolInfo symbolInfo;
SymbolInfo? symbolInfo;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the return value is non-nullable from the Semantic Model of the SG, didn't consider it as marking as nullable.

{
IdentifierNameSyntax identifierName => identifierName.Identifier.ValueText,
MemberAccessExpressionSyntax memberAccessExpression => memberAccessExpression.Name.Identifier.Text,
_ => Singleton
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the service lifetime will have the default value of Singleton returned from the default/discard clause of the switch expression, didn't consider marking it as nullable.

@egvijayanand
Copy link
Contributor Author

Not my area of expertise so we should have another review (I'll ping the right person). Love the code changes, code is much nicer to read THANKS a lot for the hard work.

Definitely, I should thank you for all your patience in suggesting the changes in making the code more readable and maintainable.

And as @pictos mentioned, am looking into the possibility of making it as an incremental SG. Requires some effort, so can another review be done post that?

@mandel-macaque
Copy link
Member

Not my area of expertise so we should have another review (I'll ping the right person). Love the code changes, code is much nicer to read THANKS a lot for the hard work.

Definitely, I should thank you for all your patience in suggesting the changes in making the code more readable and maintainable.

And as @pictos mentioned, am looking into the possibility of making it as an incremental SG. Requires some effort, so can another review be done post that?

I am happy to do as many reviews as needed. Is effort well spent.

@rmarinho
Copy link
Member

Can we add more information on the PR description about how it works, and example of generated code for example?

Thanks

@egvijayanand
Copy link
Contributor Author

Sure, will include those details.

1 similar comment
@egvijayanand

This comment was marked as duplicate.

namespace Microsoft.Maui.Controls.SourceGen
{
[Generator(LanguageNames.CSharp)]
public class RouteSourceGenerator : ISourceGenerator
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to implement IIncrementalGenerator, otherwise it will have an impact on incremental build times.

Some details here on this: dc9ac94

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to implement IIncrementalGenerator, otherwise it will have an impact on incremental build times.

Yes, @pictos informed the same. Working on it. Will update once done and thanks for the reference link.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have migrated to Incremental source generator.

For time being have commented out the Generator attribute on the classic option to facilitate the review. Once the review is done, we can remove the following files related to the classic option.

  • RouteSourceGenerator.cs
  • RouteSyntaxReceiver.cs

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, there is a lot going on here. Can we start with a new issue that is a "proposal"?

We should iron out what the design is for this before writing code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, there is a lot going on here. Can we start with a new issue that is a "proposal"?

@PureWeen has already opened a Discussion thread for this in the Maui CommunityToolkit repo.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will update this PR description over there too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return null;
}

private static void Execute(SourceProductionContext context, Compilation compilation, ImmutableArray<ClassDeclarationSyntax?> classes)
Copy link
Contributor

@pictos pictos Mar 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your IncrementalGenerator is broken. You can't pass the Roslyn types (ClassDeclarationSyntax) to the Execute method, they should be just in the queries pipelines.
The reason is that Roslyn types can't be compared, so there's no way to cache, and the code will be generated at every file change (no matter what file changes).

You can see the Source Generators implemented on the CommunityToolkit.MVVM or CommunityToolkit.Maui as reference

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your IncrementalGenerator is broken. You can't pass the Roslyn types (ClassDeclarationSyntax) to the Execute method, they should be just in the queries pipelines.

Will check it out with the reference repo provided to restructure it.

@PureWeen
Copy link
Member

PureWeen commented Mar 9, 2023

@egvijayanand I'm going to close this for now in favor of the discussion over on MCT. CommunityToolkit/Maui#1004. Once we get through the discussion process over there then let's take next steps. I'm hoping we can get this merged into MCT so users can take advantage right away.

@PureWeen PureWeen closed this Mar 9, 2023
@github-actions github-actions bot locked and limited conversation to collaborators Dec 13, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-controls-shell Shell Navigation, Routes, Tabs, Flyout community ✨ Community Contribution
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Shell Routing through attributes on page (using source generators)
9 participants