After K2, the Kladde runs in the browser. This course gives it a second interface: an app for phone and desktop, written in C# and XAML from a single code base. The tool for that is called .NET MAUI. This lesson clarifies what it is and what it isn’t, sets up your computer, and starts the template on an Android emulator and on Windows. At the end, src/Kladde.App sits next to the projects from K2, and you’ve seen with Hot Reload how quickly you can change an interface in this type of project.

The course assumes K2: C# and async, the Kladde’s web API with its sign-in, the booking view as a Blazor component, and xUnit. This course doesn’t repeat any of that. Where it builds on it, you’ll find a reference.

Module Lesson What you build
W2.1 W2.1.1 .NET MAUI at a glance the project Kladde.App, startable on Android and Windows
W2.1 W2.1.2 XAML, controls and layouts contact list and today’s appointments as pages with Shell
W2.2 W2.2.1 MVVM the pages as view models, tested without a device
W2.2 W2.2.2 Data: API, offline, stored securely appointments from the web API, even without a network
W2.3 W2.3.1 Blazor Hybrid the booking view as a shared component
W2.3 W2.3.2 Testing, shipping, distributing an Android package and an MSIX package

What .NET MAUI is

.NET Multi-platform App UI, or .NET MAUI for short, is a framework for native mobile and desktop apps with C# and XAML. From one shared code base you get apps for Android, iOS, macOS and Windows. It’s open source and the successor to Xamarin.Forms, extended from mobile to the desktop, with controls that were built from scratch.

What’s important to understand is what MAUI doesn’t do: it doesn’t draw its own interface that looks the same everywhere. Your code talks to the layer of MAUI controls, and that layer uses the controls of the respective platform. When necessary, your code can also call the platform directly. Underneath is the .NET class library, which is the same for all platforms; that’s why business logic, tests and models can be shared.

Target platform How it’s built Runtime and translation
Android .NET for Android C# is compiled to IL and translated to native code when the app starts (JIT)
iOS .NET for iOS compiled completely ahead of time (AOT) to native ARM code
macOS Apple’s Mac Catalyst: the iOS app, built with UIKit, runs on the desktop, supplemented by AppKit and platform calls Mono
Windows Windows UI 3 (WinUI 3) the .NET runtime (CLR)

For Android, iOS and macOS the runtime is Mono, according to Learn; for Windows it’s the .NET Core CLR. For this course, it’s enough that you practice on Android and Windows and see the same interface on both.

What MAUI brings

  • controls to display data, trigger actions, present collections and choose values,
  • a layout engine and several page types, including Shell for navigation,
  • data binding, which you need for MVVM in W2.2.1,
  • cross-platform interfaces for device features: sensors, network status, device information, clipboard, file picker, securely stored key-value pairs, text to speech, and sign-in flows in the browser that return to an app address,
  • a drawing surface for your own graphics,
  • one project for all platforms, and Hot Reload.

You use the interfaces for network status and secure storage in W2.2.2.

Who it’s for and what for

Learn gives three reasons for MAUI: you write cross-platform apps in XAML and C# from one code base, you share layout and design, and you share code, tests and business logic. For the Kladde, that means: the rules you already have from K1 and K2 stay C#. What’s new is the interface.

What MAUI isn’t in this course

  • Not a school for iOS. iOS needs a Mac on the network; the course describes the path, but the exercises run on Android and Windows.
  • Not game development.
  • Not an introduction to C# or Entra ID. You bring both from K0 through K2.
  • Not a second website. If you want to use web technology in the app, W2.3.1 covers Blazor Hybrid.

Platform limits: what MAUI needs in .NET 10

For .NET 10, Microsoft Learn lists the following minimum requirements for the targets:

Platform At least
Android Android 5.0 (API 21)
iOS iOS 12.2
macOS macOS 12 through Mac Catalyst
Windows Windows 11, or Windows 10 version 1809 or later, through WinUI 3

