cw.MauiExtensions.Services 1.0.1

dotnet add package cw.MauiExtensions.Services --version 1.0.1
                    
NuGet\Install-Package cw.MauiExtensions.Services -Version 1.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="cw.MauiExtensions.Services" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="cw.MauiExtensions.Services" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="cw.MauiExtensions.Services" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add cw.MauiExtensions.Services --version 1.0.1
                    
#r "nuget: cw.MauiExtensions.Services, 1.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package cw.MauiExtensions.Services@1.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=cw.MauiExtensions.Services&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=cw.MauiExtensions.Services&version=1.0.1
                    
Install as a Cake Tool

cw.MauiExtensions.Services

A reusable .NET MAUI library providing page presentation services (standalone pages, pages with navigation stack, popups, alert dialogs), ViewModel lifecycle management, and customizable system bar (status bar and navigation bar) handling for Android, Windows.

Features

This library offers:

  • ViewPresenter: You can create any type of page: a start page with or without a navigation stack, a page to be pushed on the stack, a page replacing the start page, and modal pages/popups.
  • ViewModel Lifecycle Management: Hook your viewmodels to lifecycle events and disposal requests.
  • ContentDialog: A overlay-style modal dialog (popup) with semi-transparent backgrounds as the base for your own custom dialogs
  • AlertDialog: A simple alert/confirmation dialog with only a title, a message, and up to 2 buttons.
  • Smart System Bar Handling: Automatically configure status and navigation bar colors accross all Android API versions.
  • Page Removal Events: Subscribe to page removal notifications wherever you want.

The ViewPresenter

The purpose of the ViewPresenter is to be a replacement for AppShell and get rid of some questionable features of AppShell, in particular:

  • URL-based routing which uses web-style navigation where apps are typically state-driven.
  • Shell hides too much behavior behind implicit magic: page instantiation, parameter injection, ..
  • Dependency Injection is awkward and fragile with Shell.
  • Shell enforces UI structure too early.
  • Shell doesn't scale well.

You can open and close pages by using the following methods of the ViewPresenter singleton instance:

Method 1: OpenMainPage(Type viewType, object? viewModel)

Creates and returns a new main page instance of the specified type, optionally initialized with the provided view model. With this method the page is not created in a NavigationPage and so doesn't support the MAUI navigation stack.

  • When the method is invoked with a non-null viewModel then the page being instantiated is expected to have a constructor with a viewModel object as parameter. The page then typically binds the viewmodel parameter to its BindingContext.
  • When viewModel is null then the page constructor must be parameterless. You can then choose to work with or without a view model in the page code-behind.

Example:

protected override Window CreateWindow(IActivationState? activationState)
{
    var page = ViewPresenter.Instance.OpenMainPage(typeof(Views.DemoTabbedPage), null);
    return new Window(page);
}

The method is typically invoked at startup of the app in App.CreateWindow which then creates a new Window where the page will be the "canvas" with the UI. If the method is called later at a moment where Application.Current.Windows already has a page assigned, then the assigned page will be replace by the newly created page.

Method 2: OpenMainNavigationPage(Type viewType, object? viewModel)

Creates and returns a new MAUI NavigationPage, or replaces a already assigned one, and assigns the page defined by the specified view type as root of the navigation stack.

  • When the method is invoked with a non-null viewModel then the page being instantiated is expected to have a constructor with a viewModel object as parameter. The page then typically binds the viewmodel parameter to its BindingContext.
  • When viewModel is null then the page constructor must be parameterless. You can then choose to work with or without a view model in the page code-behind.

Use this method if you want to create an app that uses a navigation stack for navigating between pages.

Example:

protected override Window CreateWindow(IActivationState? activationState)
{
    var page = ViewPresenter.Instance.OpenMainNavigationPage(typeof(Views.HomePage), new ViewModels.HomeViewModel());
    return new Window(page);
}

Also here, the method is usually called at startup of the app in App.CreateWindow which then creates a new Window(page). If the method is called later at a moment where Application.Current.Windows already has a page assigned, then the assigned page will be replace by the newly created NavigationPage.

