Dependency Injection into ContentView (Code-Behind) with Parameters

2.2k Views Asked by At

in my .NET MAUI MauiProgram.cs, I registered a ResourceManager and also the Code-Behind of my ContentView named MyContentView (which refers to MyContentView.xaml.cs):

Parts from MauiProgram.cs

...
var builder = MauiApp.CreateBuilder();
builder
    .UseMauiApp<App>()
    .UseMauiCommunityToolkit()
    .UseLocalizationResourceManager(settings =>
    {
        settings.RestoreLatestCulture(true);
        settings.AddResource(AppResources.ResourceManager);
    })
    .ConfigureFonts(fonts =>
    {
        fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
        fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
    });

builder.Services.AddTransient<MyItemInputControlView>();

builder.Services.AddTransient<MyContentPage>();
builder.Services.AddTransient<MyContentPageViewModel>(); 

MyItemInputControlView.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"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             xmlns:views="clr-namespace:MyApp.Pages.Views"
             xmlns:loc="clr-namespace:LocalizationResourceManager.Maui;assembly=LocalizationResourceManager.Maui"
             xmlns:viewModels="clr-namespace:MyApp.ViewModels"
             x:Class="MyApp.Pages.Views.MyItemInputControlView"
             x:Name="this">

        
    <StackLayout BindingContext="{x:Reference this}">
        <Grid Margin="20, 0, 20, 0">
            <Grid.Resources>
                <!--<Style TargetType="Entry">
                    <Setter Property="Padding" Value="2 1" />
                    <Setter Property="BorderBrush" Value="LightGray" />
                </Style>-->
            </Grid.Resources>
            
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>

            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>


            <StackLayout Grid.Row="0" Grid.Column="0" VerticalOptions="Center">
                <Label Text="{Binding NameLabelString}" />
                ...
            </StackLayout>
        </Grid>
    </StackLayout>
</ContentView>

MyItemInputControlView.xaml.cs (Code-Behind):

namespace MyApp.Pages.Views;

using CommunityToolkit.Maui.Behaviors;
using CommunityToolkit.Mvvm.Messaging;
using LocalizationResourceManager.Maui;
using MyApp.ViewModels;
using MyApp.ViewModels.Messages;

public partial class MyItemInputControlView : ContentView
{
    public static readonly BindableProperty NameLabelProperty = BindableProperty.Create(nameof(NameLabelString), typeof(string), typeof(MyItemInputControlView), string.Empty, BindingMode.TwoWay);
    public static readonly BindableProperty IsOptionalProperty = BindableProperty.Create(nameof(IsOptionalLabelString), typeof(string), typeof(MyItemInputControlView), string.Empty, BindingMode.TwoWay);
    ...
    
    private ILocalizationResourceManager localizationResourceManager;


    public string NameLabelString
    {
        get => (string)GetValue(NameLabelProperty);
        set => SetValue(NameLabelProperty, $"{localizationResourceManager[value]}:");
    }

    ...
    
    // !!!! THE CONSTRUCTOR WITH ONE PARAMETER IS NEVER REACHED (only default-constructor)!!!
    // Wanted to assign the ILocalizationResourceManager to the Code-Behind
    public MyItemInputControlView(ILocalizationResourceManager res)
    {
    
        //WeakReferenceMessenger.Default.Register<LocalizationResourceManagerProviderMessage>(this, HandleLocalizationResourceManagerProviderMessage);

        InitializeComponent();

        ...
    }
    
    private void HandleLocalizationResourceManagerProviderMessage(object recipient, LocalizationResourceManagerProviderMessage message)
    {
        // Assign the ILocalizationResourceManager instance from the message
        localizationResourceManager = message.Value;
    }
}

