question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[Proposal] IServiceCollection extension methods to register Views & ViewModels

See original GitHub issue

IServiceCollection extension methods to register Views & ViewModels

Link to Discussion

#430

Summary

This proposal is to add IServiceCollection extension methods to CommunityToolkit.Maui which registers Views and the corresponding ViewModels together. These extension methods would also allow for optionally registering Shell routes for pages not explicitly added to AppShell.xaml

Motivation

When implementing the MVVM pattern, it is common to resolve Views, ViewModels, and required dependencies from the IServiceCollection container. Since Views and ViewModels need to be registered in the IServiceCollection and they typically share a direct 1:1 relationship, it makes sense to provide helper methods to register both the View and ViewModel in a single line of code versus having to register them independently.

Furthermore, there may be requirements to register specific pages with Shell routing in code using Routing.RegisterRoute() instead of directly adding pages to the markup of AppShell.xaml. The extension methods to register Views and ViewModels in IServiceCollection could further simplify development by providing a means to register pages in Shell routing.

Detailed Design

This proposal introduces the following IServiceCollection extension methods. This option introduces more extension methods in a slightly more verbose yet fluent API with the ServiceLifetime part of the method name.

public static class IServiceCollectionExtensions
{
    /// <summary>
    /// Adds a Page or View of the type specified in <typeparamref name="TView"/> and a ViewModel
    /// of the type specified in <typeparamref name="TViewModel"/> to the specified
    /// <see cref="IServiceCollection"/> with <see cref="ServiceLifetime.Singleton"/> lifetime.
    /// </summary>
    /// <typeparam name="TView">The type of the View to add. Constrained to <see cref="Microsoft.Maui.Controls.VisualElement"/></typeparam>
    /// <typeparam name="TViewModel">The type of the ViewModel to add. Constrained to <see cref="INotifyPropertyChanged"/></typeparam>
    /// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
    /// <returns>A reference to this instance after the operation has completed.</returns>
    public static IServiceCollection AddSingletonViewAndViewModel<TView, TViewModel>(this IServiceCollection services)
        where TView : Microsoft.Maui.Controls.BindableObject
        where TViewModel : INotifyPropertyChanged
    {
        services.AddSingleton<TViewModel>();
        return services.AddSingleton<TView>();
    }

    /// <summary>
    /// Adds a Page of the type specified in <typeparamref name="TView"/> and a ViewModel
    /// of the type specified in <typeparamref name="TViewModel"/> to the specified
    /// <see cref="IServiceCollection"/> with <see cref="ServiceLifetime.Singleton"/> lifetime
    /// and registers a MAUI Shell route in <see cref="Routing"/> using the value of <paramref name="route"/> as the route.
    /// </summary>
    /// <typeparam name="TView">The type of the View to add. Constrained to <see cref="Microsoft.Maui.Controls.Page"/></typeparam>
    /// <typeparam name="TViewModel">The type of the ViewModel to add. Constrained to <see cref="INotifyPropertyChanged"/></typeparam>
    /// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
    /// <returns>A reference to this instance after the operation has completed.</returns>
    public static IServiceCollection AddSingletonViewAndViewModelWithShellRoute<TView, TViewModel>(this IServiceCollection services, string route, RouteFactory? factory = null)
        where TView : Microsoft.Maui.Controls.Page
        where TViewModel : INotifyPropertyChanged
    {
        if(factory is null)
        {
            Routing.RegisterRoute(route, typeof(TView));
        }
        else
        {
            Routing.RegisterRoute(route, factory);
        }

        return services.AddSingletonViewAndViewModel<TView, TViewModel>();
    }

