4

Summary: I am hoping to use MAUI's builder.Services to resolve services in ViewModels, but I don't understand how to do so.

I could create my own IServiceProvider, but I am hoping to avoid the needed boilerplate code, so instead I seek a "standard MAUI" solution.

I added the following line to MauiProgram.CreateMauiApp():

builder.Services.AddSingleton<IAlertService, AlertService>();

And the corresponding declarations (in other files):

public interface IAlertService
{
    // ----- async calls (use with "await") -----
    Task ShowAlertAsync(string title, string message, string cancel = "OK");
    Task<bool> ShowConfirmationAsync(string title, string message, string accept = "Yes", string cancel = "No");
}

internal class AlertService : IAlertService
{
    // ----- async calls (use with "await") -----

    public Task ShowAlertAsync(string title, string message, string cancel = "OK")
    {
        return Application.Current.MainPage.DisplayAlert(title, message, cancel);
    }

    public Task<bool> ShowConfirmationAsync(string title, string message, string accept = "Yes", string cancel = "No")
    {
        return Application.Current.MainPage.DisplayAlert(title, message, accept, cancel);
    }
}

Then in my BaseViewModel class:

internal class BaseViewModel
{
    protected static IAlertService AlertSvc = ??? GetService (aka Resolve) ???
    
    public static void Test1()
    {
        Task.Run(async () =>
            await AlertSvc.ShowAlertAsync("Some Title", "Some Message"));
    }
}

Question: How fill AlertSvc with the service registered in MauiProgram?

CREDIT: Based on a suggested "DialogService" in some SO discussion or MAUI issue. Sorry, I've lost track of the original.

Julian
  • 5,290
  • 1
  • 17
  • 40
ToolmakerSteve
  • 18,547
  • 14
  • 94
  • 196
  • Do you *need* to have a static reference to `IAlertService`? Normally I'd expect that to be provided in the constructor, so you can't have it statically... – Jon Skeet May 30 '22 at 19:00
  • No I don't. I was patterning this off a previous implementation in mvvmcross. BUT I'm concerned about adding a parameter to the constructor of this class. Is there any other way to inject? I just used `static` here as a test. – ToolmakerSteve May 30 '22 at 19:02
  • 1
    Not sure if I understand you correctly, but here goes: the IServiceProvider (default) is automatically registered in your DI container. So you could just inject IServiceProvider into the constructor and it should be injected. Then in your class you can resolve another service manually from that. Is that what you mean? Then I'll work it out into an answer – Gerald Versluis May 30 '22 at 19:05
  • Based on Gerald's answer, [Here](https://stackoverflow.com/questions/72429055/how-to-displayalert-in-a-net-maui-viewmodel/72439742#72439742) is the complete implementation I ended up using. – ToolmakerSteve Dec 02 '22 at 19:20

4 Answers4

13

All the dependencies will be provided through the IServiceProvider implementation that is part of .NET MAUI. Luckily, the IServiceProvider itself is added to the dependency injection container as well, so you can do the following.

Add what you need to your dependency injection container in the MauiProgram.cs:

builder.Services.AddSingleton<IStringService, StringService>();
builder.Services.AddTransient<FooPage>();

For completeness, my StringService looks like this:

public interface IStringService
{
    string GetString();
}

public class StringService : IStringService
{
    public string GetString()
    {
        return "string";
    }
}

Then in your FooPage, which can also be your view model or anything of course, add a constructor like this:

public FooPage(IServiceProvider provider)
{
    InitializeComponent();

    var str = provider.GetService<IStringService>().GetString();
}

Here you can see that you resolve the service manually through the IServiceProvider and you can call upon that. Note that you'll want to add some error handling here to see if the service actually can be resolved.

