1

My Goal is to NOT create dependencies between ViewModels.
I have .NET 7 MAUI project.
The situation is simple.

I have the MainPage and I linked ViewModel MainPageViewModel to it using DI

services.AddSingleton<MainPageViewModel>();

services.AddSingleton<MainPage>(provider =>
{
    return new MainPage()
    {
        BindingContext = provider.GetRequiredService<MainPageViewModel>(),
    };
});

It is working fine, the ViewModel is created and it is assigned to as BindingContext of the MainPage, Great.

Now, When I put another Child View inside the MainPage like this

<?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"
             xmlns:local="clr-namespace:MyNamespace"
             x:Class="MyNamespace.MainPage">
    
            <MyNamespace:ChildView />

</ContentPage>

and I register it with its ViewModel like this

services.AddSingleton<ChildViewModel>();

services.AddSingleton<ChildView>(provider =>
{
    return new ChildView()
    {
        BindingContext = provider.GetRequiredService<ChildViewModel>(),
    };
});

The MainPage create the ChildView directly (by calling its parameterless constructor) without consulting the DI Container
This is causing the lack of Binding for the ViewModel.

  1. I do not want to Create ChildViewModel inside MainPageViewModel and pass it.
  2. In Prism this problem could be solved using Regions, but for now Prism does not support MAUI
  3. The creating of the MainPage is like this
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
    x:Class="Trex.Mobile.App.AppShell"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:MyNamespace"
    Shell.FlyoutBehavior="Disabled">

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

</Shell>

and by using this way, the DI container is used.
Can I use something like that for my ChildView or should I change something fundamentally

Hakan Fıstık
  • 16,800
  • 14
  • 110
  • 131
  • Does this answer your question? [Dependency Injection when nesting Views in MAUI](https://stackoverflow.com/questions/76490039/dependency-injection-when-nesting-views-in-maui). [OPINION] Custom components (`ChildView`) often want access to their parent view's BindingContext. This means NOT setting BindingContext = ChildViewModel. instead, place those properties directly in ChildView. See Fix #3 in https://stackoverflow.com/a/76493901/199364. – ToolmakerSteve Jul 07 '23 at 18:46
  • Yes, basically, it is the same idea as the answer below is mentioning – Hakan Fıstık Jul 07 '23 at 18:51
  • Good; thanks for confirming that this question can be closed as a duplicate. (I like the answer below, so I will link to it on the other question. Reach all good answer from there.) – ToolmakerSteve Jul 07 '23 at 18:58

1 Answers1

2

I don't think it's possible to create the ChildView using the DI container, because Shell doesn't instantiate it.

Since the MainPage instance is created by Shell, you can use constructor injection:

public partial class MainPage : ContentPage
{
    public MainPage(MainPageViewModel viewModel)
    {
        InitializeComponent();
        BindingContext = viewModel;
    }
}

Then, as I have described in this answer, you can create a ServiceHelper class like so:

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>();
}

The ChildView is instantiated by the MainPage and cannot use Shell's constructor injection, but you can use the ServiceHelper from above to resolve the ChildViewModel:

public partial class ChildView : ContentView
{
    public ChildView()
    {
        InitializeComponent();
        BindingContext = ServiceHelper.GetService<ChildViewModel>();
    }
}

Finally, register all dependencies and setup the ServiceHelper:

builder.Services.AddSingleton<MainPage>();
builder.Services.AddSingleton<MainPageViewModel>();
builder.Services.AddSingleton<ChildView>();
builder.Services.AddSingleton<ChildViewModel>();

var app = builder.Build();

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

return app;
Julian
  • 5,290
  • 1
  • 17
  • 40
  • When I started the project, I created and used this `ServiceHelper` idea exactly as you described above, then I get rid of it because, as I remember, it was anti-pattern, and It never occurred to me again. I am really afraid that there would be no better solution. I think your solution will work, but I would like to see a better one. Thank you anyway, really appreciated. – Hakan Fıstık Jul 07 '23 at 08:59
  • What exactly is the anti-pattern here? It's just a static wrapper for the DI container to make it accessible everywhere. IMHO static wrappers and containers are totally acceptable, it's better than passing down the `IServiceProvider` manually. – Julian Jul 07 '23 at 09:04
  • 1
    here is an article that says it is [anti-pattern](https://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern), here is [a question](https://stackoverflow.com/questions/22795459) which say it is not, so I am really confused – Hakan Fıstık Jul 07 '23 at 09:38
  • Any way for practical reason, I do not see any other solution, maybe in the future if some better way exists, I would like to refactor it, because I really hate to use statics. Anyway thank you again and I really appreciate that – Hakan Fıstık Jul 07 '23 at 09:40
  • 1
    Well, we need to consider the practicality of the approach. IoC containers usually rely on or rather enforce the Service Locator pattern and sometimes constructor injection is preferred. Now, with XAML we face the issue that only parameterless constructors can be used to instantiate ContentViews in XF and MAUI, so we don't have constructor injection available at all. Therefore, I do not regard the Service Locator pattern as an anti-pattern per sé when it is often a necessity. – Julian Jul 07 '23 at 09:49
  • 1
    [OPINION] The anti-pattern described in that article is already inherent in DI; adding a ServiceHelper does not make the problem worse. Specifically, DI breaks at run-time if referenced classes (services) are not registered with the DI container. Before DI, the written code, if it compiled, would work. Breaks that don't happen until run-time are bad for both usage and code maintenance. That's the article's point. DI is a trade-off: loose coupling at the cost of moving a failure from compile-time to run-time. Hopefully static analyzers will get smart enough to find at compile time. – ToolmakerSteve Jul 07 '23 at 18:16
  • Anyway, given Maui's dependence on paramterless constructor of Views, ChildView's constructor *must* somehow get at service provider. I use https://stackoverflow.com/a/72439742/199364 or https://stackoverflow.com/a/76028981/199364: `App.Services`, relying on IServiceProvider injection into App constructor. Julian's answer is fine also. – ToolmakerSteve Jul 07 '23 at 18:34
  • 1
    I am slowly reaching the conclusion that this is the best solution available, I started implementing it and it worked, and I think I will go with this way, Thank you all – Hakan Fıstık Jul 07 '23 at 18:51