ContentPage-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"
             xmlns:viewModels="clr-namespace:MyApp.ViewModels"
             xmlns:controls="clr-namespace:MyApp.Pages.Views"
             xmlns:loc="clr-namespace:LocalizationResourceManager.Maui;assembly=LocalizationResourceManager.Maui"
             x:DataType="viewModels:ManageItemViewModel"
             x:Class="MyApp.Pages.ManageItem"
             Title="{loc:Translate ManageItem_Heading}">


    <VerticalStackLayout>

        <Label 
            Text="{loc:Translate ManageItem_HeadingLabel}"
            FontAttributes="Bold"
            VerticalOptions="Center" 
            HorizontalOptions="Center"
            Padding="20" />

        <StackLayout>
            <!-- "ManageItem_MandatoryLabelString" is the String from my Resource-File and will be translated in the Property-Getter -->
            <controls:MyItemInputControlView NameLabelString="ManageItem_MyProductNameLabel"
                                             IsOptionalLabelString="ManageItem_MandatoryLabelString"
                                             PlaceholderString="ManageItem_MyProductNamePlaceholder"
                                             EntryInput="{Binding MyItem.Name}"
                                             InputValid="{Binding MyItemNameInputValid, Mode=TwoWay}" 
                                             x:Name="firstContentView"/>
       </StackLayout>
</ContentPage>

In one of my ContentPages, I then reach a Constructor with two parameters:

//!!! IN MY REGISTERED CONTENT PAGE THE CONSTRUCTOR INCLUDING ILocalizationResourceManager PARAMETER IS REACHED !!!
public partial class MyContentPage : ContentPage
{
    public MyContentPage(MyContentPageViewModel viewModel, ILocalizationResourceManager localizationResourceManager)
    {
        InitializeComponent();

        BindingContext = viewModel;
    }   
}

Unfortunately, in my Code-Behind of my ContentView it is always just the default constructor. Why is this and what am I doing wrong here that the ContentPage correctly has 2 Constructor Parameters and my ContentView hasn't?

2

There are 2 best solutions below

2
Liqun Shen-MSFT On BEST ANSWER

I made a small demo trying to use binding for BindableProperty, which works fine.

For MyItemInputControlView.xaml, almost the same:

<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
         ...
  
   x:Class="MauiLocalizationResourceManagerSample.MyItemInputControlView">

    <StackLayout BindingContext="{x:Reference this}" HeightRequest="100">
        <StackLayout Grid.Row="0" Grid.Column="0" VerticalOptions="Center">
            <Label x:Name="mylabel" Text="{Binding NameLabelString}" />
        </StackLayout>
    </StackLayout>

</ContentView>

For MyItemInputControlView.cs, create a BindableProperty. Pay attention to the naming convention of it. For more info. you could refer to Create a bindable property

Also you can see I define a OnStringChanged method which will be invoked if NameLabelString changed. And when changed, we will set new value for label text. For more info, you could refer to Detect property changes

public partial class MyItemInputControlView : ContentView
{
    public static readonly BindableProperty NameLabelStringProperty = BindableProperty.Create(nameof(NameLabelString), typeof(string),
        typeof(MyItemInputControlView), string.Empty, BindingMode.TwoWay,propertyChanged:OnStringChanged);

    private static void OnStringChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var a = bindable as MyItemInputControlView;
        a.mylabel.Text = newValue.ToString();
    }

    public string NameLabelString
    {
        get => (string)GetValue(MyItemInputControlView.NameLabelStringProperty);
        set => SetValue(MyItemInputControlView.NameLabelStringProperty, value);
    }

    public MyItemInputControlView()
    {
        InitializeComponent();
    }
}

Then the ContentPage which consume the ContentView:

    <VerticalStackLayout>
        <controls:MyItemInputControlView NameLabelString="{Binding CounterClicked.Localized}"/>

    </VerticalStackLayout>

For convenient I just set the BindingContext of ContentPage to MainPageViewModel:

public MainPage(MainPageViewModel mainPageViewModel)
{
    InitializeComponent();
    this.BindingContext = mainPageViewModel;
}

And for MainPageViewModel:

public class MainPageViewModel
{
    ILocalizationResourceManager _localizationResourceManager;

    public LocalizedString CounterClicked { get; set; }