Method 3: PushPageAsync(Type viewType, object? viewModel, int pagesToPopCount = 0)

Opens a new page of the specified type by pushing the page on the MAUI navigation stack. This method requires a NavigationPage to be already assigned to the current window. See method 2.

  • When the method is invoked with a non-null viewModel then the page being instantiated is expected to have a constructor with a viewModel object as parameter. The page then typically binds the viewmodel parameter to its BindingContext.
  • When viewModel is null then the page constructor must be parameterless. You can then choose to work with or without a view model in the page code-behind.

Example:

public partial class HomeViewModel : ObservableObject, IPageLifecycleAware
{
    [RelayCommand]
    async Task OpenNonModalPage()
    {
        // Navigate to a non-modal page
        await ViewPresenter.Instance.PushPageAsync(typeof(NonModalPage), new NonModalViewModel(pageNumber: 1));
    }
    ...
}

Method 4: PopPageAsync(int nrofPagesToPop = 1)

Removes one or more pages from the top of the navigation stack.

Example:

[RelayCommand]
async Task Save()
{
    // Inform listeners
    WeakReferenceMessenger.Default.Send(new LocationConfiguredMessage(new LocationInfoEvent(_isLocA, StorageLoc)));

    await ViewPresenter.Instance.PopPageAsync();
}

Method 5: OpenModalPageAsync(Page modalPage)

Displays the specified page as a modal dialog on top of the current page. The method can be called from any open page and is put on the MAUI modal pages stack which supports multiple modal pages.</br> The modal page can be instructed to open in FullScreen or Overlay mode.</br>

Example - Full screen modal

<?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:vm="clr-namespace:cw.MauiExtensions.Services.Demo.ViewModels"
             x:DataType="vm:ModalViewModel"
             x:Class="MauiExtensions.Demo.Views.ModalPage"
             Title="Modal page"
             Background="{AppThemeBinding Light={StaticResource PageBackground}, Dark={StaticResource PageBackgroundDark}}">
    <VerticalStackLayout Padding="18,48,18,0"
                         Spacing="25">
    <Button Text="X"
            Command="{Binding CloseModalCommand}"/>
    <Label Text="This is a modal page. Tap X to close."
           FontSize="Medium" />
  </VerticalStackLayout>
</ContentPage>
// Full-screen modal page - no need to set mode, FullScreen is default
public partial class ModalPage : ContentPage
{
    public ModalPage()
    {
        InitializeComponent();
        
        // ModalPageMode.FullScreen is the default - no action needed
    }
}

[RelayCommand]
async Task OpenModalPage()
{
    // Navigate to modal page on the stack
    await ViewPresenter.Instance.OpenModalPageAsync(new ModalPage(new ModalViewModel()));
}

Example - Overlay modal / popup

public partial class MyPopupDialog : ContentPage
{
    public MyPopupDialog()
    {
        InitializeComponent();
        
        // Only needed for custom overlay dialogs
        ModalPageProperties.SetMode(this, ModalPageMode.Overlay);
    }
}

[RelayCommand]
async Task OpenPopupDialog()
{
    // Navigate to modal page on the stack
    await ViewPresenter.Instance.OpenModalPageAsync(new MyPopupDialog());
}

Remark - See also ContentDialog for popups returning a result.

Method 6: CloseModalPageAsync()

Closes the topmost modal page if one is present on the application's main window. The system will bring back a possible previous modal page or the underlying non-modal page.

Example:

[RelayCommand]
async Task CloseModal()
{
    // Close the modal page
    await ViewPresenter.Instance.CloseModalPageAsync();
}

AlertDialog

The library comes with a AlertDialog class for simple alert/confirmation scenarios where the popup only needs a title, a description and 1 or 2 buttons.</br> The constructor is: public AlertDialog(string title, string text, string primaryBttnText, string? secondaryBttnText)

  • Parameter title is a string that is presented at the top of the popup
  • Parameter text defines the text that shows below the title.
  • Parameter primaryBttnText defines the text of the 1st button in the dialog. You can assign any meaning to it.
  • Parameter secondaryBttnText defines the text of an optional 2nd button in the dialog.

