mauimaui-community-toolkit

MAUI - Binding Issue: ActivityIndicator in BaseContentPage Not Updating from Inherited ViewModel


I'm trying to build a base application structure in .NET MAUI, but I'm facing an issue where commands and property bindings are not properly propagating to my BaseContentPage.

I want all my pages to use a shared layout (BaseContentPage), and each page should have its own ViewModel that inherits from BasePageViewModel.

My Approach: BaseContentPage.xaml (Used as a common layout) Here is my BaseContentPage, which includes an ActivityIndicator to show when IsBusy is true:

<ContentPage
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:baseViewModel="clr-namespace:MyProject.MAUI.ViewModels.Base"
    xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
    xmlns:layout="clr-namespace:MyProject.MAUI.Views.Layout"
    x:DataType="baseViewModel:BasePageViewModel"
    x:Class="MyProject.MAUI.Views.Layout.BaseContentPage">

<ContentPage.ControlTemplate>
    <ControlTemplate>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="70" />
                <!-- Header -->
                <RowDefinition Height="*" />
                <!-- Main content -->
                <RowDefinition Height="70" />
                <!-- Footer -->
            </Grid.RowDefinitions>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="50"/>
                <ColumnDefinition Width="5*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

            <!-- Header -->
            <Grid Grid.Row="0" Grid.ColumnSpan="3" BackgroundColor="#0078D7" Padding="10">
                
                <ActivityIndicator Grid.Column="0" x:Name="FirstActivityIndicator" IsRunning="{Binding IsBusy}" 
                        IsVisible="{Binding IsBusy}" 
                        HeightRequest="50"
                        WidthRequest="50"/>

                <!-- Page Title -->
                <Label Grid.Column="1" Text="MyProject Title" 
               FontSize="20" FontAttributes="Bold" 
               TextColor="White" VerticalOptions="Center" 
               HorizontalOptions="Center" />

            </Grid>

            <!-- Placeholder for main content-->
            <ScrollView Grid.Row="1" Grid.Column="1">
                <ContentPresenter x:Name="MainContent" />
            </ScrollView>
...

BasePageViewModel (Inherited by all ViewModels) This is the Base ViewModel that all other ViewModels will inherit from:

public partial class BasePageViewModel : ObservableObject
{
[ObservableProperty]
private bool isBusy;

public BasePageViewModel()
{
    IsBusy = false;
}

BasePageWithVm (Handles BindingContext) I created this generic BasePageWithVm to ensure that each page correctly binds its ViewModel:

public class BasePageWithVm<T> : BaseContentPage where T : BasePageViewModel
{
protected T ViewModel { get; }

public BasePageWithVm(T viewModel)
{
    BindingContext = ViewModel = viewModel;
}

PageConnexionView (A Page that Uses the Structure) I created PageConnexionView, which inherits from BasePageWithVm:

<layout:BasePageWithVm xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
         xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
         xmlns:layout="clr-namespace:MyProject.MAUI.Views.Layout"
         xmlns:viewModel="clr-namespace:MyProject.MAUI.ViewModels.Connexion"
         xmlns:toolKit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
         x:Class="MyProject.MAUI.Views.Connexion.PageConnexionView"
         x:DataType="viewModel:PageConnexionViewModel"
         x:TypeArguments="viewModel:PageConnexionViewModel" 
         Title="PageConnexionView">


<ContentPage.Content>
<Button Command="{Binding TestActivityMonitorCommand}"></Button>

<ActivityIndicator x:Name="SecondActivityIndicator" IsRunning="{Binding IsBusy}" 
     IsVisible="{Binding IsBusy}" 
     HeightRequest="50"
     WidthRequest="50"/>
....

With PageConnexionView.xaml.cs :

public partial class PageConnexionView : BasePageWithVm<PageConnexionViewModel>
{
   public PageConnexionView(PageConnexionViewModel vm) : base(vm)
   {
       InitializeComponent();
   }
}

PageConnexionViewModel (ViewModel with the Command)

public partial class PageConnexionViewModel : BasePageViewModel
{
public PageConnexionViewModel()
{

}

[RelayCommand]
private async Task TestActivityMonitor()
{
    IsBusy = true; // Show ActivityIndicator
    await Task.Delay(3000); // fakeLoading
    IsBusy = false; // Hide ActivityIndicator
}

The Issue āœ… The TestActivityMonitorCommand is correctly triggered when clicking the button. āœ… The ActivityIndicator inside PageConnexionView (SecondActivityIndicator) works perfectly (shows/hides as expected). āŒ However, the ActivityIndicator inside BaseContentPage (FirstActivityIndicator) never updates!

It looks like the BindingContext in BaseContentPage is different from PageConnexionViewModel :(

Did I miss something in my implementation? šŸ”¹ Is there a better way to ensure BaseContentPage always shares the same BindingContext as PageConnexionView?

Thanks a lot for your help!


Solution

  • The FirstActivityIndicator in BaseContentPage is defined in the ControlTemplate. If you want to bind a property of an element that is in a ControlTemplate, consider using TemplateBinding markup extension.

    Try the following code,

    <Grid Grid.Row="0" Grid.ColumnSpan="3" BackgroundColor="#0078D7" Padding="10">
    
        <ActivityIndicator Grid.Column="0" x:Name="FirstActivityIndicator" IsRunning="{TemplateBinding BindingContext.IsBusy}" 
            IsVisible="{TemplateBinding BindingContext.IsBusy}" 
            HeightRequest="50"
            WidthRequest="50"/>
    

    This binds the IsRunning property to the IsBusy property.

    For more info, please refer to Pass parameters with TemplateBinding.

    Hope it helps!