Apps with Blazor in MAUI (W2.3) have stricter limits: Android 7.0 (API 24), iOS 16.4 and macOS 12, plus a current WebView of the platform. Samsung also provides support for Tizen; it isn’t part of this course.

There are two things to take away right now. First: the .NET 10 templates set the value SupportedOSPlatformVersion for Android to 24 instead of 21. API 21 is still supported, but Learn recommends raising existing projects to 24 so that crashes from missing Java features don’t happen. Second: for .NET 11, Learn names API 24 as the minimum for Android. The course stays with .NET 10; the number is only a preview.

Which versions of Xcode, Android SDK, JDK and Windows App SDK fit a MAUI version changes with every release. For that, Learn points to the “Release versions” table in the GitHub wiki of dotnet/maui. This course therefore gives no numbers. Look it up there before you set up.

Tools

You have two paths, and both are fine:

Visual Studio Visual Studio Code
Requirement Visual Studio 2022 17.12 or later with the “.NET Multi-platform App UI development” workload Visual Studio Code with the “.NET MAUI” extension
Further extensions none C# Dev Kit and C#, which the MAUI extension installs itself
Account C# Dev Kit requires signing in with a Microsoft account that has an active Visual Studio subscription; the license questions are in the C# Dev Kit FAQ

Your operating system determines what you can build (Visual Studio reaches iOS from Windows through a Mac on the network):

Your operating system Targets
Windows Windows, Android
macOS Android, iOS, macOS
Linux Android

You build, sign and install apps for iOS and Mac Catalyst only on a Mac with a matching Xcode version. With an Apple ID and membership in the Apple Developer Program, you can deploy to devices and put apps in the store. For debug builds, there’s “Hot Restart” from Windows to an iOS device; that, too, requires the paid membership. For Mac Catalyst, Learn names only Visual Studio Code as a tool for starting and debugging. Those are the reasons why this course describes iOS but doesn’t practice it.

The workload

In .NET 10, the framework comes as a .NET workload and as several NuGet packages. The advantage: you can tie a project to a specific version and easily try out preview versions. Without Visual Studio, you install it like this:

PowerShell
dotnet workload install maui

On Windows and macOS, the workload maui then appears in the list:

PowerShell
dotnet workload list

If you set it up with the Visual Studio installer, the list shows android, maui-windows, maccatalyst and ios instead. On macOS, the first command needs sudo; on Linux the workload is called maui-android.

Setting up Android

For Android you need the Java SDK, the Android SDK and an emulator. With Visual Studio, the first start of an Android app takes care of that: it downloads the default SDK, creates a virtual Android device entry (AVD) and asks for the licenses several times. Accept them only if you’ve read them.

With Visual Studio Code there are several ways. The recommended one uses an MSBuild target that installs the dependencies itself once you have a project:

PowerShell
dotnet new maui -n "MyApp"
cd MyApp
dotnet build -t:InstallAndroidDependencies -f:net10.0-android -p:AndroidSdkDirectory="C:\Android\Sdk" -p:AcceptAndroidSDKLicenses=True

Learn shows this example with net9.0-android. For .NET 10, the target framework is called net10.0-android. The path to the SDK should contain neither spaces nor non-ASCII characters. With -p:AcceptAndroidSDKLicenses=True you declare that you’ve accepted the licenses. The path C:\Android\Sdk is just an example here.

The Visual Studio Code extension checks the environment with the command .NET MAUI: Configure Android and the choice Refresh Android environment. It looks for JAVA_HOME and ANDROID_HOME; Learn recommends setting both environment variables. Which version of the JDK fits isn’t stated consistently in Learn: the installation page names the Microsoft Build of OpenJDK 17, while the page on what’s new in .NET 10 says that .NET for Android now also builds with JDK 21. The “Release versions” table is authoritative.

