What is .NET MAUI?

.NET Multi-Platform App UI is an open-source cross-platform framework for creating native mobile and desktop apps with C#. Using .NET MAUI, we can develop apps that can run on Android, iOS, macOS, and Windows from a single codebase.

.NET MAUI provides a collection of controls and APIs that enables developers to write code with a single framework for both UI and business logic. Moreover, combining with ASP.NET core for the backend services, the code reuse and sharing can be very beneficial. In addition, .NET MAUI also provides following nice features:

Single project

With .NET MAUI, we can build multi-platform apps from a single project and share the resource files like app manifest. In addition, we can add platform-specific code if necessary. In the following picture, we can see that there are different platforms within the Platforms folder where we can put platform-specific code.

.NET Community Toolkit

.NET Community Toolkit is a collection of helpers and APIs that work for all .NET developers. The toolkit is maintained and published by Microsoft, and is part of the .NET Foundation.

Here, we can use .NET Community Toolkit Mvvm to greatly simplify the model and command binding with attributes. In the following examples, we follow a workshop project from .NET Presentations to create a working app and demonstrate the amazingly ease of use of .NET Community Toolkit.

In the following example, we use an [ObservableProperty] attribute to reduce the code for model binding. Notice that we make the class a partial class since the source generator will help us to complete the code.

// Model Binding with Observable Property //
[ObservableProperty]
private bool _isRefreshing;

After adding the attribute to the private field, the source generator then helps us complete the code for model binding and give us a public property with backing field.

// <auto-generated/>
#pragma warning disable
#nullable enable
namespace MonkeyFinder.ViewModel
{

    // The partial class enables the source generator to complete the code. // 
    partial class MonkeysViewModel
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        // The source generator generate all the code necessary for model binding with a public property with the backing field. //
        /// <inheritdoc cref="_isRefreshing"/>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.0.0.0")]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        public bool IsRefreshing
        {
            get => _isRefreshing;
            set
            {
                if (!global::System.Collections.Generic.EqualityComparer<bool>.Default.Equals(_isRefreshing, value))
                {
                    OnIsRefreshingChanging(value);
                    OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.IsRefreshing);
                    _isRefreshing = value;
                    OnIsRefreshingChanged(value);
                    OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.IsRefreshing);
                }
            }
        }
        //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>Executes the logic for when <see cref="IsRefreshing"/> is changing.</summary>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.0.0.0")]
        partial void OnIsRefreshingChanging(bool value);
        /// <summary>Executes the logic for when <see cref="IsRefreshing"/> just changed.</summary>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.0.0.0")]
        partial void OnIsRefreshingChanged(bool value);
    }
}

After that, we first need to bind the view model.

<?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:model="clr-namespace:MonkeyFinder.Model"
             xmlns:viewmodel="clr-namespace:MonkeyFinder.ViewModel"
             xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls"
             x:DataType="viewmodel:MonkeysViewModel"
             Title="{Binding Title}"
             ios:Page.UseSafeArea="True"
             x:Class="MonkeyFinder.View.MainPage">
<!-- We bind the view model to the view with 
     xmlns:viewmodel="clr-namespace:MonkeyFinder.ViewModel" and x:DataType="viewmodel:MonkeysViewModel" -->

Then, we simply bind the public property.

 <RefreshView Grid.ColumnSpan="2"
             Command="{Binding GetMonkeysCommand}"
             IsRefreshing="{Binding IsRefreshing}">
<!-- We can then bind the property IsRefreshing generated by the source generator to the RefreshView 
     with IsRefreshing="{Binding IsRefreshing}" -->

Another example is command binding. In the following example, we can see that an [RelayCommand] attribute is added to the private method. Notice that the method is an asynchronous method and returns a Task.

// RelayCommand attribute helps us to generate public methods for command binding. //
// The generated methods support asynchronous operations as well if we make our methods asynchronous. //
[RelayCommand]
private async Task GetClosestMonkeyAsync()
{
    if (IsBusy || Monkeys.Count == 0)
    {
        return;
    }

    try
    {
        var location = await _geolocation.GetLastKnownLocationAsync() ??
            await _geolocation.GetLocationAsync(new GeolocationRequest
            {
                DesiredAccuracy = GeolocationAccuracy.Medium,
                Timeout = TimeSpan.FromSeconds(30)
            });

        if (location is null)
        {
            return;
        }

        var first = Monkeys.OrderBy(m =>
            location.CalculateDistance(m.Latitude, m.Longitude, DistanceUnits.Miles)
        ).FirstOrDefault();

        if (first is null)
        {
            return;
        }

        await Shell.Current.DisplayAlert(
            "Closest Monkey",
            $"{first.Name} in {first.Location}",
            "OK");
    }
    catch (Exception ex)
    {
        Debug.WriteLine(ex);
        await Shell.Current.DisplayAlert(
            "Error!",
            $"Unable to get closest monkeys: {ex.Message}",
            "OK");
    }
}