The popup is opened by invoking the ShowAsync() method on the created AlertDialog. The method returns a ContentDialogResult which is an enum value that can be: None, Primary, or Secondary. None is returned when the dialog is closed by tapping outside of the popup.

Example:

[RelayCommand]
async Task ShowAlert()
{
    var alert = new AlertDialog(title: "Confirm Delete",
                                text: "Are you sure you want to delete this item? This action cannot be undone.",
                                primaryBttnText: "Delete",
                                secondaryBttnText: "Cancel");

    var result = await alert.ShowAsync();

    if (result == ContentDialogResult.Primary)
    {
        await DeleteItemAsync();
    }
)

You can style 2 things in the AlertDialog:

  • The style of the content border. Configure ResourceKeys.AlertDialogBorderStyle for this.
  • The style of the 2 buttons. Configure ResourceKeys.AlertDialogButtonStyle for this.

To make it even easier to popup an alert dialog , the library also provides a static helper method AlertDialog.ShowAlertAsync(string title, string text, string primaryBttnText, string? secondaryBttnText)</br> Example:

await Alert.ShowAsync("Error", "You must enter a server address", "OK");

ContentDialog for easy popup dialogs

The library comes with a ContentDialog class that you can use as a base class for your own custom popup dialogs. It supports: type-safe results, full XAML support, automatic overlay styling, and lifecycle management.

Example:

using cw.MauiExtensions.Services.Views;

var dialog = new ContentDialog
{
    ContentView = new MyCustomView()
};

var result = await dialog.ShowAsync();
// Handle result: ContentDialogResult.None, Primary, or Secondary

See the ContentDialog documentation for a detailed description.


System bars and navigation bar coloring

The latest UI recommendations for mobile apps call for a UI where all page background colors extend to the device's status bar, the maui navigation bar (when available) and the system navigation bar (when available).</br> Out of the box MAUI does not provide this functionality on all platforms. Especially on Android support for configuring the system bars across all Android versions is limited.</br>

How does the library help you with this?

1. iOS

On iOS 13+ the color of the status bar is transparant. This has the following advantages:

  • On a page with a MAUI navigation stack the MAUI navigation bar is drawn as an overlay on top of the system bar with a vertical offset, so true edge-to-edge display is possible by assigning the same background color to the navigation bar and page background.
  • On a page that doesn't use the MAUI navigation stack and on modal pages (full screen or overlay) the background color of the page extends to the bar. Depending on the value of IgnoreSafeArea (typically set as a property of the root container of a page) the page content starts below or at the top of the bar. So it is possible to achieve true edge-to-edge display.

Summary - With iOS the library doesn't need to do anything special to achieve edge-to-edge display.

2. Android

To achieve a true edge-to-edge layout on Android the library comes with a number of configuration options. The 2 most important are: UseSmartSystemBarColoring and UseSmartSystemBarColoringWithModals. Both are true by default. If you set them to false then the library will not do anything special and the system bars (status bar at the top and navigation bar at the bottom) will stay as configured by the OS or by other libraries such as CommunityToolkit.Maui.

The next thing to consider is whether you want edge-to-edge for root pages and/or for modal pages.

3.1 Root pages

EdgeToEdgeForRootPages - This option indicates whether at the start of the app (and when the theme changes) the library must extend the page content under the system bars or not.</br>

  • If true (the default for Android API 32+) the library will configure the system not to adjust the view to fit inside the "safe areas" and not draw a solid color behind the bars on Android running API 32+. This works well for all type of pages, including a NavigationPage. Unfortunately it doesn't work as expected when you have a NavigationPage and you want the bars to automatically update when the app is open and you change the theme.</br> Per default there will be a offset for the page content in order to start below the status bar. If you don't want that you must set the option EdgeToEdgeStartContentBelowBar to false.
  • If false and when running on Android API < 32, the library will explicitly color the systems bars with the color referenced by SystemBarsBackgroundColor (light mode) and SystemBarsBackgroundDarkColor (dark mode). No change is done on the insets and therefore the page starts below the status bar.</br> This is what you must use when your app uses a NavigationPage and also when you want the color of the system bars to be different from the background of the page.

