4

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?

OXO
  • 391
  • 1
  • 8
  • Need to show more code. What ContentView? Show its declaration. What 2 parameters? Its up to you to declare the constructor you need. Show the constructor(s) you have now in ContentView. (Edit question to add this code.) Might be useful to also show XAML that uses that ContentView. – ToolmakerSteve Mar 13 '23 at 21:11
  • @ToolmakerSteve - I have added the relevant Code. Basically, I want to assign my ILocalizationResourceManager-Instance and use it while the ContentView is instantiated to translate the provided Labels from the Resource-Files. Unfortunately, when I try to call NameLabelString="{loc:Translate ManageItem_MyProductNameLabel}" from within my ContentPage, it would not translate it, whereas other elements on the ContentPage can be translated like this. Maybe since it is a ContentView or due to the DataBinding? So, the idea was to translate it in the Property Getter in the Code-Behind. – OXO Mar 14 '23 at 06:19
  • (1) What happens if you comment out the default constructor? (2) I assume it’s a typo, but your MauiProgram.cs builder refers to a different ContentView name. – ToolmakerSteve Mar 14 '23 at 17:57
  • 1
    1) It says there is not such constructor and is missing the default constructor 2) Oh yes, this was a type and I corrected it – OXO Mar 14 '23 at 19:04

2 Answers2

1

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

Liqun Shen-MSFT
  • 3,490
  • 2
  • 3
  • 11
  • I think this is a good way to realize this! What I did not mention is that there is not only one MyItemInputControlView on my ContentPage, but the ContentPage implements a couple of them in a row with different Names that have to be localized. So, I think, I need more properties. What I really don't get and maybe there is an explanation for that, but when you consider the way I did it in my ContentPage, why is the following not working? ``````? – OXO Mar 14 '23 at 19:31
  • I think your code also good and works fine on my side. I just change to `````` and without any other change on my code(you can see my updates). It works fine . – Liqun Shen-MSFT Mar 15 '23 at 02:18
0

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.

Liqun Shen-MSFT
  • 3,490
  • 2
  • 3
  • 11
  • I would prefer a BindableProperty, though. Don't know how to set it correctly. When I introduce a Property in MyItemViewModel and set ILocalizationResourceManager in constructor of ManageMyItem before InitializeComponent() , I would expect to have it set initially. Then in InitializeComponent(), I would expect to have my MyItemInputControlView setup. In Code-Behind, I would introduce a BindableProperty. I also introduced a in MyContentPage. When View is loaded, I miss Binding – OXO Mar 14 '23 at 07:48
  • What I wanted to say is that at the time my ContentView is initialized it needs the ILocalizationResourceManager and my Binding obviously is not initiated or set to this instance from the ViewModel at this time. Don't know what I am doing wrong here. – OXO Mar 14 '23 at 08:04
  • Okay, I will update my answer using an bindable property – Liqun Shen-MSFT Mar 14 '23 at 08:08
  • @OXO Hi, I have updated my answer. If you have any question, feel free to ask. – Liqun Shen-MSFT Mar 14 '23 at 09:02
  • And **NameLabelString** is a bindable property it could also bind to property in ViewModel. – Liqun Shen-MSFT Mar 14 '23 at 09:18
  • Basically, what I want is probably just translate the provided NameLabelString-Value. In my Content-Page it is possible like this: ```Title="{loc:Translate MyItem_Heading}">``` with "MyItem_Heading" from the Resource-File. Unfortunately, "NameLabelString", values for Entrys in ContentView are just bindable to ContentView (Code-Beh) and BindableProperties there. I don't know, how I could change that, but bottom line is, what I wanted to do is a call of just `````` from my ContentPage – OXO Mar 14 '23 at 10:45
  • Your solution looked good and was the way, I thought it should work. But, in InitializeComponent() when the view is created, I still crash. It looks like the Binding did not work to the BindingContext.service from the ContentPage. Anyway, if the Binding could work as discussed above and I could get also bound values from my Entry of the ContentView (which I left out of my Code Example), I would use the loc:Translate construct – OXO Mar 14 '23 at 11:13
  • @OXO Hi, I add a new answer and please have a try. – Liqun Shen-MSFT Mar 14 '23 at 13:45