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!
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!