An emulator is only fast if hardware acceleration is turned on. Learn explicitly warns that it runs very slowly otherwise. On Windows on ARM (for example, Surface Pro X), the Android emulator doesn’t run because it needs x64; there you use a real device over USB. The emulator has no virtual hardware for Bluetooth, NFC, SD card, headphones and USB.

Windows as a target

Windows as a target needs no emulator: the app runs as a window on the computer you develop on. According to Learn, a new MAUI project is deployed as an unpackaged app by default. For packaged apps (MSIX), Windows Developer Mode must be turned on: Settings, Privacy & security, “For developers” (Windows 11), then “Developer Mode”. Turning it on requires administrator rights and can be blocked on a company’s devices. If Visual Studio asks for Developer Mode while building, follow the link to the settings for developers. The names of the settings pages change between Windows versions.

Creating the template

On the command line, dotnet new maui creates a project with a XAML interface. That’s the shortest way, and it looks the same in both tools:

PowerShell
dotnet new maui -n Kladde.App -o src\Kladde.App

With Visual Studio, you choose Create a new project, filter for MAUI, take the template .NET MAUI App, enter name and location, and choose the .NET version. With Visual Studio Code, it’s Create .NET Project (or .NET: New Project… in the command palette) and the template .NET MAUI App; the project must be created in an empty folder.

Don’t let the name confuse you: Kladde.App is the project name and also the default namespace of the classes.

What the template creates

Learn names four important code files:

File Job
MauiProgram.cs the shared entry point: builds and starts the app
App.xaml and App.xaml.cs resources for the whole app such as colors and styles; the code creates the window
AppShell.xaml and AppShell.xaml.cs the structure of the app: tabs, menu and pages
MainPage.xaml and MainPage.xaml.cs the template’s start page with the button “Click me”

In addition there are the folders Platforms and Resources. Start with the entry class:

C#
namespace MyMauiApp;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>();

        return builder.Build();
    }
}

That’s the smallest possible version. The template adds more lines to the builder, for example for fonts. Every platform calls CreateMauiApp and gets a MauiApp back. So there’s exactly one entry point, even though four platforms start. Registrations for dependency injection come later, in W2.2.1.

The class App derives from Application and creates the window:

C#
namespace MyMauiApp;

public class App : Application
{
    public App()
    {
        InitializeComponent();
    }

    protected override Window CreateWindow(IActivationState? activationState)
    {
        return new Window(new AppShell());
    }
}

AppShell is a class derived from Shell that describes the visible structure of the app. The template’s page, MainPage, derives from ContentPage. At startup, this chain runs: MauiProgram builds the app, the constructor of App is created, CreateWindow creates AppShell, and AppShell shows MainPage. Its constructor calls InitializeComponent, which creates the objects from the XAML file, connects them into a tree and sets it as the content of the page.

Before .NET 9, App had an assignment to MainPage instead. If you find older examples that write MainPage = new AppShell();, that is the old form. With .NET 10, CreateWindow applies.

One project, several platforms

MAUI works with one project: an SDK project that targets several platforms (multi-targeting). Learn sums it up like this: one shared project for all platforms, a simplified choice of the target to start, shared resource files, a single manifest with the app’s title, ID and version, access to platform-specific interfaces, and a shared entry point.

The Platforms folder has a subfolder for each platform with the platform-specific resources and the code that starts the app there. When building for Android, only the files from Platforms\Android go into the package, not those of the other platforms. Where you need platform-specific code, you use partial classes or conditional compilation:

C#
#if ANDROID
                  handler.PlatformView.SetBackgroundColor(Colors.Red.ToPlatform());
#elif IOS
                  handler.PlatformView.BackgroundColor = Colors.Red.ToPlatform();
                  handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.Line;
#elif WINDOWS
                  handler.PlatformView.Background = Colors.Red.ToPlatform();
#endif

This example from Learn sets the background color of a control: each platform has its own call for it. In this course you don’t need code like this; it only shows that the way to the platform stays open.