    /// <summary>
    /// Adds a Page or View of the type specified in <typeparamref name="TView"/> and a ViewModel
    /// of the type specified in <typeparamref name="TViewModel"/> to the specified
    /// <see cref="IServiceCollection"/> with <see cref="ServiceLifetime.Scoped"/> lifetime.
    /// </summary>
    /// <typeparam name="TView">The type of the View to add. Constrained to <see cref="Microsoft.Maui.Controls.VisualElement"/></typeparam>
    /// <typeparam name="TViewModel">The type of the ViewModel to add. Constrained to <see cref="INotifyPropertyChanged"/></typeparam>
    /// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
    /// <returns>A reference to this instance after the operation has completed.</returns>
    public static IServiceCollection AddScopedViewAndViewModel<TView, TViewModel>(this IServiceCollection services)
        where TView : Microsoft.Maui.Controls.BindableObject
        where TViewModel : INotifyPropertyChanged
    {
        services.AddScoped<TViewModel>();
        return services.AddScoped<TView>();
    }

    /// <summary>
    /// Adds a Page of the type specified in <typeparamref name="TView"/> and a ViewModel
    /// of the type specified in <typeparamref name="TViewModel"/> to the specified
    /// <see cref="IServiceCollection"/> with <see cref="ServiceLifetime.Scoped"/> lifetime
    /// and registers a MAUI Shell route in <see cref="Routing"/> using the value of <paramref name="route"/> as the route.
    /// </summary>
    /// <typeparam name="TView">The type of the View to add. Constrained to <see cref="Microsoft.Maui.Controls.Page"/></typeparam>
    /// <typeparam name="TViewModel">The type of the ViewModel to add. Constrained to <see cref="INotifyPropertyChanged"/></typeparam>
    /// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
    /// <returns>A reference to this instance after the operation has completed.</returns>
    public static IServiceCollection AddScopedViewAndViewModelWithShellRoute<TView, TViewModel>(this IServiceCollection services, string route, RouteFactory? factory = null)
        where TView : Microsoft.Maui.Controls.Page
        where TViewModel : INotifyPropertyChanged
    {
        if(factory is null)
        {
            Routing.RegisterRoute(route, typeof(TView));
        }
        else
        {
            Routing.RegisterRoute(route, factory);
        }

        return services.AddScopedViewAndViewModel<TView, TViewModel>();
    }

    /// <summary>
    /// Adds a Page or View of the type specified in <typeparamref name="TView"/> and a ViewModel
    /// of the type specified in <typeparamref name="TViewModel"/> to the specified
    /// <see cref="IServiceCollection"/> with <see cref="ServiceLifetime.Transient"/> lifetime.
    /// </summary>
    /// <typeparam name="TView">The type of the View to add. Constrained to <see cref="Microsoft.Maui.Controls.VisualElement"/></typeparam>
    /// <typeparam name="TViewModel">The type of the ViewModel to add. Constrained to <see cref="INotifyPropertyChanged"/></typeparam>
    /// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
    /// <returns>A reference to this instance after the operation has completed.</returns>
    public static IServiceCollection AddTransientViewAndViewModel<TView, TViewModel>(this IServiceCollection services)
        where TView : Microsoft.Maui.Controls.BindableObject
        where TViewModel : INotifyPropertyChanged
    {
        services.AddTransient<TViewModel>();
        return services.AddTransient<TView>();
    }

    /// <summary>
    /// Adds a Page of the type specified in <typeparamref name="TView"/> and a ViewModel
    /// of the type specified in <typeparamref name="TViewModel"/> to the specified
    /// <see cref="IServiceCollection"/> with <see cref="ServiceLifetime.Transient"/> lifetime
    /// and registers a MAUI Shell route in <see cref="Routing"/> using the value of <paramref name="route"/> as the route.
    /// </summary>
    /// <typeparam name="TView">The type of the View to add. Constrained to <see cref="Microsoft.Maui.Controls.Page"/></typeparam>
    /// <typeparam name="TViewModel">The type of the ViewModel to add. Constrained to <see cref="INotifyPropertyChanged"/></typeparam>
    /// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
    /// <returns>A reference to this instance after the operation has completed.</returns>
    public static IServiceCollection AddTransientViewAndViewModelWithShellRoute<TView, TViewModel>(this IServiceCollection services, string route, RouteFactory? factory = null)
        where TView : Microsoft.Maui.Controls.Page
        where TViewModel : INotifyPropertyChanged
    {
        if(factory is null)
        {
            Routing.RegisterRoute(route, typeof(TView));
        }
        else
        {
            Routing.RegisterRoute(route, factory);
        }

        return services.AddTransientViewAndViewModel<TView, TViewModel>();
    }
}

