MAUI Architectural Patterns


Various architectural patterns in .NET MAUI help you design scalable, maintainable and testable applications. No matter how complex an application is, a good application structure obviously creates an opportunity for reuse. This short article will serve as a guide to help you select the right architectural pattern. Below are some of the popular architectural patterns that .NET MAUI supports:

Model-View-ViewModel (MVVM)

Some of you might already know, but MVVM is an architectural pattern that is most popularly used to build .NET MAUI applications. So it actually helps the code to segregate the UI logic from the business logic and makes the code maintainable and testable.

What is MVVM?

  • Model: It’s the data, the business logic, and the backend functionality, all rolled into one. It is supposed to contain the data and state of the application and handles the required data and trivial logic.
  • View: It is the user interface representation usually defined in XAML files. It exposes the data exposed by the ViewModel.
  • ViewModel: Intermediary between the Model and the View. It holds onto the data that needs to go into the View and commands that respond to user’s interactions.

Benefits of MVVM in .NET MAUI

  • Separation of Concerns:It allows UI code be dealt with separately from the business logic.
  • Data Binding: Offers a one-to-one View to ViewModel connection that will automatically update the UI when the data is changed.
  • Testability: It means the ViewModel can be tested independently from the UI.
  • Code Reusability: This means that the same ViewModel is used on multiple Views also.

Key Components of MVVM

  • Data Binding: View binds property or collection in ViewModel, so whenever the data changes in the ViewModel the UI reflects the change.
  • Commands: The ViewModel uses commands to process user actions from the View.

MVVM Implementation in .NET MAUI

To implement MVVM in .NET MAUI, you will need:

  1. Your data represented by a Model class.
  2. A ViewModel class that will encapsulate the data and it’s logic.
  3. A View (Page) that binds to the ViewModel.

MVVM CRUD Example

Product Model

Create the model to represent a Product.

namespace MauiAppExample.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public double Price { get; set; }
    }
}

ProductViewModel

We will have the ViewModel responsible for all CRUD operations and keep the list of products.

using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;
using MauiAppExample.Models;
using Microsoft.Maui.Controls;

namespace MauiAppExample.ViewModels
{
    public class ProductViewModel : BindableObject
    {
        private ObservableCollection<Product> products;
        private string newProductName;
        private double newProductPrice;
        private Product selectedProduct;

        public ProductViewModel()
        {
            Products = new ObservableCollection<Product>
            {
                new Product { Id = 1, Name = "Laptop", Price = 999.99 },
                new Product { Id = 2, Name = "Smartphone", Price = 499.99 }
            };

            AddProductCommand = new Command(AddProduct);
            UpdateProductCommand = new Command(UpdateProduct, CanUpdateOrDelete);
            DeleteProductCommand = new Command(DeleteProduct, CanUpdateOrDelete);
            SelectProductCommand = new Command<Product>(SelectProduct);
        }

        public ObservableCollection<Product> Products
        {
            get => products;
            set
            {
                products = value;
                OnPropertyChanged();
            }
        }

        public string NewProductName
        {
            get => newProductName;
            set
            {
                newProductName = value;
                OnPropertyChanged();
            }
        }

        public double NewProductPrice
        {
            get => newProductPrice;
            set
            {
                newProductPrice = value;
                OnPropertyChanged();
            }
        }

        public Product SelectedProduct
        {
            get => selectedProduct;
            set
            {
                selectedProduct = value;
                OnPropertyChanged();
                ((Command)UpdateProductCommand).ChangeCanExecute();
                ((Command)DeleteProductCommand).ChangeCanExecute();
            }
        }

        public ICommand AddProductCommand { get; }
        public ICommand UpdateProductCommand { get; }
        public ICommand DeleteProductCommand { get; }
        public ICommand SelectProductCommand { get; }

        private void AddProduct()
        {
            if (!string.IsNullOrWhiteSpace(NewProductName) && NewProductPrice > 0)
            {
                int newId = Products.Any() ? Products.Max(p => p.Id) + 1 : 1;
                Products.Add(new Product { Id = newId, Name = NewProductName, Price = NewProductPrice });
                NewProductName = string.Empty;
                NewProductPrice = 0;
            }
        }

        private void UpdateProduct()
        {
            if (SelectedProduct != null && !string.IsNullOrWhiteSpace(NewProductName) && NewProductPrice > 0)
            {
                var product = Products.FirstOrDefault(p => p.Id == SelectedProduct.Id);
                if (product != null)
                {
                    product.Name = NewProductName;
                    product.Price = NewProductPrice;
                }
                ClearSelection();
            }
        }