In both cases the library also sets the tint of the icons of the system bars to light or dark by looking at the brightness of SystemBarsBackgroundColor or SystemBarsBackgroundDarkColor.

Theme changes - Unless UseSmartSystemBarColoring is set to false the library will automatically react to a theme change and re-draw the system bars.

Required resource settings for any type of page

The library retrieves the color of the system bar from Application.Current.Resources, i.e. from Colors.xaml. The resource keys can be changed in SystemBarsBackgroundColor and SystemBarsBackgroundDarkColor but if you want to use the default keys you need a definition for:

  • "SystemBarsBackground" and
  • "SystemBarsBackgroundDark"
Required resource settings for a MAUI NavigationPage

When the root page is a MAUI NavigationPage and you want to have a edge-to-edge background color then you must also configure the navigation bar and page background colors to match the system bar colors. This can be easily done by opening the main page using ViewPresenter.OpenMainNavigationPage. This method automatically sets both the background color and the text color of the MAUI navigation bar to the color referenced by NavigationBarBackgroundColor or NavigationBarBackgroundDarkColor and NavigationBarTextColor or NavigationBarTextDarkColor.

If you are OK with with the default expected resource keys then you must configure the following keys in Colors.xaml:

  • "SystemBarsBackground" and "SystemBarsBackgroundDark"
  • "NavigationBarBackground" and "NavigationBarBackgroundDark"
  • "NavigationBarText" and "NavigationBarTextDark"

3.2 Modal and Popup pages

EdgeToEdgeForModalPages - This option indicates whether the library must extend the modal page content under the system bars or not.</br>

  • If true (the default) the library will configure the page to extend beneath the system bars on Android running API 32+. This is the best option for a edge-to-edge layout with a single background color on the whole screen.</br> Per default there will be a offset for the page content in order to start below the status bar. If you don't want that you must set the option EdgeToEdgeStartContentBelowBar to false.

  • If false , or when the API is lower than 32, the library will explicitly color the systems bars with the color referenced by SystemBarsBackgroundColor (light mode) and SystemBarsBackgroundDarkColor (dark mode). No change is done on the insets and therefore the modal page starts below the status bar.</br> Use this setting also when you want the color of the system bars to be different from the background of the page.

In both cases the tint of the icons of the system bars is determined by the Android system by looking at the brightness of the background color.</br>

Note - Setting EdgeToEdgeForModalPages to true also works well when using CommuniyToolkit.Maui popups across all Android versions.

Required resource settings for modal pages

You must provide a key definition in Colors.xaml for:

  • "SystemBarsBackground" and
  • "SystemBarsBackgroundDark"

3. Windows

On Windows all pages start below the title bar and therefore true edge-to-edge display is not possible unless you create a TitleBar control for the window and style it to match the background color with the page background and, if available, assign the same color to the MAUI navigation bar.</br>

The TitleBar is created as follows (example in xaml):

<?xml version="1.0" encoding="utf-8" ?>
<TitleBar xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
          xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
          xmlns:vm="clr-namespace:cw.MauiExtensions.Services.Demo.ViewModels"
          Title="Demo App"
          Subtitle="Welcome to the Demo App"
          Icon="demoicon.png"
          HeightRequest="48"
          BackgroundColor="{AppThemeBinding Light={StaticResource SystemBarsBackground}, Dark={StaticResource SystemBarsBackgroundDark}}"
          ForegroundColor="{AppThemeBinding Light={StaticResource NavigationBarText}, Dark={StaticResource NavigationBarTextDark}}"
          x:DataType="vm:DesktopTitleBarViewModel"
          x:Class="cw.MauiExtensions.Services.Demo.Views.DesktopTitleBar"
          x:Name="RootTitleBar"> 
  <TitleBar.Content>
    <Grid ColumnDefinitions="Auto,*,Auto"
          Margin="0"
          Padding="8,0"
          WidthRequest="400">
      <Image Grid.Column="0" 
             Source="info.png"
             HeightRequest="16" 
             WidthRequest="16"
             Margin="0,0,8,0"/>
      <Grid Grid.Column="1"
            Background="Transparent">
        <Label Text="Can't drag here"
               VerticalOptions="Center"
               HorizontalOptions="Start"
               FontSize="12"/>
      </Grid>
      <ImageButton Grid.Column="2"
                   HeightRequest="24"
                   WidthRequest="24"
                   BorderWidth="0"
                   Source="settings.png"
                   Background="Transparent"
                   Command="{Binding BindingContext.ShowSettingsCommand, Source={x:Reference RootTitleBar}}"/>
      </Grid>
  </TitleBar.Content>