Usage Syntax

This example illustrates how the extension methods could be used within MauiProgram

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .UseMauiCommunityToolkit()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            })
            .Services
                // Adds HomePage and HomePageViewModel with ServiceLifetime.Transient
                .AddTransientViewAndViewModel<HomePage, HomePageViewModel>()
                // Adds HomePage and HomePageViewModel with ServiceLifetime.Singleton
                .AddSingletonViewAndViewModel<HomePage, HomePageViewModel>()
                // Adds HomePage and HomePageViewModel with ServiceLifetime.Transient and Shell route: "HomePage"
                .AddTransientViewAndViewModelWithShellRoute<HomePage, HomePageViewModel>()
                // Adds HomePage and HomePageViewModel with ServiceLifetime.Transient and Shell route: "MyCustomRoute"
                .AddTransientViewAndViewModelWithShellRoute<HomePage, HomePageViewModel>("MyCustomRoute")
                // Adds HomePage and HomePageViewModel with ServiceLifetime.Singleton and Shell route: "HomePage"
                .AddSingletonViewAndViewModelWithShellRoute<HomePage, HomePageViewModel>()
                // Adds HomePage and HomePageViewModel with ServiceLifetime.Singleton and Shell route: "MyCustomRoute"
                .AddSingletonViewAndViewModelWithShellRoute<HomePage, HomePageViewModel>("MyCustomRoute");

        return builder.Build();
    }
}

Drawbacks

These extension methods are MVVM specific and this repo/package is MAUI specific. Due to the necessary Microsoft.Maui.Controls.VisualElement and Microsoft.Maui.Controls.Page type constraints needed on the methods they cannot be moved to the CommunityToolkit.Mvvm which is a more natural fit. Adding the methods to this repo/package may be confusing to some developers.

These methods also do not address the following scenarios:

  • What if the Views and ViewModels need different lifetimes? E.g. Scoped for TView and Transient for TViewModel?
  • What if multiple Views/Pages use the same ViewModel?
  • Why doesn’t the helper method confirm TViews BindingContext is TViewModel?

Alternatives

These extension methods are merely convenience methods for developers. The alternative is to continue registering Views and ViewModels in the IServiceCollection independently and then separately registering routes via Routing.RegisterRoute(); if needed.

Unresolved Questions

  1. Does ServiceLifetime.Scoped make sense in the context of a MAUI application for Pages, Views, and ViewModels? Can someone provide a concrete example of using the Scoped lifetime?
  2. If Option 1 were to be chosen, should the lifetime parameter have a default value set? If yes, is ServiceLifetime.Transient the correct value or should it be ServiceLifetime.Singleton?
  3. Are method names too verbose? If yes, how would you reduce the verbosity?
  4. Is there a better API surface we should consider?

Removed Suggestions

Option 1

This option introduces fewer extension methods by utilizing a parameter to specify the IServiceCollection container lifetime of the Page or View and ViewModel. In this option, ServiceLifetime has a default value of ServiceLifetime.Transient. This may or may not be the correct default value, or perhaps a default value should not be provided at all.

