Home > Enterprise >  Binding MAUI ControlTemplate to ContentPage view model
Binding MAUI ControlTemplate to ContentPage view model

Time:01-14

I'm trying to add a common control I want to appear at the bottom of every content page in the app (these will all be inside a TabBar). I made a ControlTemplate in my App.xaml, and the Picker I placed in the bottom appears, but the ItemsSource property binding isn't working (there are no items visible).

I'm not sure how to get this to work. I'm new to Xamarin/MAUI, and am open to suggestions for different approaches if I'm going in the wrong direction to accomplish this.

I've tried using TemplateBinding instead of Binding in the XAML, and I've also placed the same properties in the App.xaml.cs and the AppShell.xaml.cs code-behind files, in case the bindings were being redirected there, which didn't make a difference. I also started out with the Environments property just being of type Env[], and switched to ObservableCollection as a troubleshooting measure (even though the collection is obviously static).

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:My.App"
             x:Class="My.App">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Resources/Styles/Colors.xaml" />
                <ResourceDictionary Source="Resources/Styles/Styles.xaml" />
                <ResourceDictionary>
                    <ControlTemplate x:Key="InputPageTemplate">
                        <VerticalStackLayout BindingContext="{Binding Source={RelativeSource TemplatedParent}}">
                            <ContentPresenter />
          <!-- ********************  BINDINGS NOT WORKING ******************** -->
                            <Picker ItemsSource="{Binding Environments}"
                                    SelectedItem="{Binding AppConfig.Environment}" />
                        </VerticalStackLayout>
                    </ControlTemplate>
                </ResourceDictionary>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

MyPage.cs

public class MyPage : ContentPage
{
    public MyPage()
    {
        if (!Application.Current!.Resources.TryGetValue("InputPageTemplate", out var resource) ||
            resource is not ControlTemplate template)
        {
            throw new Exception("Missing InputPageTemplate control template");
        }

        var appConfig = new AppConfig { Environment = Env.B };
        ViewModel = new MyViewModel(appConfig);
        BindingContext = ViewModel;
        ControlTemplate = template;
    }
}

MyViewModel.cs

public class MyViewModel
{
    public MyViewModel(AppConfig appConfig)
    {
        AppConfig = appConfig;
    }

    public AppConfig AppConfig { get; }
    public ObservableCollection<Env> Environments => new(Enum.GetValues<Env>());
}

AppConfig.cs

public class AppConfig : INotifyPropertyChanged
{
    private Env _environment;
    public Env Environment
    {
        get => _environment;
        set
        {
            _environment = value;
            OnPropertyChanged();
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    public void OnPropertyChanged([CallerMemberName] string name = "") =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

Env.cs

public enum Env
{
    A,
    B
}

CodePudding user response:

Ok this is going to be long.

When you want to bind something in your ControlTemplate, you do not bind it directly. You have to use different source.

There are few ways to do this. One is to use the word TemplateBinding. The other is to set the BindingContext to Templated parent.

For example:

<Control Template...
   <Grid BindingContext="{Binding Source={RelativeSource TemplatedParent}"...
       <Button Command="{Binding MyCommand}"...

When you press that button, the binding will execute MyCommand, of "whatever" the control is during runtime.

So, If in your ViewModel you have GetDataCommand, you will do this in your View:

<mycontrols:MyView MyCommand="{Binding GetDataCommand}...

The deal here is to have this custom control class MyView with BindableProperty. That will allow you to bind between the ViewModel and the ControlTemplate.

public class MyView : ContentView{
     public static readonly MyCommandProperty = ...
     public ICommand MyCommand...
}

When I am not sure how something is done, I usually click F12 and browse the platform code. First, you get the idea how everything works, second you learn how to do it yourself.

Also, I recommend that you use the Dependency Injection, and limit the use of constructors. I also recommend CommunityToolkit.MVVM. It will save you time from implementing INotifyPropertyChanged. (And the possible mistakes from doing it wrong).

CodePudding user response:

The way to get the template to share a binding context with the content page is to include a path in the binding, like so:

<VerticalStackLayout
   BindingContext="{Binding Source={RelativeSource TemplatedParent}, Path=BindingContext}">
<!--     Add this:                                                 ^^^^^^^^^^^^^^^^^^^^^    -->
  • Related