infinitezero
infinitezero

Reputation: 2077

How to resolve dependencies in a .net MAUI ContentView?

I already tried this but it didn't work for me. GetService returns null.

Disclaimer: This is a MRE (minimal reproducable example), so please don't suggest a way that doesn't use the service.

I have a ContentView from which I want to access my singletons that have been added to the dependency injector.

I want LuckyNumberText to contain a random number using the service. However, because it's a content view, the constructor has to be parameterless and thus cannot be injected.

App.xaml

<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MauiMRE"
             x:Class="MauiMRE.App">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Resources/Styles/Colors.xaml" />
                <ResourceDictionary Source="Resources/Styles/Styles.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

App.xaml.cs

namespace MauiMRE;

public partial class App : Application
{
    public static IServiceProvider Services => ServiceProvider.Current;

    public App()
    {
        InitializeComponent();

        MainPage = new AppShell();
    }
}

AppShell.xaml

<?xml version="1.0" encoding="UTF-8" ?>
<Shell
    x:Class="MauiMRE.AppShell"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:MauiMRE"
    Shell.FlyoutBehavior="Disabled">

    <ShellContent
        Title="Home"
        ContentTemplate="{DataTemplate local:MainPage}"
        Route="MainPage" />

</Shell>

AppShell.xaml.cs

namespace MauiMRE;

public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();
    }
}

Mainpage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiMRE.MainPage"             
             xmlns:local="clr-namespace:MauiMRE"
             >
    <local:MyContentView></local:MyContentView>
</ContentPage>

MainPage.xaml.cs

namespace MauiMRE;

public partial class MainPage : ContentPage
{   
    public MainPage()
    {
        InitializeComponent();
    }   
}


MauiProgram.cs

using Microsoft.Extensions.Logging;

namespace MauiMRE;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });

#if DEBUG
        builder.Logging.AddDebug();
#endif      
        builder.Services.AddSingleton<MyContentView>();
        builder.Services.AddSingleton<MyContentViewModel>();
        builder.Services.AddSingleton<MyService>();
        return builder.Build();
    }
}

MyContentView.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiMRE.MyContentView">
    <VerticalStackLayout>
        <Label 
            Text="{Binding LuckyNumberText}"
            VerticalOptions="Center" 
            HorizontalOptions="Center" />
    </VerticalStackLayout>
</ContentView>

MyContentView.xaml.cs

namespace MauiMRE;

public partial class MyContentView : ContentView
{
    public MyContentView()
    {
        InitializeComponent();
        BindingContext = new MyContentViewModel();
    }
}

MyContentViewModel.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MauiMRE;

public class MyContentViewModel
{    

    public MyContentViewModel()
    {
        var service = ServiceProvider.GetService<MyService>(); 
        var result = service.RandomInt(); // crashes before this line is reached
    }

    public string LuckyNumberText
    {
        get
        {            
            return $"Lucky number: {1}";

        }
    }
}

MyService.cs


namespace MauiMRE
{
    public static class ServiceProvider
    {
        public static T GetService<T>()
        {
            var current = Current; // runs
            return current.GetService<T>(); // crashes
        }

        public static IServiceProvider Current
            =>
#if WINDOWS10_0_17763_0_OR_GREATER
                MauiWinUIApplication.Current.Services;
#elif ANDROID
                MauiApplication.Current.Services; // pretty sure this line crashes
#else
            null;
#endif
    }
}

Upvotes: 2

Views: 2140

Answers (2)

ToolmakerSteve
ToolmakerSteve

Reputation: 21243

ORIGINAL ANSWER

  • Inject IServiceProvider into App constructor:
public partial class App : Application
{
    public static IServiceProvider Services;   // Added

    public App(IServiceProvider services)
    {
        Services = services;   // Added

        InitializeComponent();

        MainPage = new AppShell();
    }
}

Credit: Gerald Versluis' answer to a different question shows injection of IServiceProvider into a constructor.

[With this approach] Do not inject any other parameters into App constructor, unless injected classes don't use GetService. Those would attempt to be resolved BEFORE App.Services is ready. This means any use of App.Services.GetService would break. But we need GetService so that custom component MyView below can have a parameterless constructor.

[See "UPDATE USING Marc Fabregat's approach" below, which removes this limitation.]


USAGE

It is now possible to use:
App.Services.GetService<SomeType>(); everywhere.
Including in the App constructor itself:

    public App(IServiceProvider services)
    {
        Services = services;   // Added

        InitializeComponent();

        // Assuming "MyPage", and any of its dependencies are registered in "CreateMauiApp()".
        MainPage = App.Services.GetService<MyPage>();
    }

UPDATE USING Marc Fabregat's approach

Marc Fabregat's answer avoids this limitation, by directly using a path to the service provider.

To adapt that answer to the code I show here:

public partial class App : Application
{
    // This definition works even for views injected into App constructor.
    public static IServiceProvider Services => AppServiceProvider.Current;

    public App(MyPage mp)
    {
        InitializeComponent();

        MainPage = mp;
    }
}

MyService used in MyView (with either App.Services definition above)

{
    public MyView()
    {
        InitializeComponent();

        MyService myService = App.Services.GetService<MyService>();
        var result = myService.RandomInt();
    }
}

I've verified this works.


For DI to work, the class being injected must have a public constructor

public class MyService
{
    public MyService()   // Must be PUBLIC, or DI will give an (unhelpful) exception.
    {
    }

    ...
}

OP had private MyService(){ ... }.

Upvotes: 2

Marc Fabregat
Marc Fabregat

Reputation: 81

You can create a helper like this:

    public static class AppServiceProvider
    {
        public static TService GetService<TService>()
            => Current.GetService<TService>();

        public static IServiceProvider Current
            =>
#if WINDOWS10_0_17763_0_OR_GREATER
            MauiWinUIApplication.Current.Services;
#elif ANDROID
            MauiApplication.Current.Services;
#elif IOS || MACCATALYST
                MauiUIApplicationDelegate.Current.Services;
#else
            null;
#endif
    }

then call it wherever you need

public partial class MyContentView : ContentView
{
    public MyContentView()
    {
        InitializeComponent();
        BindingContext = AppServiceProvider.GetService<MyContentViewModel>();;
    }
}

hope it helps

Upvotes: 1

Related Questions