The Resources folder holds the shared resources. Every image file serves as a source from which the platforms’ resolutions are generated at build time. Which folders apply is set by the project file; the action follows from the folder:

Resource Action in the project file Folder in the template
App icon MauiIcon Resources\AppIcon
Images MauiImage Resources\Images
Fonts MauiFont Resources\Fonts
Splash screen MauiSplashScreen Resources\Splash
Raw files MauiAsset Resources\Raw
CSS files MauiCss any
XML
<ItemGroup>
    <!-- Images -->
    <MauiImage Include="Resources\Images\*" />

    <!-- Fonts -->
    <MauiFont Include="Resources\Fonts\*" />

    <!-- Raw assets -->
    <MauiAsset Include="Resources\Raw\*" />
</ItemGroup>

XAML files automatically get the action MauiXaml. A platform’s own resource, such as an image in Platforms\Android\Resources\drawable-xhdpi, wins over the shared one. The app’s title, ID and version are in one place and are merged with each platform’s manifest at build time; in Visual Studio you find them in the project’s properties under MAUI Shared.

What’s new in .NET 10

The page “What’s new in .NET MAUI for .NET 10” is long. For your work, the following counts. The list also shows what you shouldn’t rebuild from old tutorials:

Area Status in .NET 10
Delivery .NET workload plus NuGet packages; projects can be tied to versions
Android API 36 is the default; the templates set the minimum version 24; you can build with JDK 21
Starting dotnet run now works for Android, with -p:AdbTarget=-e for the running emulator or -d for a connected device
Marshal methods turned on in .NET 10; problems often show up as a hang at startup, then set AndroidEnableMarshalMethods to false
CoreCLR on Android experimental, not meant for production
XAML a source generator (MauiXamlInflator), more in W2.1.2; global and implicit namespaces as a preview feature
Aspire a template for a service defaults project for MAUI (OpenTelemetry, service discovery)
deprecated ListView and its cells, TableView: use CollectionView instead; DisplayAlert and DisplayActionSheet: use DisplayAlertAsync and DisplayActionSheetAsync instead; FadeTo, ScaleTo and the other animations: use the variants with Async instead; Page.IsBusy: use ActivityIndicator instead
removed or internal ClickGestureRecognizer removed (TapGestureRecognizer); MessagingCenter internal (replacement: WeakReferenceMessenger from CommunityToolkit.Mvvm)

The row on Android API 36 had a trap in the preview versions: Visual Studio and Visual Studio Code didn’t know the version yet and reported the error XA5207. Whether that still matters with your tool version, you’ll see from the error text.

That also lets you start Android apps on the command line when an emulator is running:

PowerShell
dotnet run -f net10.0-android -p:AdbTarget=-e

Learn shows dotnet run -p:AdbTarget=-e without -f. A MAUI project has several target platforms; -f is the option of dotnet run that selects one.

Starting: emulator and Windows

Visual Studio. In the toolbar, in the Debug Target menu, choose the Android emulators and an entry, or Windows Machine. On the first Android start, Visual Studio sets up the SDK and emulator (see above). The template has the button Click me; every press increases a counter.

Visual Studio Code. First check the environment (.NET MAUI: Configure Android, then Refresh Android environment). In the status bar at the bottom, clicking the curly braces { } sets the Debug Target; the commands .NET MAUI: Pick Android Device and .NET MAUI: Pick Windows Device do the same. You start with F5 or the Run button. If Visual Studio Code asks for a debugger, choose C# and your project’s launch configuration.

iOS and Mac, described. With Visual Studio on Windows, you pair a Mac as a build host (Pair to Mac). Then you choose an iOS simulator as the target; Visual Studio builds, starts the “Remote iOS Simulator for Windows” and installs the app there. On the Mac itself you work with Visual Studio Code, choose .NET MAUI: Pick iOS Device and start with F5. For a real iOS device you need the Apple ID in Xcode, Developer Mode turned on on the device and, in the Developer Program, the device’s registration in the team.