public static class IServiceCollectionExtensions
{
    /// <summary>
    /// Adds a Page or View of the type specified in <typeparamref name="TView"/> and a ViewModel
    /// of the type specified in <typeparamref name="TViewModel"/> to the specified
    /// <see cref="IServiceCollection"/>.
    /// </summary>
    /// <typeparam name="TView">The type of the Page or View to add. Constrained to <see cref="Microsoft.Maui.Controls.VisualElement"/></typeparam>
    /// <typeparam name="TViewModel">The type of the ViewModel to add. Constrained to <see cref="INotifyPropertyChanged"/></typeparam>
    /// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
    /// <param name="lifetime">The <see cref="ServiceLifetime"/> by which the View and ViewModel will be registered in <see cref="IServiceCollection"/>. Defaults to <see cref="ServiceLifetime.Transient"/></param>
    /// <returns>A reference to this instance after the operation has completed.</returns>
    public static IServiceCollection AddViewAndViewModel<TView, TViewModel>(this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Transient)
        where TView : Microsoft.Maui.Controls.VisualElement
        where TViewModel : INotifyPropertyChanged
    {
        switch (lifetime)
        {
            case ServiceLifetime.Singleton:
                services.AddSingleton<TViewModel>();
                return services.AddSingleton<TView>();
            case ServiceLifetime.Scoped:
                services.AddScoped<TViewModel>();
                return services.AddScoped<TView>();
            case ServiceLifetime.Transient:
                services.AddTransient<TViewModel>();
                return services.AddTransient<TView>();
            default:
                throw new ArgumentOutOfRangeException(nameof(lifetime));
        }
    }

    /// <summary>
    /// Adds a Page of the type specified in <typeparamref name="TView"/> and a ViewModel
    /// of the type specified in <typeparamref name="TViewModel"/> to the specified
    /// <see cref="IServiceCollection"/> and registers a MAUI Shell route in
    /// <see cref="Routing"/> using <typeparamref name="TView"/> type name as the route.
    /// </summary>
    /// <typeparam name="TView">The type of the Page to add. Constrained to <see cref="Microsoft.Maui.Controls.Page"/></typeparam>
    /// <typeparam name="TViewModel">The type of the ViewModel to add. Constrained to <see cref="INotifyPropertyChanged"/></typeparam>
    /// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
    /// <param name="lifetime">The <see cref="ServiceLifetime"/> by which the View and ViewModel will be registered in <see cref="IServiceCollection"/>. Defaults to <see cref="ServiceLifetime.Transient"/></param>
    /// <returns>A reference to this instance after the operation has completed.</returns>
    public static IServiceCollection AddViewAndViewModelWithShellRoute<TView, TViewModel>(this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Transient)
        where TView : Microsoft.Maui.Controls.Page
        where TViewModel : INotifyPropertyChanged
    {
        return services.AddViewAndViewModelWithShellRoute<TView, TViewModel>(typeof(TView).Name, lifetime);
    }

    /// <summary>
    /// Adds a Page of the type specified in <typeparamref name="TView"/> and a ViewModel
    /// of the type specified in <typeparamref name="TViewModel"/> to the specified
    /// <see cref="IServiceCollection"/> and registers a MAUI Shell route in
    /// <see cref="Routing"/> using the value of <paramref name="route"/> as the route.
    /// </summary>
    /// <typeparam name="TView">The type of the Page to add. Constrained to <see cref="Microsoft.Maui.Controls.Page"/></typeparam>
    /// <typeparam name="TViewModel">The type of the ViewModel to add. Constrained to <see cref="INotifyPropertyChanged"/></typeparam>
    /// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
    /// <param name="route">Route at which this page will be registered with Shell routing.</param>
    /// <param name="lifetime">The <see cref="ServiceLifetime"/> by which the View and ViewModel will be registered in <see cref="IServiceCollection"/>. Defaults to <see cref="ServiceLifetime.Transient"/></param>
    /// <returns>A reference to this instance after the operation has completed.</returns>
    public static IServiceCollection AddViewAndViewModelWithShellRoute<TView, TViewModel>(this IServiceCollection services, string route, ServiceLifetime lifetime = ServiceLifetime.Transient)
        where TView : Microsoft.Maui.Controls.Page
        where TViewModel : INotifyPropertyChanged
    {
        Routing.RegisterRoute(route, typeof(TView));
        return services.AddViewAndViewModel<TView, TViewModel>(lifetime);
    }
}

