1

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
    }
}

infinitezero
  • 1,610
  • 3
  • 14
  • 29
  • Where is your call to GetService? – Jason Jun 24 '23 at 22:17
  • @Jason there is no call to get service because it didn't work. As such I didn't reproduce it here. – infinitezero Jun 24 '23 at 22:17
  • 1
    GetService is the way to do this. If you’re having a problem using it, you need to provide details – Jason Jun 24 '23 at 22:21
  • 1) *"I already tried this"*: show the exact line with your type, and in which methd of which class you called it. 2) Does your `App` constructor have any parameters? If so, add to question that declaration line. 3) If you put a breakpoint at start of your ContentView's constructor, and another in `CreateMauiApp` on `return builder.Build();`, does the Build get called before your ContentVIew is constructed? – ToolmakerSteve Jun 24 '23 at 22:39

2 Answers2

2

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(){ ... }.

ToolmakerSteve
  • 18,547
  • 14
  • 94
  • 196
  • This doesn't work for me. I hit the following breakpoints: `InitializeComponent` in `AppShell()`, `InitializeComponent()` in `MainPage()`, `MauiApplication.Current.Services` in `IServiceProvider Current`. Afterwards I crash with an unhandled exception in `JNINativeWrapper`. The stacktrace does not contain any of my functions, but passes through `ShellContent.cs` and `ShellSectionRenderer.cs` – infinitezero Jun 25 '23 at 01:04
  • Yep. That's what happens unless you do everything exactly correctly. Strip out everything you can, to get it as simple as possible, and update the code in question to match what you did. Including what didn't work. Make sure the cause is what you think it is: remove code until you have something that runs (even though it doesn't do what you want) - need to be sure exactly what line breaks it. Even though I've encountered these issues before, it took me a number of edits before I had it working correctly. **May have better success using Marc's answer directly.** – ToolmakerSteve Jun 25 '23 at 01:10
  • I updated everything in the OP. It seems like it's Marc's line that breaks it. – infinitezero Jun 25 '23 at 01:26
  • Ah. I was testing on Windows. Based on your comment on Marc’s code, you are testing on Android? Did you put breakpoint on that line, step over it? It returns null? Or is exception in the caller, when it tries to use that? To see, split calling line in two: one to get Current, other to call GetService. Which line crashes? – ToolmakerSteve Jun 25 '23 at 01:31
  • I switched to Windows to check and it also fails in the same piece of code (obviously not in the android block). Right after passing that breakpoint the code crashes before hitting the next breakpoint. – infinitezero Jun 25 '23 at 01:33
  • GetService is the one. Current still runs. I'll update the OP. – infinitezero Jun 25 '23 at 01:38
  • @infinitezero reports that `MyService`'s constructor was `private`. This caused DI to fail. – ToolmakerSteve Jun 25 '23 at 01:55
1

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

  • Is there a doc or blog post you can link, showing this approach? – ToolmakerSteve Jun 25 '23 at 00:50
  • 1
    sure, I got it from a post from David Ortinau. Here is a repo from him using it. https://github.com/davidortinau/WeatherTwentyOne/blob/main/src/WeatherTwentyOne/Services/ServiceExtensions.cs – Marc Fabregat Jun 26 '23 at 18:25