Common errors

What you observe What to check
The emulator is very slow is hardware acceleration turned on (Hyper-V and AEHD)?
There is no emulator on Windows on ARM the emulator needs x64; use a device over USB
dotnet workload list shows no maui was Visual Studio set up with the workload (then android, maui-windows, maccatalyst, ios appear) or was dotnet workload install maui run?
Android licenses aren’t accepted in a terminal with administrator rights, in the folder cmdline-tools/<version>/bin of the Android SDK, run sdkmanager --licenses
App hangs at startup on .NET 10 try AndroidEnableMarshalMethods set to false; if that helps, report it as an issue in the dotnet/android repository
Windows asks for Developer Mode see the section on Windows; according to Learn it isn’t needed for unpackaged apps

For everything not covered here, the page “Troubleshooting known issues” in the MAUI documentation takes you further.

Hot Reload

Hot Reload is the reason why working on an app can be pleasant. There are two kinds:

  • .NET Hot Reload applies changes to C# code while the app is running, without rebuilding and without you having to stop or hit a breakpoint.
  • XAML Hot Reload applies saved or typed changes to XAML. The navigation state and the data are kept because the page isn’t rebuilt; only the changed properties are set.

Requirements and limits according to Learn:

Point Statement
Configuration the app runs in the Debug configuration with the debugger attached; the configuration must be named exactly that
Tool Visual Studio 2022 and Visual Studio Code
Targets XAML Hot Reload works on Windows for Android, iOS and WinUI, on emulators, simulators and real devices
Saving in Visual Studio, by default every change applies as you type; the option “Apply XAML Hot Reload on document save” switches to saving
C# in XAML XAML Hot Reload does not reload C# code: a new event handler in the code works only after the code is reloaded in the XAML; a new name with x:Name likewise
Files don’t add, remove or rename files or NuGet packages: then rebuild and redeploy
Errors an error in the XAML file, error code XHR…, blocks all changes until it’s fixed
Reloading the Hot Reload button or Alt+F10 reloads the changed page completely; focus and selection may be lost
Resources if you change a resource dictionary, all XAML files are reloaded

Learn also names a source of errors when Hot Reload does nothing without a message: all .cs files must be saved as UTF-8 with BOM. Files without a BOM, as many editors on Linux or macOS create them, can cause XAML Hot Reload to fail silently. In Visual Studio Code, you set this in the status bar with Save with Encoding. The output of the Hot Reload window (in Visual Studio under View, Output, Hot Reload) shows status messages.

Exercise 1

Start the template. Start the template on the Android emulator and on Windows.

  1. In an empty folder outside the Kladde, create a project that you’ll delete again after the exercise: dotnet new maui -n MauiScratch.
  2. Open it in Visual Studio or Visual Studio Code.
  3. Choose an Android emulator as the target. The first time, your tool sets up the SDK and emulator.
  4. Start the app and press Click me four times.
  5. Stop the app, choose Windows as the target (Windows Machine or Pick Windows Device) and start the app again.

How to check: the same page appears on both targets, and the counter is at four after four presses. In the emulator you also find the app in the list of installed apps. If the emulator runs noticeably sluggishly, check whether hardware acceleration is turned on.

Solution 1

All steps are described in the sections above. A command-line way for Android, if the emulator is already running:

PowerShell
cd MauiScratch
dotnet run -f net10.0-android -p:AdbTarget=-e

For Windows you choose the target in your tool; the course starts the Windows app from Visual Studio or Visual Studio Code. If the first Android start aborts with an error message about licenses, sdkmanager --licenses helps (see the table of errors).

Cleaning up: shut down the emulator when you no longer need it; it takes up memory and disk space until you delete the virtual device (Android Device Manager). There are no costs.

Exercise 2