        private void DeleteProduct()
        {
            if (SelectedProduct != null)
            {
                Products.Remove(SelectedProduct);
                ClearSelection();
            }
        }

        private void SelectProduct(Product product)
        {
            if (product != null)
            {
                SelectedProduct = product;
                NewProductName = product.Name;
                NewProductPrice = product.Price;
            }
        }

        private void ClearSelection()
        {
            SelectedProduct = null;
            NewProductName = string.Empty;
            NewProductPrice = 0;
        }

        private bool CanUpdateOrDelete()
        {
            return SelectedProduct != null;
        }
    }
}
  • Products: Contains the collection of products.
  • Commands:
  • AddProductCommand: It adds a new product on the list.
  • UpdateProductCommand and DeleteProductCommand: It updates or deletes the selected product.
  • SelectedProduct: It keeps track of currently selected product for editing and deletion. ChangeCanExecute() updates the commands’ executable status when a product is selected.

MainPage UI

Products will be displayed and managed with CRUD operations using The View that will be providing UI.

<?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:MauiAppExample.ViewModels"
             x:Class="MauiAppExample.MainPage">

    <ContentPage.BindingContext>
        <vm:ProductViewModel />
    </ContentPage.BindingContext>

    <StackLayout Padding="20" Spacing="20">
        <!-- ListView to display products -->
        <ListView x:Name="ProductsList" ItemsSource="{Binding Products}" SelectedItem="{Binding SelectedProduct, Mode=TwoWay}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <TextCell Text="{Binding Name}" Detail="{Binding Price, StringFormat='Price: {0:C}'}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

        <!-- Entry for product name -->
        <Entry Placeholder="Product Name" Text="{Binding NewProductName}" />

        <!-- Entry for product price -->
        <Entry Placeholder="Product Price" Text="{Binding NewProductPrice}" Keyboard="Numeric" />

        <!-- Button to add a product -->
        <Button Text="Add Product" Command="{Binding AddProductCommand}" />

        <!-- Button to update a selected product -->
        <Button Text="Update Product" Command="{Binding UpdateProductCommand}" IsEnabled="{Binding SelectedProduct}" />

        <!-- Button to delete a selected product -->
        <Button Text="Delete Product" Command="{Binding DeleteProductCommand}" IsEnabled="{Binding SelectedProduct}" />
    </StackLayout>
</ContentPage>

ListView:

  • Displays the list of products.
  • It binds to Products collection of the ViewModel.
  • SelectedProduct is bound to SelectedItem to track what product is being edited or deleted.

Entries for Product Name and Price:

  • NewProductName and NewProductPrice should be binded to NewProduct properties.

Buttons for CRUD Operations:

  • Add Product: Adds a new product by calling AddProductCommand.
  • Update Product and Delete Product: We only make these button enabled when there is a product selected.

Setting Up Dependency Injection

You can also configure DI in MauiProgram.cs to make your ViewModel testable and automatically provide it to your View.

MauiProgram.cs:

 // Register the ViewModel for DI
        builder.Services.AddSingleton<ProductViewModel>();

MainPage.xaml.cs:

using MauiAppExample.ViewModels;

namespace MauiAppExample
{
    public partial class MainPage : ContentPage
    {
        public MainPage(ProductViewModel viewModel)
        {
            InitializeComponent();
            BindingContext = viewModel;
        }
    }
}

Model-View-Presenter (MVP)

One other common architectural pattern used in .NET MAUI can be used to separate the UI from presentation logic, making the application more testable, and this is MVP.

  • Model: It’s a ring of data and business logic.
  • View: The component usually implements an interface, which represents the UI.
  • Presenter: It acts as the bridge between View and Model. It holds all the logic to allow the user to input and update the View.

Key Features of MVP in .NET MAUI:

  • View Abstraction: The interface, view, is represented so that the implementation can be easily replaced and tested.
  • Presenter Logic: Testing is easier because All business logic is run in the Presenter.

Model-View-Controller (MVC)

MVC is an architectural pattern that separates an application into three main components: Model, View, and Controller. If you don’t need all the complexity of MVVM or MVP for your separation of component but a simpler case—then using MVC in .NET MAUI is an option.

  • Model: Contains data and logic.
  • View: It defines the UI similar to the other patterns.
  • Controller: It is responsible for handling user interactions, working with data out of the Model, and responds with an appropriate View update.

