Back to Blog
4 min read

.NET MAUI Preview: Cross-Platform App Development

.NET MAUI (Multi-platform App UI) is approaching its release with the latest preview at Build 2022. This framework enables building native cross-platform applications for Android, iOS, macOS, and Windows from a single codebase.

Getting Started with MAUI

Install the required workloads:

# Install MAUI workload
dotnet workload install maui

# Create new MAUI project
dotnet new maui -n MyMauiApp

# Build and run
cd MyMauiApp
dotnet build -t:Run -f net7.0-android

Project Structure

MAUI uses a single project for all platforms:

<!-- MyMauiApp.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net7.0-android;net7.0-ios;net7.0-maccatalyst</TargetFrameworks>
    <TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">
      $(TargetFrameworks);net7.0-windows10.0.19041.0
    </TargetFrameworks>
    <OutputType>Exe</OutputType>
    <RootNamespace>MyMauiApp</RootNamespace>
    <UseMaui>true</UseMaui>
    <SingleProject>true</SingleProject>
  </PropertyGroup>

  <ItemGroup>
    <MauiIcon Include="Resources\AppIcon\appicon.svg" />
    <MauiSplashScreen Include="Resources\Splash\splash.svg" />
    <MauiImage Include="Resources\Images\*" />
    <MauiFont Include="Resources\Fonts\*" />
  </ItemGroup>
</Project>

Building a Product Catalog App

Create the main app structure:

// MauiProgram.cs
using Microsoft.Extensions.Logging;

namespace MyMauiApp;

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

        // Register services
        builder.Services.AddSingleton<IProductService, ProductService>();
        builder.Services.AddSingleton<IAuthService, AuthService>();
        builder.Services.AddHttpClient<IApiClient, ApiClient>(client =>
        {
            client.BaseAddress = new Uri("https://api.example.com/");
        });

        // Register ViewModels
        builder.Services.AddTransient<ProductListViewModel>();
        builder.Services.AddTransient<ProductDetailViewModel>();
        builder.Services.AddTransient<CartViewModel>();

        // Register Pages
        builder.Services.AddTransient<ProductListPage>();
        builder.Services.AddTransient<ProductDetailPage>();
        builder.Services.AddTransient<CartPage>();

#if DEBUG
        builder.Logging.AddDebug();
#endif

        return builder.Build();
    }
}

MVVM with CommunityToolkit

Use the MVVM Community Toolkit for cleaner code:

// ViewModels/ProductListViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace MyMauiApp.ViewModels;

public partial class ProductListViewModel : ObservableObject
{
    private readonly IProductService _productService;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(HasProducts))]
    private ObservableCollection<Product> _products = new();

    [ObservableProperty]
    private bool _isLoading;

    [ObservableProperty]
    private string _searchQuery = string.Empty;

    public bool HasProducts => Products.Count > 0;

    public ProductListViewModel(IProductService productService)
    {
        _productService = productService;
    }

    [RelayCommand]
    private async Task LoadProductsAsync()
    {
        if (IsLoading) return;

        try
        {
            IsLoading = true;
            var products = await _productService.GetProductsAsync(SearchQuery);
            Products.Clear();
            foreach (var product in products)
            {
                Products.Add(product);
            }
        }
        catch (Exception ex)
        {
            await Shell.Current.DisplayAlert("Error", ex.Message, "OK");
        }
        finally
        {
            IsLoading = false;
        }
    }

    [RelayCommand]
    private async Task SearchAsync()
    {
        await LoadProductsAsync();
    }

    [RelayCommand]
    private async Task NavigateToDetailAsync(Product product)
    {
        await Shell.Current.GoToAsync($"{nameof(ProductDetailPage)}",
            new Dictionary<string, object>
            {
                { "Product", product }
            });
    }
}

XAML Views

Create responsive layouts:

<!-- Pages/ProductListPage.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:MyMauiApp.ViewModels"
             xmlns:models="clr-namespace:MyMauiApp.Models"
             x:Class="MyMauiApp.Pages.ProductListPage"
             x:DataType="viewmodels:ProductListViewModel"
             Title="Products">

    <ContentPage.Resources>
        <Style x:Key="ProductCard" TargetType="Frame">
            <Setter Property="CornerRadius" Value="10" />
            <Setter Property="Padding" Value="10" />
            <Setter Property="Margin" Value="5" />
            <Setter Property="HasShadow" Value="True" />
        </Style>
    </ContentPage.Resources>

    <Grid RowDefinitions="Auto,*">
        <!-- Search Bar -->
        <SearchBar Grid.Row="0"
                   Placeholder="Search products..."
                   Text="{Binding SearchQuery}"
                   SearchCommand="{Binding SearchCommand}" />

        <!-- Products List -->
        <RefreshView Grid.Row="1"
                     IsRefreshing="{Binding IsLoading}"
                     Command="{Binding LoadProductsCommand}">

            <CollectionView ItemsSource="{Binding Products}"
                            SelectionMode="Single"
                            SelectionChangedCommand="{Binding NavigateToDetailCommand}"
                            SelectionChangedCommandParameter="{Binding SelectedItem, Source={RelativeSource Self}}">

                <CollectionView.ItemsLayout>
                    <GridItemsLayout Orientation="Vertical"
                                     Span="{OnIdiom Phone=2, Tablet=3, Desktop=4}"
                                     VerticalItemSpacing="10"
                                     HorizontalItemSpacing="10" />
                </CollectionView.ItemsLayout>

                <CollectionView.ItemTemplate>
                    <DataTemplate x:DataType="models:Product">
                        <Frame Style="{StaticResource ProductCard}">
                            <VerticalStackLayout Spacing="5">
                                <Image Source="{Binding ImageUrl}"
                                       HeightRequest="150"
                                       Aspect="AspectFill" />
                                <Label Text="{Binding Name}"
                                       FontAttributes="Bold"
                                       FontSize="16"
                                       LineBreakMode="TailTruncation" />
                                <Label Text="{Binding Price, StringFormat='${0:F2}'}"
                                       TextColor="{StaticResource Primary}"
                                       FontSize="14" />
                                <Label Text="{Binding Category}"
                                       FontSize="12"
                                       TextColor="Gray" />
                            </VerticalStackLayout>
                        </Frame>
                    </DataTemplate>
                </CollectionView.ItemTemplate>

                <CollectionView.EmptyView>
                    <VerticalStackLayout VerticalOptions="Center"
                                         HorizontalOptions="Center">
                        <Label Text="No products found"
                               FontSize="18"
                               HorizontalTextAlignment="Center" />
                        <Button Text="Refresh"
                                Command="{Binding LoadProductsCommand}" />
                    </VerticalStackLayout>
                </CollectionView.EmptyView>
            </CollectionView>
        </RefreshView>

        <!-- Loading Indicator -->
        <ActivityIndicator Grid.RowSpan="2"
                           IsRunning="{Binding IsLoading}"
                           IsVisible="{Binding IsLoading}"
                           VerticalOptions="Center"
                           HorizontalOptions="Center" />
    </Grid>
</ContentPage>

Platform-Specific Code

Handle platform differences:

// Services/DeviceService.cs
namespace MyMauiApp.Services;

public partial class DeviceService
{
    public partial string GetDeviceId();
    public partial void ShowNativeNotification(string title, string message);
}

// Platforms/Android/DeviceService.cs
namespace MyMauiApp.Services;

public partial class DeviceService
{
    public partial string GetDeviceId()
    {
        return Android.Provider.Settings.Secure.GetString(
            Platform.CurrentActivity?.ContentResolver,
            Android.Provider.Settings.Secure.AndroidId) ?? "unknown";
    }

    public partial void ShowNativeNotification(string title, string message)
    {
        var context = Platform.CurrentActivity;
        if (context == null) return;

        var builder = new AndroidX.Core.App.NotificationCompat.Builder(context, "default")
            .SetContentTitle(title)
            .SetContentText(message)
            .SetSmallIcon(Resource.Drawable.notification_icon)
            .SetAutoCancel(true);

        var notificationManager = AndroidX.Core.App.NotificationManagerCompat.From(context);
        notificationManager.Notify(1, builder.Build());
    }
}

// Platforms/iOS/DeviceService.cs
namespace MyMauiApp.Services;

public partial class DeviceService
{
    public partial string GetDeviceId()
    {
        return UIKit.UIDevice.CurrentDevice.IdentifierForVendor?.ToString() ?? "unknown";
    }

    public partial void ShowNativeNotification(string title, string message)
    {
        var content = new UserNotifications.UNMutableNotificationContent
        {
            Title = title,
            Body = message,
            Sound = UserNotifications.UNNotificationSound.Default
        };

        var trigger = UserNotifications.UNTimeIntervalNotificationTrigger
            .CreateTrigger(1, false);

        var request = UserNotifications.UNNotificationRequest
            .FromIdentifier(Guid.NewGuid().ToString(), content, trigger);

        UserNotifications.UNUserNotificationCenter.Current
            .AddNotificationRequest(request, null);
    }
}

Shell Navigation

Configure app navigation:

// AppShell.xaml.cs
namespace MyMauiApp;

public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();

        // Register routes
        Routing.RegisterRoute(nameof(ProductDetailPage), typeof(ProductDetailPage));
        Routing.RegisterRoute(nameof(CartPage), typeof(CartPage));
        Routing.RegisterRoute(nameof(CheckoutPage), typeof(CheckoutPage));
    }
}
<!-- AppShell.xaml -->
<?xml version="1.0" encoding="UTF-8" ?>
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:pages="clr-namespace:MyMauiApp.Pages"
       x:Class="MyMauiApp.AppShell"
       FlyoutBehavior="Flyout">

    <FlyoutItem Title="Products" Icon="products.png">
        <ShellContent Route="ProductList"
                      ContentTemplate="{DataTemplate pages:ProductListPage}" />
    </FlyoutItem>

    <FlyoutItem Title="Cart" Icon="cart.png">
        <ShellContent Route="Cart"
                      ContentTemplate="{DataTemplate pages:CartPage}" />
    </FlyoutItem>

    <FlyoutItem Title="Profile" Icon="profile.png">
        <ShellContent Route="Profile"
                      ContentTemplate="{DataTemplate pages:ProfilePage}" />
    </FlyoutItem>

    <MenuItem Text="Logout"
              IconImageSource="logout.png"
              Clicked="OnLogoutClicked" />
</Shell>

Summary

.NET MAUI provides:

  • Single project for all platforms
  • Native performance and UI
  • MVVM support with CommunityToolkit
  • Hot reload for rapid development
  • Platform-specific customization

MAUI is the evolution of Xamarin.Forms, making cross-platform development more accessible.


References:

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.