Introduction to .NET MAUI: Build Cross-Platform Apps with C#
Learn .NET MAUI basics: create cross-platform mobile and desktop apps using C# and .NET.
20+ years shipping production .NET services in enterprise systems. Written from production experience, not tutorials.
- ✓Basic knowledge of C# and .NET
- ✓Familiarity with object-oriented programming
- ✓Visual Studio 2022 or VS Code with MAUI extension
- ✓.NET 6 SDK or later
- .NET MAUI is a cross-platform framework for building native mobile and desktop apps with C# and XAML.
- It replaces Xamarin.Forms and supports Android, iOS, macOS, Windows, and Tizen.
- Use MVVM pattern for clean separation of concerns.
- Data binding connects UI to data models automatically.
- Hot Reload allows real-time UI changes during development.
Think of .NET MAUI as a universal remote that can control your TV, sound system, and streaming device with one set of buttons. Instead of writing separate code for Android, iOS, and Windows, you write one C# codebase and .NET MAUI translates it to each platform's native language.
Imagine you need to build a mobile app for your startup that tracks inventory across warehouses. Your team uses Android phones, but the warehouse managers prefer iPads. Later, the CEO wants a desktop dashboard for Windows. Traditionally, you'd need three separate codebases—multiplying development time and bugs. .NET MAUI solves this by letting you write one C# codebase that targets all platforms.
.NET MAUI (Multi-platform App UI) is the evolution of Xamarin.Forms, now part of .NET 6+. It provides a single project structure, a shared UI layer using XAML, and access to native platform APIs. You can build apps for Android, iOS, macOS, Windows, and even Tizen (Samsung smart TVs) from a single codebase.
In this tutorial, you'll learn the fundamentals: setting up a .NET MAUI project, understanding the app structure, using XAML for UI, implementing the MVVM pattern, and handling data binding. We'll also cover real-world debugging scenarios and common pitfalls. By the end, you'll be able to create a simple but functional cross-platform app.
Setting Up Your .NET MAUI Project
To start, ensure you have .NET 6 SDK or later installed. Open a terminal and run:
``bash dotnet new maui -n MyFirstMauiApp cd MyFirstMauiApp dotnet build ``
This creates a new .NET MAUI project with a default structure. The project contains: - MauiProgram.cs: Entry point where you configure services and the app. - App.xaml / App.xaml.cs: Application-level resources and lifecycle. - AppShell.xaml / AppShell.xaml.cs: Defines navigation structure. - MainPage.xaml / MainPage.xaml.cs: The default page. - Platforms/ folder: Platform-specific code for Android, iOS, macOS, Windows, etc.
Let's examine MauiProgram.cs:
```csharp using Microsoft.Extensions.Logging;
namespace MyFirstMauiApp;
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"); });
#if DEBUG builder.Logging.AddDebug(); #endif
return builder.Build(); } } ```
This is similar to ASP.NET Core's startup. You can register services, configure fonts, and add logging. The UseMauiApp<App>() method sets the application class.
Key Takeaway: The project template provides a solid foundation. Customize MauiProgram.cs to add dependency injection and logging.
Understanding XAML and Code-Behind
XAML (eXtensible Application Markup Language) is used to define the UI declaratively. Each XAML file has a corresponding code-behind file (e.g., MainPage.xaml.cs) that contains event handlers and logic.
Open MainPage.xaml:
```xml <?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" x:Class="MyFirstMauiApp.MainPage">
<ScrollView> <VerticalStackLayout Padding="30,0" Spacing="25"> <Label Text="Hello, World!" Style="{StaticResource Headline}" SemanticProperties.HeadingLevel="Level1" /> <Button x:Name="CounterBtn" Text="Click me" SemanticProperties.Hint="Counts the number of times you click" Clicked="OnCounterClicked" /> </VerticalStackLayout> </ScrollView>
</ContentPage> ```
And MainPage.xaml.cs:
```csharp namespace MyFirstMauiApp;
public partial class MainPage : ContentPage { int count = 0;
public MainPage() { InitializeComponent(); }
private void OnCounterClicked(object sender, EventArgs e) { count++; CounterBtn.Text = $"Clicked {count} times"; } } ```
The x:Class attribute links the XAML to the code-behind class. The InitializeComponent() method is generated and loads the XAML. Event handlers like Clicked are wired up in XAML.
Key Takeaway: XAML separates UI design from logic. Use code-behind for simple event handling; for complex apps, use MVVM.
The MVVM Pattern in .NET MAUI
MVVM (Model-View-ViewModel) is the recommended pattern for .NET MAUI apps. It separates concerns: - Model: Data and business logic. - View: XAML UI. - ViewModel: Mediator that exposes data and commands to the View.
To implement MVVM, you need: 1. A ViewModel class that implements INotifyPropertyChanged. 2. Data binding in XAML to bind UI elements to ViewModel properties. 3. Commands to handle user actions.
Let's create a simple ViewModel:
```csharp using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows.Input;
namespace MyFirstMauiApp.ViewModels;
public class MainViewModel : INotifyPropertyChanged { private int _count; public int Count { get => _count; set { if (_count != value) { _count = value; OnPropertyChanged(); OnPropertyChanged(nameof(CountDisplay)); } } }
public string CountDisplay => $"Clicked {Count} times";
public ICommand IncrementCommand { get; }
public MainViewModel() { IncrementCommand = new Command(Increment); }
private void Increment() { Count++; }
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } ```
Now update MainPage.xaml to bind to this ViewModel:
```xml <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:vm="clr-namespace:MyFirstMauiApp.ViewModels" x:Class="MyFirstMauiApp.MainPage">
<ContentPage.BindingContext> <vm:MainViewModel /> </ContentPage.BindingContext>
<VerticalStackLayout Padding="30" Spacing="25"> <Label Text="{Binding CountDisplay}" /> <Button Text="Click me" Command="{Binding IncrementCommand}" /> </VerticalStackLayout>
</ContentPage> ```
BindingContextis set to an instance ofMainViewModel.Labelbinds toCountDisplayproperty.Buttonbinds toIncrementCommand.
Key Takeaway: MVVM promotes testability and maintainability. Use data binding to connect View and ViewModel.
Data Binding and Commands in Depth
Data binding is the backbone of MVVM. It synchronizes the UI with the underlying data. There are several binding modes: - OneWay: Target updates when source changes. - TwoWay: Both source and target update each other. - OneTime: Target is set once and never updated. - OneWayToSource: Source updates when target changes.
Commands replace event handlers. The ICommand interface has Execute and CanExecute methods. You can bind a Button's Command property to a command in the ViewModel.
Example with TwoWay binding for an Entry (text input):
``xml <Entry Text="{Binding UserName, Mode=TwoWay}" /> ``
In ViewModel:
``csharp private string _userName; public string UserName { get => _userName; set { if (_userName != value) { _userName = value; ``OnPropertyChanged(); } } }
Commands can also accept parameters. Use Command<T>:
```csharp public ICommand SubmitCommand { get; }
public MainViewModel() { SubmitCommand = new Command<string>(Submit); }
private void Submit(string parameter) { // Use parameter } ```
In XAML:
``xml <Button Text="Submit" Command="{Binding SubmitCommand}" CommandParameter="Hello" /> ``
Key Takeaway: Choose the right binding mode. Use TwoWay for input controls. Commands with parameters allow passing data from View to ViewModel.
Navigation in .NET MAUI
Navigation allows moving between pages. .NET MAUI supports several navigation patterns: - Shell navigation: Uses URI-based routing. - NavigationPage: Stack-based navigation. - Modal navigation: For popups.
Shell is the recommended approach. Define routes in AppShell.xaml:
```xml <Shell x:Class="MyFirstMauiApp.AppShell" xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:pages="clr-namespace:MyFirstMauiApp.Pages">
<TabBar> <ShellContent Title="Home" ContentTemplate="{DataTemplate pages:MainPage}" /> <ShellContent Title="Settings" ContentTemplate="{DataTemplate pages:SettingsPage}" /> </TabBar>
</Shell> ```
To navigate programmatically:
``csharp await Shell.Current.GoToAsync("//Settings"); ``
You can also pass data:
``csharp await Shell.Current.GoToAsync("//Details?id=123"); ``
And receive in the target page by implementing IQueryAttributable:
``csharp public class DetailsPage : ContentPage, IQueryAttributable { public void ApplyQueryAttributes(IDictionary<string, object> query) { if (query.TryGetValue("id", out var id)) { // Use id } } } ``
Key Takeaway: Shell navigation provides a consistent routing system. Use GoToAsync for navigation and IQueryAttributable for receiving parameters.
Accessing Platform-Specific Features
One of MAUI's strengths is accessing native APIs. Use the Microsoft.Maui.Essentials namespace for common features like geolocation, camera, and sensors.
Example: Get current location:
```csharp using Microsoft.Maui.Devices.Sensors;
async Task GetLocationAsync() { try { var location = await Geolocation.GetLastKnownLocationAsync(); if (location == null) { location = await Geolocation.GetLocationAsync(new GeolocationRequest { DesiredAccuracy = GeolocationAccuracy.Medium, Timeout = TimeSpan.FromSeconds(10) }); } // Use location } catch (FeatureNotSupportedException) { // Not supported on device } catch (PermissionException) { // Permissions not granted } } ```
For platform-specific code, use conditional compilation or partial classes:
``csharp #if ANDROID // Android-specific code #elif IOS // iOS-specific code #endif ``
Or use the Platform class:
``csharp #if ANDROID using Android.Content; #endif ``
Key Takeaway: MAUI Essentials provides a unified API for common features. Use conditional compilation for platform-specific logic.
Dependency Injection in .NET MAUI
Dependency injection (DI) is built into .NET MAUI. Register services in MauiProgram.cs:
``csharp builder.Services.AddSingleton<IDataService, DataService>(); builder.Services.AddTransient<MainViewModel>(); builder.Services.AddTransient<MainPage>(); ``
Then inject into constructors:
``csharp public partial class MainPage : ContentPage { public MainPage(MainViewModel viewModel) { ``InitializeComponent(); BindingContext = viewModel; } }
For ViewModels, inject services:
```csharp public class MainViewModel { private readonly IDataService _dataService;
public MainViewModel(IDataService dataService) { _dataService = dataService; } } ```
Key Takeaway: DI promotes loose coupling. Register services with appropriate lifetimes (Singleton, Transient, Scoped).
The Case of the Missing Data: When BindingContext Was Null
- Always set BindingContext after the page is loaded, e.g., in OnAppearing or via a ViewModel locator.
- Test on all target platforms early in development.
- Use conditional breakpoints to inspect BindingContext values per platform.
- Consider using a DI container to inject ViewModels and avoid manual assignment.
- Log BindingContext state in OnAppearing to catch null references.
Debug.WriteLine(BindingContext?.GetType())Check Output window for binding errors| File | Command / Code | Purpose |
|---|---|---|
| MauiProgram.cs | using Microsoft.Extensions.Logging; | Setting Up Your .NET MAUI Project |
| MainPage.xaml | | Understanding XAML and Code-Behind | |
| MainViewModel.cs | using System.ComponentModel; | The MVVM Pattern in .NET MAUI |
| BindingExample.xaml | Data Binding and Commands in Depth | |
| AppShell.xaml | | Navigation in .NET MAUI | |
| LocationService.cs | using Microsoft.Maui.Devices.Sensors; | Accessing Platform-Specific Features |
| MauiProgram.cs with DI | using Microsoft.Extensions.Logging; | Dependency Injection in .NET MAUI |
Key takeaways
Common mistakes to avoid
5 patternsSetting BindingContext in constructor before InitializeComponent
Forgetting to call OnPropertyChanged in property setter
Using code-behind for complex logic instead of MVVM
Not handling permissions for platform features
Ignoring binding errors in Output window
Interview Questions on This Topic
What is the MVVM pattern and why is it used in .NET MAUI?
Frequently Asked Questions
20+ years shipping production .NET services in enterprise systems. Written from production experience, not tutorials.
That's ASP.NET. Mark it forged?
4 min read · try the examples if you haven't