The source generator then helps us to complete the code for command binding and give us a public method. The returns value is an [IAsyncRelayCommand] interface, which supports asynchronous operations.

// <auto-generated/>
#pragma warning disable
#nullable enable
namespace MonkeyFinder.ViewModel
{
    // The partial class enables the source generator to complete the code. // 
    partial class MonkeysViewModel
    {
        /// <summary>The backing field for <see cref="GetClosestMonkeyCommand"/>.</summary>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.0.0.0")]
        private global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand? getClosestMonkeyCommand;

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        // The source generator generate all the code necessary for command binding. //
        // The return type of IAsyncRelayCommand interface support asynchronous operations //
        /// <summary>Gets an <see cref="global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand"/> instance wrapping <see cref="GetClosestMonkeyAsync"/>.</summary>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.0.0.0")]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        public global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand GetClosestMonkeyCommand => 
            getClosestMonkeyCommand ??= new global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand(new global::System.Func<global::System.Threading.Tasks.Task>(GetClosestMonkeyAsync));
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    }
}

We can then bind the command to the button.

<Button Text="Find Closest"
        Style="{StaticResource ButtonOutline}"
        Command="{Binding GetClosestMonkeyCommand}"
        IsEnabled="{Binding IsNotBusy}"
        Grid.Row="1"
        Grid.Column="1"
        Margin="8"/>
<!-- We can also bind the methods generated by the source generator to the Button 
     with Command="{Binding GetClosestMonkeyCommand}" and Command="{Binding GetMonkeysCommand}" -->

We just demonstrate some simple examples here. .NET Community Toolkit has a lot more features waiting to be discovered. Check out the GitHub page for more information (https://github.com/CommunityToolkit/dotnet).

Cross-platform APIs for device features

We can access many device features such as network states, GPS, accelerometer, battery, etc. within .NET MAUI APIs. For example, in the following picture, we register connectivity, geolocation, and map interface in the DI container.

// Registering IConnectivity, IGeolocation, and IMap interface in the DI container. //
builder.Services.AddSingleton<IConnectivity>(Connectivity.Current);
builder.Services.AddSingleton<IGeolocation>(Geolocation.Default);
builder.Services.AddSingleton<IMap>(Map.Default);

// Registering IHttpClientFactory to manage HttpClient instances and make http requests. //
builder.Services.AddHttpClient<IMonkeyService, MonkeyService>();

After registering the interface, we can do the dependency injection as usual and use the device features.

// Device Features from Dependency Injection //
private readonly IConnectivity _connectivity;
private readonly IGeolocation _geolocation;

private readonly IMonkeyService _monkeyService;

// Using dependency injection to inject and use the device features //
public MonkeysViewModel(IMonkeyService monkeyService, IConnectivity connectivity, IGeolocation geolocation)
{
    
    _monkeyService = monkeyService;

    _connectivity = connectivity;
    _geolocation = geolocation;
}
var location = await _geolocation.GetLastKnownLocationAsync() ??
await _geolocation.GetLocationAsync(new GeolocationRequest
{
    DesiredAccuracy = GeolocationAccuracy.Medium,
    Timeout = TimeSpan.FromSeconds(30)
});

Hot reload

Finally, hot reload is supported with .NET MAUI, which greatly reduce the time spending on re-compiling the code and deploying to the emulator. The change in XAML is reflected in the running app instantly and many changes in the C# code can be reflected in the running app directly.

.NET MAUI + Blazor

Blazor is a web UI framework in .NET that enables .NET developers to build interactive web UI with C# and run the code directly in the browser using WebAssembly.

With the introduction of .NET MAUI and .NET 6, Blazor also got some new experimental features such as Blazor Hybrid and Blazor Mobile Bindings. Blazor Hybrid let developers use the web UI components with BlazorWebView in a MAUI app while Blazor Mobile Bindings let developers build native MAUI controls using Razor syntax. The goal is to leverage web technologies like HTML and CSS across mobile, desktop, and web applications. With .NET 7 right around the corner and tons of new features and improvement for both .NET MAUI and Blazor, the progress of developing client-side applications with .NET and C# is really exciting!!

Shen Fu Huang (Monex Insight - Developer)

Reference:
https://dotnet.microsoft.com/en-us/apps/maui
https://docs.microsoft.com/en-us/dotnet/maui/what-is-maui
https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor
https://docs.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-6.0
https://docs.microsoft.com/ja-jp/mobile-blazor-bindings/
https://github.com/CommunityToolkit/dotnet
https://dotnetfoundation.org/community/resources
https://github.com/dotnet-presentations/dotnet-maui-workshop