This example illustrates how the extension methods in Option 1 could be used within MauiProgram

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .UseMauiCommunityToolkit()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            })
            .Services
                // Adds HomePage and HomePageViewModel with ServiceLifetime.Transient (defualt value)
                .AddViewAndViewModel<HomePage, HomePageViewModel>()
                // Adds HomePage and HomePageViewModel with ServiceLifetime.Transient
                .AddViewAndViewModel<HomePage, HomePageViewModel>(ServiceLifetime.Singleton)
                // Adds HomePage and HomePageViewModel with ServiceLifetime.Transient (defualt value) and Shell route: "HomePage"
                .AddViewAndViewModelWithShellRoute<HomePage, HomePageViewModel>()
                // Adds HomePage and HomePageViewModel with ServiceLifetime.Transient (defualt value) and Shell route: "MyCustomRoute"
                .AddViewAndViewModelWithShellRoute<HomePage, HomePageViewModel>("MyCustomRoute")
                // Adds HomePage and HomePageViewModel with ServiceLifetime.Singleton and Shell route: "MyCustomRoute"
                .AddViewAndViewModelWithShellRoute<HomePage, HomePageViewModel>("MyCustomRoute", ServiceLifetime.Singleton);

        return builder.Build();
    }
}

Issue Analytics

  • State:closed
  • Created a year ago
  • Comments:17 (9 by maintainers)

github_iconTop GitHub Comments

2reactions
rjygrahamcommented, Jul 13, 2022

Hey @brminnick, I just wanted to follow up on this and provide some thoughts below on the questions raised during the July 2022 community standup.

@jfversluis

  • ServiceLifetime is defined in Microsoft.Extensions.DependencyInjection. Thankfully it seems like everyone (including community) preferred Option 2 so this doesn’t even matter.
  • Helper method not setting ViewModel to View BindingContext.
    • Agree this is a pitfall. IMO, auto-wireup of View/ViewModel is something that should be baked into MAUI proper. Perhaps we can start with XML docs for v1, implement an analyzer for v2, and eventually get the auto-wireup implemented in MAUI?

@pictos

  • Add overload method for Routing.RegisterRoute(string route, RouteFactory factory). Agreed this overload method should be available as well.
  • BindableObject as constraint for registering Views and ViewModels. As mentioned by @brminnick the non-Shell methods would use BindableObject as the TView constraint to allow for the widest usage possible.

@VladislavAntonyuk

  • Different lifetimes for Views and ViewModels. I can’t think of a use-case where this would apply, but if it did come up, the developer would just have to fall back to registering the View and ViewModel separately as they do today.

Semi-related to the scenario @VladislavAntonyuk mentioned is what happens if there are multiple Views/Pages that use the same ViewModel? I can envision this in scenarios where a View is purpose-built for Desktop/TV (Mac/Windows/Tizen) and another View is created for Mobile.

The original intent of these helper extension methods iso make developers’ lives easier within the 80/20 rule. There’s no way we’ll be able to cover 100% of the real-life scenarios, but I think we can capture at least 80% of the use-cases and that seems like a “win” to me.

Look forward to everyone’s thoughts and guidance for moving forward (or not)!

1reaction
brminnickcommented, Jul 13, 2022

We’ve reached the minimum threshold of 50% approval and you are now clear to move forward and implement this Proposal, @rjygraham! 🎉

I’ve moved this to the Proposal Approved Column on our Project Board, added the approved tag and assigned it to @rjygraham.

Read more comments on GitHub >

github_iconTop Results From Across the Web

ServiceCollectionExtensions - .NET MAUI Community Toolkit
The ServiceCollectionExtensions provide a series of extension methods that simplify registering Views and ViewModels within a .
Read more >
Dependency Injection in Xamarin.Forms with .Net Extensions
In this post we will explore how to use the .NET Extensions dependency injection services in a Xamarin.Forms app.
Read more >
c# - How can I send a parameter to a ViewModel that I get ...
I think you wanted to pass some parameter to viewmodel from view. Below code will do that job. Create a public property or...
Read more >
Dependency injection overview
DotVVM supports dependency injection in viewmodels, static command services, and custom controls. Register services. In ASP.NET Core, DotVVM uses the Microsoft.
Read more >
Prism for .NET MAUI - Public Beta - Dan Siegel
Maui is the first platform from Prism to support Registering a single View with different names each mapped to a different ViewModel. This...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found