</TitleBar>

And in App.xaml.cs:

protected override Window CreateWindow(IActivationState? activationState)
{
    var page = ViewPresenter.Instance.OpenMainNavigationPage(typeof(HomePage), new HomeViewModel());
    var titleBar = new DesktopTitleBar(new DesktopTitleBarViewModel());
    Window window = new Window()
    {
        Page = page,
        TitleBar = titleBar
    };
    return window;
}

The TitleBar control is ignored when the app is running on Android or iOS.

Remark - Modal pages (full screen and overlay) also start below the title bar. When a page has a semi-transparant background color then the previous page will shine through and typically will be dimmed. The title bar however will stay untouched.

Summary - With Windows the library doesn't need to do anything special to achieve edge-to-edge display, provided you configure a custom TitleBar.


ViewModel Lifecycle Management

When using pages and popups, it is important to manage the lifecycle of the associated ViewModels properly. MAUI doesn't provide built-in support for ViewModel lifecycle events, which can lead to memory leaks and unexpected behavior.

cw.MauiExtensions.Services provides automatic ViewModel lifecycle management through two opt-in interfaces:

IPageLifecycleAware

Receive notifications when your ViewModel's page appears or disappears:

using cw.MauiExtensions.Services.Interfaces;

public class MyViewModel : ObservableObject, IPageLifecycleAware
{
    public void OnPageCreated()
    {
        // Called when page appears
        // Start timers, refresh data, subscribe to events
    }

    public void OnPageDestroyed()
    {
        // Called when page disappears
        // Stop timers, pause operations
    }
}

Use cases:

  • Refresh data when page appears
  • Start/stop timers based on visibility
  • Track page views for analytics
  • Save state when navigating away

IDisposableOnViewClosed

Automatically dispose resources when the page is removed from navigation:

using cw.MauiExtensions.Services.Interfaces;

public class MyViewModel : ObservableObject, 
                           IPageLifecycleAware,
                           IDisposableOnViewClosed
{
    private Timer? _refreshTimer;
    private bool _isDisposed;

    public void OnPageCreated()
    {
        _refreshTimer = new Timer(5000);
        _refreshTimer.Start();
    }

    public void OnPageDestroyed()
    {
        _refreshTimer?.Stop();
    }

    public void Dispose()
    {
        if (_isDisposed) return;
        
        _refreshTimer?.Stop();
        _refreshTimer?.Dispose();
        _refreshTimer = null;
        
        _isDisposed = true;
    }
}

Use cases:

  • Unsubscribe from event publishers
  • Dispose timers and HTTP clients
  • Cancel ongoing async operations
  • Release unmanaged resources
  • Prevent memory leaks

Lifecycle Event Flow

Page Created
    ↓
ViewPresenter hooks lifecycle events
    ↓
Page.Appearing → IPageLifecycleAware.OnPageCreated()
    ↓
[User interacts with page]
    ↓
Page.Disappearing → IPageLifecycleAware.OnPageDestroyed()
    ↓
Page Removed/Popped
    ↓
Unhook events → IDisposableOnViewClosed.Dispose()
    ↓
PageDestroyed event raised

Page Destroyed Notifications

The library alse provides a PageDestroyed event in ViewPresenter to get a notification when a page is removed. It is triggered in the following scenarios:

  • A page is popped from the navigation stack.
  • A modal page is closed.

Note: before the event is raised and when the page has a IDisposableOnPageClosed viewmodel assigned to its BindingContext, the viewmodel's Dispose method (if implemented) will be called.

WeakReferenceMessenger