    public MainPageViewModel(ILocalizationResourceManager localizationResourceManager)
    {
        _localizationResourceManager = localizationResourceManager;
        CounterClicked = new(() => _localizationResourceManager["HelloWorld"]);
    }

}

Don't forget to register MainPageViewModel in MauiProgram as we use DI for it:

builder.Services.AddTransient<MainPageViewModel>();

And the NameLabelString could be localized and pass it to the ContentView. Much appreciated if you could have a try.

Update

Also if you don't want to use bindings, simply set

<VerticalStackLayout>
    <controls:MyItemInputControlView NameLabelString="{loc:Translate HelloWorld}"/>
</VerticalStackLayout>

This tutorial made by Gerald Versluis inspire me. You could refer to Translate Your .NET MAUI App with LocalizationResourceManager.Maui

8
Liqun Shen-MSFT On

Update This time I tried using bindable property.

We know that ContentPage could use Dependency injection in Construction but ContentView cannot. Let's suppose our MainPage (which consume the ContentView) has DI correctly in Constructor.

public class MainPageViewModel
{
    public ILocalizationResourceManager service { get; set; }
    
    // I assume you have registered a localizationResourceManager instance
    public MainPageViewModel(ILocalizationResourceManager localizationResourceManager)
    {
        service = localizationResourceManager;
    }
}

But we know that MyItemInputControlView(ContentView) cannot DI. So the problem is how to pass localizationResourceManager instance to ContentView. Then we use bindable property.

Let's create a bindable property for MyItemInputControlView:

public partial class MyItemInputControlView : ContentView
{
    ......
    public static readonly BindableProperty LocalizationResourceManagerProperty = BindableProperty.Create(nameof(LocalizationResourceManager), typeof(ILocalizationResourceManager), typeof(MyItemInputControlView), propertyChanged: OnServiceChanged);

    static void OnServiceChanged(BindableObject bindable, object oldValue, object newValue)
    {
    // Property changed implementation goes here
        ILocalizationResourceManager a = newValue as ILocalizationResourceManager;
    }

    public ILocalizationResourceManager LocalizationResourceManager
    {
        get => (ILocalizationResourceManager)GetValue(MyItemInputControlView.LocalizationResourceManagerProperty);
        set => SetValue(MyItemInputControlView.LocalizationResourceManagerProperty, value);
    }

    ...
}

Then in MainPage who consume the ContentView. Be attention with the binding, what i want is to pass the value of service property in MainPageViewModel to our BindableProperty LocalizationResourceManager.

<StackLayout>
        
    <controls:MyItemInputControlView LocalizationResourceManager="{Binding Source={x:Reference thispage},Path=BindingContext.service}"
                                         
    x:Name="firstContentView"/>

And each time LocalizationResourceManager changes, then update the label string with correct resource:

    static void OnServiceChanged(BindableObject bindable, object oldValue, object newValue)
    {
    // Property changed implementation goes here
        ILocalizationResourceManager a = newValue as ILocalizationResourceManager;
        label.text = "what you want";
    }

By the way I think you could just bind NameLabelString to one property in ViewModel instead of giving a string value to it.

<controls:MyItemInputControlView 
    NameLabelString="{Binding MyLabelText}"
    .......                                   
                                         
    x:Name="firstContentView"/>

That might be easier, right?

====================== origin answer ===============

Up to now there is not a direct way to use dependency injection in a custom control/ContentView.

See this issue on Github: Dependency Injection for Custom Controls. It's still enhancement under consideration and you could follow this issue.

And as far as I know, there should be some workarounds but depends on what you want.

One way is to use ServiceProvider. More info please refer to this discussions: Dependency Injection in ContentView #8363. You could define a ServiceProvider like this code:

public static class ServiceProvider
{
    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 in ContentView you could get service you register in MauiProgram.cs such like this:

var service = serviceProvider.GetService(typeof(ILocalizationResourceManager)) as ILocalizationResourceManager;

Another way is to attach a bindable property to ContentView then get the parameter through the ContentPage which consume the ContentView. More info you could refer to tripjump comments.

Hope it works for you.