Key Features of MVC in .NET MAUI:

  • Simplicity: It is easier said than done, but MVC is simpler than MVVM and is good to use if your app doesn’t specifically need a strong delineation between user interface and business logic.
  • Decoupling: Not as testable as either MVVM or MVP, yet still basic decoupling of UI and logic.

Flux/Redux

Almost all the time Flux/Redux is used for state management in web applications, but it can also be used in .NET MAUI.

  • Actions: It is Describing the intent of changing the state.
  • Store: It is how applications state is held and receives updates for state based on actions.
  • Reducers: Functions that will take the current state and action and only return the new state.

Key Features of Flux/Redux in .NET MAUI:

  • Centralized State: It maintains the entire application state in one store which itself makes it easy to manage and debug.
  • Predictability: This happens because to the reducer functions, which are set to change the state in a predictable way, starting from a previously well defined state.

Example:

  • An application state management approach is available in .NET MAUI that uses a centralized store like Redux which maintains the application state using patterns such as immutable state and actions.

Reactive Programming with MVU (Model-View-Update)

MVU (ModelViewUpdate) is a type of reactive architectural pattern focused on immutability and a unidirectional flow of data. This is gaining popularity in the .NET ecosystem and operates in .NET MAUI to build reactive UIs.

  • Model: Immutable state of the UI.
  • View: Shows current state of the model.
  • Update: It is a function that receives the current model, and the message and creates a new model.

Key Features of MVU in .NET MAUI:

  • Unidirectional Flow: Every time the View changes, that is the only data available, Model to View, it’s easy to see changes in state.
  • Immutability: An immutable Model makes it predictable to update, easier to debug.
  • React-like Pattern: Just like UI frameworks like React state management, MVU makes state management easier and cleaner.

Clean Architecture

Above all, Clean Architecture is an approach that provides organization of the application and provides better maintainability and supportability.

  • Domain Layer: It holds business logic and domain entity.
  • Application Layer: It is handling application-specific logic, such as use cases and DTOs.
  • Infrastructure Layer: It deals with external concerns like data persistence or network communication.
  • Presentation Layer: It is representing the UI layer, typically using MVVM in a .NET MAUI application.

Key Features of Clean Architecture in .NET MAUI:

  • Separation of Concerns: Each layer plays a specific role that makes it an easy application to test and maintain.
  • Decoupling: Inverted dependencies mean policies that are higher level (such as use case) are not coupled to the lower level (such as the database).

Example of Clean Architecture in .NET MAUI:

  • Entities and business logic live in the Domain layer.
  • Services or use cases in the Application layer work with domain models.
  • Infrastructure layer has classes that handle some external resource, e.g. databases, APIs, etc.
  • MVVM is used to bind the data to the UI on the Presentation layer.

Dependency Injection (DI)

Dependency Injection (DI) is not an architectural pattern, but it is one of the important things while building a maintainable and testable architecture .NET MAUI. It is used with MVVM, MVC, or Clean Architecture.

  • Dependency Injection is built in .NET MAUI.
  • In MauiProgram.cs you can register your services and ViewModels.
  • The modular nature of the app is achieved by the DI container automatically injecting dependencies in the classes.
public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

        // Register services for DI
        builder.Services.AddSingleton<ProductViewModel>();
        builder.Services.AddTransient<IProductService, ProductService>();

        return builder.Build();
    }
}
public partial class MainPage : ContentPage
{
    public MainPage(ProductViewModel viewModel)
    {
        InitializeComponent();
        BindingContext = viewModel;
    }
}

As Singleton, our ProductViewModel will have a single instance throughout the application’s life time.

Transient means the IProductService will be registered as a Transient which means a new instance is created everytime it is requested.

With DI, we can provide dependencies without needing to instantiate them manually and thereby decreasing tight coupling.

Comparison of Architectural Patterns in .NET MAUI

PatternDescriptionBest For
MVVMSeparates UI from business logic, uses binding and commands.Most .NET MAUI apps needing testability and maintainability.
MVPUses a presenter to manage UI logic.Smaller apps with less complexity, emphasis on testability.
MVCSeparates UI, data, and control logic.Simple apps without complex state handling.
Flux/ReduxCentralized state management, unidirectional data flow.Apps needing predictable state changes and centralized state management.
MVUReactive, unidirectional data flow with immutable models.Highly dynamic and reactive UIs.
Clean ArchitectureLayers of responsibilities, decoupled.Large apps needing clear separation of layers and dependencies.