Any object in your app can subscribe to the PageDestroyed event to get notified when pages are removed. If you prefer however a more loosely coupled approach, you can of course also use the CommunityToolkit.Mvvm WeakReferenceMessenger to broadcast the event to interested objects and/or services. Do this by subscribing to the PageDestroyed event in a centralized location (e.g. App.xaml.cs) and broadcasting the event via the messenger.</br> Like so:

using cw.MauiExtensions.Services.Core;
using cw.MauiExtensions.Services.Events;

public partial class App : Application
{
    public App()
    {
        InitializeComponent();
        
        // Subscribe to page removal events
        ViewPresenter.Instance.PageDestroyed += OnPageRemoved;
    }

    private void OnPageRemoved(object? sender, PageRemovedEventArgs e)
    {
        Debug.WriteLine($"Page removed: {e.RemovedPage.GetType().Name}");
        
        // Optionally broadcast via WeakReferenceMessenger
        WeakReferenceMessenger.Default.Send(e);
    }
}

An object; e.g. a viewModel can then subscribe to this event like so:

using CommunityToolkit.Mvvm.Messaging;
using cw.MauiExtensions.Services.Events;

public class MyViewModel : ObservableObject,
                           IDisposableOnViewClosed
{
    public MyViewModel()
    {
        WeakReferenceMessenger.Default.Register<PageRemovedEventArgs>(this, (r, m) =>
        {
           // Do something
        });
    }

    public void Dispose()
    {
        WeakReferenceMessenger.Default.UnregisterAll(this);
    }
}

Installation

  1. Add a reference to the cw.MauiExtensions.Services project in your MAUI application.

  2. Register the services in your MauiProgram.cs:

using cw.MauiExtensions.Services.Extensions;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .UseMauiExtensionsServices() // Add this line
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

        return builder.Build();
    }
}
  1. Make sure that the following resources are defined:

Required Color Resources

Define these colors in your App.xaml or Colors.xaml (choose your own colors):


<Color x:Key="SystemBarsBackground">#FFFFFF</Color>
<Color x:Key="SystemBarsBackgroundDark">#000000</Color>


<Color x:Key="NavigationBarBackground">#FFFFFF</Color>
<Color x:Key="NavigationBarBackgroundDark">#000000</Color>
<Color x:Key="NavigationBarText">#101010</Color>
<Color x:Key="NavigationBarTextDark">#FFFFFF</Color>


<Color x:Key="ContentDialogBackgroundOverlay">#4C000000</Color>
<Color x:Key="ContentDialogBackgroundOverlayDark">#80000000</Color>

Additional required Colors in the ContentDialogBorder style when AlertDialog is used


<Color x:Key="ContentDialogBorderBackground">#FAFCFE</Color>
<Color x:Key="ContentDialogBorderBackgroundDark">#202020</Color>
<Color x:Key="ContentDialogBorderStroke">#E0E0E0</Color>
<Color x:Key="ContentDialogBorderStrokeDark">#3D3D3D</Color>

Required Styles

Define these styles in your Styles.xaml:


<Style x:Key="ContentDialogBorder" TargetType="Border">
    <Setter Property="VerticalOptions" Value="Center" />
    <Setter Property="Padding" Value="10,24,10,24"/>
    <Setter Property="StrokeShape" Value="RoundRectangle 15"/>
    <Setter Property="StrokeThickness" Value="1"/>
    <Setter Property="Stroke" Value="{AppThemeBinding Light={StaticResource ContentDialogBorderStroke}, Dark={StaticResource ContentDialogBorderStrokeDark}}" />
    <Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource ContentDialogBorderBackground}, Dark={StaticResource ContentDialogBorderBackgroundDark}}" />
</Style>


<Style x:Key="TextOnlyButton" TargetType="Button">
    <Setter Property="BackgroundColor" Value="Transparent" />
    <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
    <Setter Property="BorderWidth" Value="0" />
    <Setter Property="Padding" Value="8,4" />
</Style>

Configuration

Configure the library in your MauiProgram.cs to customize resource keys and behavior:

using cw.MauiExtensions.Services.Extensions;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .UseMauiExtensionsServices(options =>
            {
                // Customize resource key names if you use different names in your app
                options.ResourceKeys.AlertDialogBorderStyle = "ContentDialogBorder";
                options.ResourceKeys.AlertDialogButtonStyle = "TextOnlyButton";

                options.ResourceKeys.SystemBarsBackgroundColor = "SystemBarsBackground";
                options.ResourceKeys.SystemBarsBackgroundDarkColor = "SystemBarsBackgroundDark";
                options.ResourceKeys.NavigationBarBackgroundColor = "PageBackground";
                options.ResourceKeys.NavigationBarBackgroundDarkColor = "PageBackgroundDark";
                options.ResourceKeys.NavigationBarTextColor = "NavigationBarText";
                options.ResourceKeys.NavigationBarTextDarkColor = "NavigationBarTextDark";
                options.ResourceKeys.ContentDialogBackgroundOverlayColor = "ContentDialogBackgroundOverlay";
                options.ResourceKeys.ContentDialogBackgroundOverlayDarkColor = "ContentDialogBackgroundOverlayDark";
                
                options.UseSmartSystemBarColoring = true; // Default: true
                options.UseSmartSystemBarColoringWithModals = true; // Default: true
                
                // Configure edge-to-edge display
                options.EdgeToEdgeForRootPages = true; // Default: true
                options.EdgeToEdgeForModalPages = true; // Default: true
                options.EdgeToEdgeStartContentBelowBar = true;  // Default: true
            })
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

        return builder.Build();
    }
}

Configuration Options

Property Type Default Description
ResourceKeys.AlertDialogBorderStyle string "ContentDialogBorder" Border style applied to alert dialogs
ResourceKeys.AlertDialogButtonStyle string "TextOnlyButton" Style applied to alert dialog buttons
ResourceKeys.ContentDialogBackgroundOverlayColor string "ContentDialogBackgroundOverlay" Overlay color for content dialogs (light mode)
ResourceKeys.ContentDialogBackgroundOverlayDarkColor string "ContentDialogBackgroundOverlayDark" Overlay color for content dialogs (dark mode)
ResourceKeys.NavigationBarBackgroundColor string "NavigationBarBackground" MAUI NavigationBar background color (light mode)
ResourceKeys.NavigationBarBackgroundDarkColor string "NavigationBarBackgroundDark" MAUI NavigationBar background color (dark mode)
ResourceKeys.NavigationBarTextColor string "NavigationBarText" MAUI NavigationBar text color (light mode)
ResourceKeys.NavigationBarTextDarkColor string "NavigationBarTextDark" MAUI NavigationBar text color (dark mode)
ResourceKeys.SystemBarsBackgroundColor string "SystemBarsBackground" System bars background color (light mode)
ResourceKeys.SystemBarsBackgroundDarkColor string "SystemBarsBackgroundDark" System bars background color (dark mode)
EdgeToEdgeForRootPages bool true Enable edge-to-edge display for root pages
EdgeToEdgeForModalPages bool true Enable edge-to-edge display for modal pages
EdgeToEdgeStartContentBelowBar bool true When edge-to-edge is enabled, add padding to start content below the status bar
UseSmartSystemBarColoring bool true Enable smart system bar coloring at app startup and on theme changes
UseSmartSystemBarColoringWithModals bool true Enable smart system bar color handling for modal pages and popups

Troubleshooting

MissingResourceException

If you get a MissingResourceException, ensure you have defined all required color resources in your Colors.xaml or App.xaml. The exception message will tell you which resource key is missing.


License

This project is licensed under the MIT License - see the LICENSE file for details.

Copyright (c) 2025 [Jos Huybrighs]

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Product Compatible and additional computed target framework versions.
.NET net10.0-android36.0 is compatible.  net10.0-ios26.0 is compatible.  net10.0-maccatalyst26.0 is compatible.  net10.0-windows10.0.19041 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0-android36.0

    • No dependencies.
  • net10.0-ios26.0

    • No dependencies.
  • net10.0-maccatalyst26.0

    • No dependencies.
  • net10.0-windows10.0.19041

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1 136 1/23/2026
1.0.0 129 1/21/2026