Try out Hot Reload. Start the template from exercise 1 with the debugger (F5 or the green button, not “without debugging”).

  1. Open MainPage.xaml, change the text of a heading and save. The running app shows the new text, and the counter keeps its value.
  2. In the same file, change the background color or the margin of a row.
  3. Open MainPage.xaml.cs, find the method that increases the counter when the button is pressed, and change the text it sets. Apply the change (in Visual Studio, the Hot Reload button).
  4. In the XAML file, add a new control with x:Name and access it from the code. Watch what happens.

How to check: steps 1 and 2 show the effect without a restart, with the counter unchanged. In step 4, the new name only works after a rebuild, because XAML Hot Reload doesn’t reload the C# code.

Solution 2

Step 3 changes C# code, not XAML; it depends on .NET Hot Reload. Which code changes can be applied without rebuilding the app depends on the kind of change; on the .NET Hot Reload page, Learn describes what doesn’t work. If nothing shows up, check the points in the table: Debug configuration, debugger attached, no XHR errors, .cs files with BOM. Step 4: after a rebuild and a restart of the app, the name applies. Remember the rule for practice: XAML without x:Name and without new events can be changed while the app is running; everything else needs a rebuild.

Exercise 3

Create Kladde.App. Create the project for the Kladde.

  1. Change to the main folder of your Kladde (where Kladde.slnx and the folder src are).
  2. Create the project: dotnet new maui -n Kladde.App -o src\Kladde.App.
  3. Open src\Kladde.App\Kladde.App.csproj in your tool and start the app on Android and Windows, as in exercise 1.
  4. In the project’s properties (in Visual Studio MAUI Shared, General), set the title to “Kladde”. Leave the ID in the form com.<your-name>.kladde, with a name of your choice, and the version at 1.0.
  5. Stop the app. Add src\Kladde.App to the repository, in a branch feature/maui-app, with a commit message like feat(app): MAUI template as Kladde.App.

How to check: the project starts on Android and Windows and shows the template’s page; git status shows src/Kladde.App with the template’s files, but without the folders bin and obj. If they’re listed, add bin/ and obj/ to the .gitignore.

Solution 3

The command creates a directory src/Kladde.App next to the K2 projects. The template generates namespaces after the project name: your classes are in the namespace Kladde.App. You don’t add the project to the solution file Kladde.slnx in this module yet: whether and how the app gets in is decided by W2.2.1, once there are the first tests, because then it has to be settled what a call of dotnet build or dotnet test should build for the solution. Learn warns about solutions with a MAUI project for a different reason: a dotnet publish on a solution tries to publish every project individually. So always publish the app project itself.

The app’s ID is a string following the pattern of a reversed domain. It isn’t an address that has to be reachable. Choose it deliberately; later, when you distribute the app, a lot depends on it (W2.3.2).

Cleaning up: delete the folder MauiScratch from exercises 1 and 2.

The Kladde: an app next to the web application

With this lesson, the Kladde has a new project: src/Kladde.App, an app that starts on Android and Windows and still shows the template’s page. You don’t change the projects from K2 (Kladde.Core, Kladde.Data, Kladde.Web and the others). The app will use the Kladde’s web API, from W2.2.2, and the Kladde’s rules stay where they are. In W2.1.2 you build the first pages of your own: a contact list and today’s appointments, still with fixed sample data in the code.

What you can do now

  • You explain what .NET MAUI is, how the four platforms are addressed, and what is required at minimum in .NET 10 according to Learn.
  • You set up the workload, Android SDK, emulator and Windows as a target, and you know what’s missing for iOS: a Mac on the network.
  • You match MauiProgram, App, AppShell, Platforms and Resources to their jobs and know where platform-specific code goes.
  • You start the app on an Android emulator and on Windows and use Hot Reload, with its limits.
  • You know the new features and deprecations of .NET 10 that older tutorials still show differently, and you’ve created Kladde.App.

Read more