Gerald Versluis
  • 30,492
  • 6
  • 73
  • 100
  • 1
    Since that is not a parameterless constructor, I assume that means all my subclasses of BaseViewModel (`class MyViewModel : BaseViewModel`) will need constructor `public MyViewModel(IServiceProvider provider) : base(provider)`? – ToolmakerSteve May 30 '22 at 19:12
  • 1
    Unfortunately yes. I guess another route would be to add public property to your `Application` (or similar) and resolve services through that. – Gerald Versluis May 30 '22 at 19:43
  • 1
    Ahh. That's what I'm looking for. Didn't realize I could do `public App(IServiceProvider provider)` to get at Maui's provider there. I'll store it there, then reference from a property in BaseViewModel. – ToolmakerSteve May 30 '22 at 20:01
  • 1
    [Here](https://stackoverflow.com/a/72439742/199364) is the complete implementation I settled on. – ToolmakerSteve May 30 '22 at 20:38
  • This should be a part of the MAUI App class generated by Visual Studio. A property for "Services" should be added there. I did this - see the reply below if anyone needs it. – Hoven Apr 16 '23 at 16:20
4

It is possible to access service provider without using DI injection via parameter of constructor (as in other answers):

myvar = Application.Current.Handler.MauiContext.Services.GetService<SomeType>();

I’m not aware of any reason to do this instead of using DI, but I mention it for completeness. Its also informative to see this access to MauiContext.

Hakan Fıstık
  • 16,800
  • 14
  • 110
  • 131
ToolmakerSteve
  • 18,547
  • 14
  • 94
  • 196
3

If you cannot (or do not want to) use the automatic constructor injection of MAUI Shell, you can still leverage the IServiceProvider instance which is part of the MauiApp class.

Instead of storing a reference to the implementation of the IServiceProvider in the App class, which introduces other dependency issues when writing unit tests for ViewModels and such, you can just create a static ServiceHelper class:

public static class ServiceHelper
{
    public static IServiceProvider Services { get; private set; }

    public static void Initialize(IServiceProvider serviceProvider) => 
        Services = serviceProvider;

    public static T GetService<T>() => Services.GetService<T>();
}

Then, you initialize this ServiceHelper in the MauiProgram.cs file after calling the Build() method:

public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();

    // ...

    builder.Services.AddSingleton<IAlertService, AlertService>();

    var app = builder.Build();

    //we must initialize our service helper before using it
    ServiceHelper.Initialize(app.Services);

    return app;
}

Finally, you can resolve any dependencies in your classes, such as your ViewModels as follows:

private IAlertService _alertService;

public class MyViewModel()
{
    _alertService = ServiceHelper.GetService<IAlertService>();
}

This can then also be used in unit tests, where you can simply provide a fake or mock implementation to the ServiceHelper, e.g.:

using Moq;

namespace MyTests;

[TestFixture]
public class MyViewModelTests
{
    [Setup]
    public void Setup()
    {
        var alertServiceMock = new Mock<IAlertService>();
        var serviceProviderMock = new Mock<IServiceProvider>();
        serviceProviderMock
            .Setup(sp => sp.GetService<IAlertService>())
            .Returns(alertServiceMock.Object);

        ServiceHelper.Initialize(serviceProviderMock.Object);
    }

    // write tests here...
}

I just also published a blog article about this topic, which goes into more detail.

Julian
  • 5,290
  • 1
  • 17
  • 40
2

This should be a part of the MAUI App.xaml.cs file generated by Visual Studio (as on April 2023). A property for "Services" should be already added there, like we have in ASP.NET Core. I had to do this as suggested by Gerald and Steve

public partial class App : Application
{
    // cached for later use
    public IServiceProvider Services { get; private set; }

    public App(IServiceProvider provider)
    {
        InitializeComponent();

        Services = provider;

        MainPage = new AppShell();
    }
}
Julian
  • 5,290
  • 1
  • 17
  • 40
Hoven
  • 393
  • 4
  • 8
  • Exactly. I have a [link “Here”](https://stackoverflow.com/a/72439742/199364) in a comment on the question, showing similar code. Thank you for adding an answer here, so it is easier for people to find. – ToolmakerSteve Apr 17 '23 at 00:28