Skip to content

Instantly share code, notes, and snippets.

@tshego3
Last active May 22, 2025 11:57
Show Gist options
  • Save tshego3/902c4ba8573bfb6c9d47c1e6fac189b1 to your computer and use it in GitHub Desktop.
Save tshego3/902c4ba8573bfb6c9d47c1e6fac189b1 to your computer and use it in GitHub Desktop.
.NET MAUI Cheatsheet

.NET MAUI Cheatsheet

Explanation of .NET MAUI
  1. Explanation of .NET MAUI Project Layout, MainPage, and AppShell
  2. .NET MAUI Cheatsheet

Explanation of .NET MAUI Project Layout, MainPage, and AppShell

In .NET MAUI (Multi-platform App UI), the project structure and the way you define the initial UI of your application have evolved compared to older Xamarin.Forms projects. Below is a detailed explanation of the project layout, the differences between MainPage and AppShell, and their usage cases.

1. .NET MAUI Project Layout

A typical .NET MAUI project has the following structure:

  • Platforms: Contains platform-specific code and resources for Android, iOS, Windows, macOS, etc.
    • Android, iOS, MacCatalyst, Windows, etc.
  • Resources: Contains app-wide resources like images, fonts, styles, and raw assets.
    • Images, Fonts, Styles, Raw, etc.
  • App.xaml: Defines global resources and the entry point of the application.
  • App.xaml.cs: The code-behind file for App.xaml. Contains the App class, which is the entry point of the application.
  • MainPage.xaml: The default page (UI) of the application.
  • MainPage.xaml.cs: The code-behind file for MainPage.xaml.
  • AppShell.xaml: (Optional) Defines the shell-based navigation structure for the app.
  • AppShell.xaml.cs: The code-behind file for AppShell.xaml.

2. MainPage vs. AppShell

MainPage

  • Definition: MainPage is the default page that is displayed when the application starts. It is a simple, single-page UI.
  • Usage: Suitable for simple apps that do not require complex navigation or multiple pages.
  • Example:
    public App()
    {
        InitializeComponent();
        MainPage = new MainPage(); // Sets MainPage as the starting page
    }

AppShell

  • Definition: AppShell is a more advanced navigation structure that allows you to define a hierarchical navigation system, including flyout menus, tabs, and nested navigation.
  • Usage: Ideal for apps with multiple pages, complex navigation, or a need for a consistent UI structure across platforms.
  • Example:
    public App()
    {
        InitializeComponent();
        MainPage = new AppShell(); // Sets AppShell as the starting point
    }

3. Evolution of the App Class

In .NET MAUI, the way the initial window and page are set up has changed to provide more flexibility and control.

Old Approach (Xamarin.Forms-like)

In older versions, you would directly set the MainPage property in the App constructor:

public App()
{
    InitializeComponent();
    MainPage = new MainPage(); // Directly set MainPage
}

New Approach (.NET MAUI)

In .NET MAUI, the CreateWindow method is overridden to provide more control over the creation of the application window:

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

    protected override Window CreateWindow(IActivationState? activationState)
    {
        return new Window(new MainPage()); // Create a new Window with MainPage
    }
}

This new approach allows for more advanced scenarios, such as:

  • Customizing the window (e.g., setting size, title, or other properties).
  • Handling platform-specific initialization.
  • Supporting multiple windows (e.g., on desktop platforms).

4. Usage Cases

When to Use MainPage

  • Your app has a single page or very simple navigation.
  • You do not need advanced navigation features like flyout menus or tabs.
  • You want to keep the app structure simple and lightweight.

When to Use AppShell

  • Your app has multiple pages and requires a structured navigation system.
  • You want to implement features like flyout menus, tabs, or nested navigation.
  • You need a consistent UI structure across different platforms.

5. Example of AppShell Usage

AppShell.xaml

<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:views="clr-namespace:YourAppNamespace.Views"
       x:Class="YourAppNamespace.AppShell">

    <FlyoutItem Title="Home">
        <ShellContent Route="MainPage" ContentTemplate="{DataTemplate views:MainPage}" />
    </FlyoutItem>

    <FlyoutItem Title="Settings">
        <ShellContent Route="SettingsPage" ContentTemplate="{DataTemplate views:SettingsPage}" />
    </FlyoutItem>

</Shell>

AppShell.xaml.cs

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

App.xaml.cs

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

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

6. Key Takeaways

  • MainPage: Simple, single-page apps.
  • AppShell: Complex apps with structured navigation.
  • CreateWindow: Provides more control over window creation in .NET MAUI.

.NET MAUI Cheatsheet

1. Project Setup

  • Create a new .NET MAUI project:
# Create a new .NET MAUI project
dotnet new maui -n MyMauiApp

# Enter the project directory
cd MyMauiApp

# Restore NuGet packages
dotnet restore

#######################
# Android (Droid) Commands
#######################

# Clean the project for Android (if needed)
dotnet clean -f net6.0-android

# Build the project targeting Android
dotnet build -f net6.0-android

# Run/Deploy the app on an Android emulator or device
dotnet run -f net6.0-android

# Publish the project for Android in Release configuration
dotnet publish -f net6.0-android -c Release

#######################
# iOS Commands
#######################

# Clean the project for iOS (if needed)
dotnet clean -f net6.0-ios

# Build the project targeting iOS (requires macOS with iOS SDK)
dotnet build -f net6.0-ios
dotnet build -f net9.0-ios -c Release

# Run the app on an iOS simulator/device
dotnet run -f net6.0-ios

# Publish the project for iOS in Release configuration
dotnet publish -f net9.0-ios -c Release
  • Run the app:
    dotnet build -t:Run -f net7.0-android
    dotnet build -t:Run -f net7.0-ios

2. UI Elements

  • Label:

    <Label Text="{x:Static local:Constants.WelcomeText}" />
    <Label Text="Hello, MAUI!"
           FontSize="24"
           TextColor="Blue"
           HorizontalOptions="Center"
           VerticalOptions="Center" />
  • Button:

    <Button Text="Click Me"
            Clicked="OnButtonClicked"
            HorizontalOptions="Center"
            VerticalOptions="Center" />
    private void OnButtonClicked(object sender, EventArgs e)
    {
        DisplayAlert("Button Clicked", "You clicked the button!", "OK");
    }
  • Entry (Text Input):

    <Entry Placeholder="Enter text here"
           Text="{Binding UserInput}"
           HorizontalOptions="FillAndExpand" />
  • Image:

    <Image Source="dotnet_bot.png"
           Aspect="AspectFill"
           HorizontalOptions="Center"
           VerticalOptions="Center" />
  • ListView:

    <ListView ItemsSource="{Binding Items}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <ViewCell>
                    <Label Text="{Binding Name}" />
                </ViewCell>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>

3. Layouts

  • StackLayout:

    <StackLayout Orientation="Vertical"
                 Spacing="10"
                 Padding="20">
        <Label Text="First Item" />
        <Label Text="Second Item" />
        <Button Text="Click Me" />
    </StackLayout>
  • Grid:

    <Grid RowDefinitions="Auto, Auto, *"
          ColumnDefinitions="*, *"
          RowSpacing="10"
          ColumnSpacing="10">
        <Label Text="Row 0, Column 0" Grid.Row="0" Grid.Column="0" />
        <Label Text="Row 0, Column 1" Grid.Row="0" Grid.Column="1" />
        <Button Text="Click Me" Grid.Row="1" Grid.ColumnSpan="2" />
    </Grid>
  • AbsoluteLayout:

    <AbsoluteLayout>
        <BoxView Color="Red"
                 AbsoluteLayout.LayoutFlags="PositionProportional"
                 AbsoluteLayout.LayoutBounds="0.5, 0.5, 100, 100" />
    </AbsoluteLayout>

4. Navigation

  • Navigation Stack: PushAsync adds to the stack, allowing back navigation. PushModalAsync does not affect the stack and requires explicit closure.

  • User Experience: PushAsync is for linear navigation, while PushModalAsync is for modal, temporary interactions.

  • Navigate to a new page:

    await Shell.Current.GoToAsync(nameof(NewPage));
    await Shell.Current.Navigation.PushAsync(new NewPage());
    await Navigation.PushAsync(new NewPage());
  • Navigate back:

    await Shell.Current.Navigation.PopAsync();
    await Navigation.PopAsync();
  • Modal Navigation:

    await Shell.Current.Navigation.PushModalAsync(new ModalPage());
    await Shell.Current.Navigation.PopModalAsync();
    await Navigation.PushModalAsync(new ModalPage());
    await Navigation.PopModalAsync();

5. Data Binding

  • BindingContext:

    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
            BindingContext = new MainViewModel();
        }
    }
  • Two-Way Binding:

    <Entry Text="{Binding UserInput, Mode=TwoWay}" />
  • Command Binding:

    <Button Text="Click Me"
            Command="{Binding ClickCommand}" />
    
    // This binds the CommandParameter to the NoteId property of the current data context.
    <Button Text="Delete"
            Command="{Binding DeleteNoteAsync}"
            CommandParameter="{Binding NoteId}" />
    
    // This binds the CommandParameter to the entire current data context note object.
    <Button Text="Delete"
            Command="{Binding DeleteNoteAsync}"
            CommandParameter="{Binding .}" />
    public class MainViewModel : INotifyPropertyChanged
    {
        public ICommand ClickCommand { get; }
    
        public MainViewModel()
        {
            ClickCommand = new Command(OnClick);
        }
    
        private void OnClick()
        {
            // Handle click
        }
    }

6. Dependency Injection

  • Register Services:

    public static class MauiProgram
    {
        public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();
            builder
                .UseMauiApp<App>()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                });
    
            builder.Services.AddSingleton<IMyService, MyService>();
            builder.Services.AddTransient<MainPage>();
    
            return builder.Build();
        }
    }
  • Inject Services:

    public partial class MainPage : ContentPage
    {
        private readonly IMyService _myService;
    
        public MainPage(IMyService myService)
        {
            InitializeComponent();
            _myService = myService;
        }
    }

7. Platform-Specific Code

  • Device Information:

    var deviceType = DeviceInfo.DeviceType; // Physical or Virtual
    var platform = DeviceInfo.Platform; // Android, iOS, etc.
  • Platform-Specific XAML:

    <ContentPage>
        <OnPlatform x:TypeArguments="Thickness">
            <On Platform="iOS" Value="0,20,0,0" />
            <On Platform="Android" Value="0,0,0,0" />
        </OnPlatform>
    </ContentPage>
  • Robust File Handling in .NET MAUI: Capturing Photos or Picking Files with Fallback Options

    • Enum Definition
    [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
    public enum MediaSelectionOptions : int
    {
        [EnumMember(Value = "Capture Picture"), Display(Name = "Capture Picture")]
        CapturePicture = 0,
        [EnumMember(Value = "Capture Video"), Display(Name = "Capture Video")]
        CaptureVideo = 1,
        [EnumMember(Value = "Select File"), Display(Name = "Select File")]
        SelectFile = 2
    }
    • Enum: MediaSelectionOptions defines three options: CapturePicture, CaptureVideo, and SelectFile.

    • JsonConverter: The StringEnumConverter ensures that the enum values are serialized/deserialized as strings.

    • EnumMember: Specifies the string value for each enum member.

    • Variables Initialization

    FileResult? result = null;
    Stream? stream = null;
    • result: Holds the result of the file or photo capture operation.

    • stream: Holds the stream of the selected or captured file.

    • Display Action Sheet

    string action = await Shell.Current.DisplayActionSheet(
        "Would you like to take a picture or select one?",
        "Cancel",
        null,
        nameof(Enums.MediaSelectionOptions.CapturePicture),
        nameof(Enums.MediaSelectionOptions.SelectFile)
    );
    • DisplayActionSheet: Displays a dialog with options to either capture a picture or select a file.

    • Enum Parsing and Switch Case

    if (Enum.TryParse<Enums.MediaSelectionOptions>(action, out var selectedOption))
    {
        switch (selectedOption)
        {
            case Enums.MediaSelectionOptions.CapturePicture:
                if (MediaPicker.Default.IsCaptureSupported)
                {
                    result = await MediaPicker.Default.CapturePhotoAsync();
                    if (result != null)
                    {
                        stream = await result.OpenReadAsync();
                    }
                }
                else
                {
                    await Shell.Current.DisplayAlert("Error", "Camera capture is not supported on this device.", "OK");
                }
                break;
    
            case Enums.MediaSelectionOptions.SelectFile:
                result = await FilePicker.Default.PickAsync();
                if (result != null)
                {
                    stream = await result.OpenReadAsync();
                }
                break;
    
            default:
                break;
        }
    }
    else
    {
        await Shell.Current.DisplayAlert("Info", "No selection was made.", "OK");
    }
    
    if (result != null)
    {
        stream!.Position = 0;
        byte[] bytes = [];
        using (var memoryStream = new MemoryStream())
        {
            stream.CopyTo(memoryStream);
            bytes = memoryStream.ToArray();
        }
     }
    • Enum.TryParse: Attempts to parse the selected action into the MediaSelectionOptions enum.
    • Switch Case: Executes different code based on the selected option:
      • CapturePicture: Checks if capturing photos is supported, then captures a photo and opens a stream to the photo.
      • SelectFile: Opens a file picker to select a file and opens a stream to the selected file.
    • DisplayAlert: Shows alerts for errors or information.

8. Styling

  • Global Styles:

    <Application.Resources>
        <ResourceDictionary>
            <Style TargetType="Label">
                <Setter Property="FontSize" Value="18" />
                <Setter Property="TextColor" Value="Black" />
            </Style>
        </ResourceDictionary>
    </Application.Resources>
  • Local Styles:

    <ContentPage.Resources>
        <ResourceDictionary>
            <Style x:Key="MyLabelStyle" TargetType="Label">
                <Setter Property="FontSize" Value="24" />
                <Setter Property="TextColor" Value="Blue" />
            </Style>
        </ResourceDictionary>
    </ContentPage.Resources>
    
    <Label Text="Styled Label" Style="{StaticResource MyLabelStyle}" />

9. Animations

  • Simple Animation:

    await myLabel.FadeTo(0, 1000); // Fade out over 1 second
    await myLabel.FadeTo(1, 1000); // Fade in over 1 second
  • Scale Animation:

    await myButton.ScaleTo(2, 1000); // Scale up to 2x size over 1 second
    await myButton.ScaleTo(1, 1000); // Scale back to normal size over 1 second

10. Localization

  • Resource Files:

    <Label Text="{x:Static resources:AppResources.HelloMessage}" />
    // AppResources.resx
    <data name="HelloMessage" xml:space="preserve">
        <value>Hello, World!</value>
    </data>

Step-by-Step Guide to Using Fluent UI Font Icons in .NET MAUI

1. Add the Font File
  • Download the Fluent UI .ttf font file.
  • Place the font file in the Resources/Fonts folder of your .NET MAUI project.
  • Ensure the build action for the font file is set to MauiFont.
2. Register the Font
  • Open your MauiProgram.cs file.

  • Register the font using the ConfigureFonts method. Here's an example:

    public static class MauiProgram
    {
        public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();
            builder
                .UseMauiApp<App>()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("FluentSystemIcons-Regular.ttf", "FluentSystemIconsRegular");
                });
    
            return builder.Build();
        }
    }
3. Generate the Icon Class
  • Use IconFont2Code to generate a class with the Unicode values for the icons.

  • Save the generated class in your project, for example, in App.Resources.Fonts.

    namespace App.Resources.Fonts
    {
        // Generated by IconFont2Code: https://andreinitescu.github.io/IconFont2Code
        // If you change the class to 'public' in order to use it outside its assembly
        // you should also change the fields from 'static readonly' to 'public static readonly'
        public static class FluentUI
        {
            public static readonly string FontFamily = "FluentUI";
    
            public static readonly string access_time_20_regular = "\ue000";
            public static readonly string timer_20_regular = "\ue001";
            // Add other icons as needed
        }
    }
4. Use the Icon in XAML
  • Reference the font and use the Unicode value in your XAML files:
<xmlns:fonts="clr-namespace:App.Resources.Fonts" />
<Label
    Text="{Binding Source={x:Static fonts:FluentUI.timer_20_regular}}"
    FontFamily="FluentSystemIconsRegular"
    FontSize="20" />
5. Use the Icon in C#
  • You can also set the icon in your C# code:
    var label = new Label
    {
        Text = App.Resources.Fonts.FluentUI.timer_20_regular,
        FontFamily = "FluentSystemIconsRegular",
        FontSize = 20
    };

11. File Handling

  • Read a file:

    using var stream = await FileSystem.OpenAppPackageFileAsync("myfile.txt");
    using var reader = new StreamReader(stream);
    var content = await reader.ReadToEndAsync();
  • Write to a file:

    var filePath = Path.Combine(FileSystem.AppDataDirectory, "myfile.txt");
    await File.WriteAllTextAsync(filePath, "Hello, MAUI!");

12. Networking

  • HTTP Client:
    var httpClient = new HttpClient();
    var response = await httpClient.GetAsync("https://api.example.com/data");
    var content = await response.Content.ReadAsStringAsync();

13. Preferences

  • Save a preference:

    Preferences.Set("username", "JohnDoe");
  • Retrieve a preference:

    var username = Preferences.Get("username", "default_value");

14. Notifications

  • Local Notifications:

    var notification = new NotificationRequest
    {
        Title = "Test Notification",
        Description = "This is a test notification",
        ReturningData = "Dummy data"
    };
    
    await LocalNotificationCenter.Current.Show(notification);

15. Debugging

  • Debug.WriteLine:

    Debug.WriteLine("This is a debug message");
  • Breakpoints:

    • Set breakpoints in your code and run the app in debug mode to inspect variables and step through code.

16. Testing

  • Unit Testing:
    [Test]
    public void TestMethod()
    {
        var result = MyClass.MyMethod();
        Assert.AreEqual(expectedValue, result);
    }

17. Performance Tips

  • Use BindableObject for custom controls to leverage data binding.
  • Avoid heavy computations on the UI thread; use Task.Run for background operations.
  • Use ListView with DataTemplate for efficient list rendering.

18. Common Pitfalls

  • Memory Leaks: Ensure to unsubscribe from events and dispose of resources properly.
  • UI Thread: Always update the UI on the main thread using MainThread.BeginInvokeOnMainThread.

19. Useful NuGet Packages

  • CommunityToolkit.Maui: Provides additional controls and helpers.
  • Refit: Simplifies REST API consumption.
  • Shiny: For background tasks, notifications, and more.

20. Resources

Certainly! Let's expand the .NET MAUI cheat sheet to include lifecycle events and reusable component loops examples.

21. Lifecycle Events

.NET MAUI provides lifecycle events that allow you to hook into key moments in the lifecycle of pages, views, and the application itself.

Page Lifecycle Events

  • OnAppearing: Called when the page is about to appear.
  • OnDisappearing: Called when the page is about to disappear.
protected override void OnAppearing()
{
    base.OnAppearing();
    Debug.WriteLine("Page is appearing");
    // Perform actions like loading data or starting animations
}

protected override void OnDisappearing()
{
    base.OnDisappearing();
    Debug.WriteLine("Page is disappearing");
    // Perform cleanup or stop animations
}

Application Lifecycle Events

  • OnStart: Called when the application starts.
  • OnSleep: Called when the application goes to the background.
  • OnResume: Called when the application resumes from the background.
public partial class App : Application
{
    public App()
    {
        InitializeComponent();
    }

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

    protected override void OnStart()
    {
        Debug.WriteLine("App is starting");
        // Perform actions like initializing services
    }

    protected override void OnSleep()
    {
        Debug.WriteLine("App is sleeping");
        // Save state or pause ongoing tasks
    }

    protected override void OnResume()
    {
        Debug.WriteLine("App is resuming");
        // Restore state or resume tasks
    }
}

View Lifecycle Events

For custom views, you can override lifecycle methods like OnParentSet, OnSizeAllocated, etc.

public class CustomView : ContentView
{
    protected override void OnParentSet()
    {
        base.OnParentSet();
        Debug.WriteLine("CustomView parent has been set");
    }

    protected override void OnSizeAllocated(double width, double height)
    {
        base.OnSizeAllocated(width, height);
        Debug.WriteLine($"CustomView size allocated: {width}x{height}");
    }
}

22. Reusable Component Loops

Reusable components are a great way to avoid code duplication and maintain a clean architecture. Below are examples of creating reusable components and using them in loops.

Creating a Reusable Component

  1. Custom Control (e.g., a reusable card):
    • Create a custom control by subclassing ContentView.
public class CardView : ContentView
{
    public static readonly BindableProperty TitleProperty =
        BindableProperty.Create(nameof(Title), typeof(string), typeof(CardView), string.Empty);

    public string Title
    {
        get => (string)GetValue(TitleProperty);
        set => SetValue(TitleProperty, value);
    }

    public CardView()
    {
        var titleLabel = new Label
        {
            FontSize = 18,
            TextColor = Colors.Black,
            VerticalOptions = LayoutOptions.Center
        };
        titleLabel.SetBinding(Label.TextProperty, new Binding(nameof(Title), source: this));

        Content = new StackLayout
        {
            Padding = 10,
            Children = { titleLabel }
        };
    }
}
  1. Using the Reusable Component in XAML:
    • Register the namespace and use the custom control.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MyMauiApp"
             x:Class="MyMauiApp.MainPage">

    <StackLayout>
        <local:CardView Title="Card 1" />
        <local:CardView Title="Card 2" />
    </StackLayout>
</ContentPage>

Using Reusable Components in Loops

  1. Binding a List of Items to a Reusable Component:
    • Use a CollectionView or ListView to dynamically generate reusable components.
<CollectionView ItemsSource="{Binding Cards}">
    <CollectionView.ItemTemplate>
        <DataTemplate>
            <local:CardView Title="{Binding Title}" />
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>
  1. ViewModel for the List:
    • Define a Card model and a ViewModel to bind the data.
public class Card
{
    public string Title { get; set; }
}

public class MainViewModel
{
    public ObservableCollection<Card> Cards { get; set; }

    public MainViewModel()
    {
        Cards = new ObservableCollection<Card>
        {
            new Card { Title = "Card 1" },
            new Card { Title = "Card 2" },
            new Card { Title = "Card 3" }
        };
    }
}
  1. Binding the ViewModel:
    • Set the BindingContext in the page.
public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
        BindingContext = new MainViewModel();
    }
}

Dynamic Component Creation in Code

If you need to create components dynamically in code (e.g., in a loop):

var stackLayout = new StackLayout();

foreach (var card in cards)
{
    var cardView = new CardView { Title = card.Title };
    stackLayout.Children.Add(cardView);
}

Content = stackLayout;

23. Reusable Component Best Practices

  • Encapsulation: Keep the logic and UI of the reusable component self-contained.
  • Bindable Properties: Use BindableProperty to make components flexible and data-bindable.
  • Performance: Avoid heavy computations in reusable components; use data binding and ObservableCollection for dynamic updates.
  • Reusability: Design components to be generic and reusable across different parts of the app.

24. Example: Reusable Component with Lifecycle

Here’s an example of a reusable component that uses lifecycle events:

public class LifecycleAwareCard : ContentView
{
    public LifecycleAwareCard()
    {
        var label = new Label { Text = "Lifecycle Card" };
        Content = label;

        // Simulate lifecycle awareness
        this.Appearing += (sender, e) => label.Text = "Card Appeared";
        this.Disappearing += (sender, e) => label.Text = "Card Disappeared";
    }
}

Use it in XAML:

<local:LifecycleAwareCard />
  • Lifecycle Events: Use OnAppearing, OnDisappearing, OnStart, OnSleep, and OnResume to manage app and page lifecycles.
  • Reusable Components: Create custom controls using ContentView and bindable properties for reusability.
  • Loops: Use CollectionView or ListView to dynamically generate reusable components from a list of data..

25. Checking Internet Connection

To check for an active internet connection, you can use the Connectivity API provided by .NET MAUI.

Step 1: Add Required Permissions

  • For Android, add the following permissions to the AndroidManifest.xml file:
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />

Step 2: Check Internet Connection

  • Use the Connectivity class to check the network access and type.
using Microsoft.Maui.Networking;

public bool IsInternetAvailable()
{
    var current = Connectivity.NetworkAccess;

    if (current == NetworkAccess.Internet)
    {
        Debug.WriteLine("Internet is available");
        return true;
    }
    else
    {
        Debug.WriteLine("No internet access");
        return false;
    }
}

Step 3: Monitor Connectivity Changes

  • Subscribe to connectivity changes to react to network changes in real-time.
public void MonitorConnectivity()
{
    Connectivity.ConnectivityChanged += ConnectivityChangedHandler;
}

private void ConnectivityChangedHandler(object sender, ConnectivityChangedEventArgs e)
{
    if (e.NetworkAccess == NetworkAccess.Internet)
    {
        Debug.WriteLine("Internet connection restored");
    }
    else
    {
        Debug.WriteLine("Internet connection lost");
    }
}

Step 4: Example Usage

if (IsInternetAvailable())
{
    // Proceed with network operations
}
else
{
    DisplayAlert("No Internet", "Please check your internet connection.", "OK");
}

26. Dark/Light Theming

.NET MAUI supports dark and light themes out of the box. You can dynamically switch between themes or apply them based on the system settings.

Step 1: Define Theme-Specific Resources

  • Add theme-specific colors and styles in App.xaml or a separate resource dictionary.
<Application.Resources>
    <ResourceDictionary>
        <!-- Light Theme -->
        <ResourceDictionary x:Key="LightTheme">
            <Color x:Key="BackgroundColor">White</Color>
            <Color x:Key="TextColor">Black</Color>
        </ResourceDictionary>

        <!-- Dark Theme -->
        <ResourceDictionary x:Key="DarkTheme">
            <Color x:Key="BackgroundColor">Black</Color>
            <Color x:Key="TextColor">White</Color>
        </ResourceDictionary>
    </ResourceDictionary>
</Application.Resources>

Step 2: Apply Theme Based on System Settings

  • Use the AppThemeBinding to automatically switch between themes based on the system theme.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage"
             BackgroundColor="{AppThemeBinding Light=White, Dark=Black}">

    <Label Text="Hello, MAUI!"
           TextColor="{AppThemeBinding Light=Black, Dark=White}"
           HorizontalOptions="Center"
           VerticalOptions="Center" />
</ContentPage>

Step 3: Dynamically Change Themes

  • Programmatically switch themes using the Application.Current.UserAppTheme property.
// Set Light Theme
Application.Current.UserAppTheme = AppTheme.Light;

// Set Dark Theme
Application.Current.UserAppTheme = AppTheme.Dark;

// Follow System Theme
Application.Current.UserAppTheme = AppTheme.Unspecified;

Step 4: Detect System Theme

  • Use the Application.Current.RequestedTheme property to detect the current system theme.
var currentTheme = Application.Current.RequestedTheme;

if (currentTheme == AppTheme.Dark)
{
    Debug.WriteLine("Dark theme is active");
}
else if (currentTheme == AppTheme.Light)
{
    Debug.WriteLine("Light theme is active");
}

Step 5: Example: Toggle Theme Button

  • Add a button to toggle between light and dark themes.
<Button Text="Toggle Theme"
        Clicked="OnToggleThemeClicked"
        HorizontalOptions="Center"
        VerticalOptions="Center" />
private void OnToggleThemeClicked(object sender, EventArgs e)
{
    if (Application.Current.UserAppTheme == AppTheme.Dark)
    {
        Application.Current.UserAppTheme = AppTheme.Light;
    }
    else
    {
        Application.Current.UserAppTheme = AppTheme.Dark;
    }
}

27. Combining Internet Check and Theming

Here’s an example of combining both features in a single page:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage"
             BackgroundColor="{AppThemeBinding Light=White, Dark=Black}">

    <StackLayout>
        <Label Text="Internet Status:"
               TextColor="{AppThemeBinding Light=Black, Dark=White}"
               HorizontalOptions="Center"
               VerticalOptions="Center" />

        <Label x:Name="InternetStatusLabel"
               TextColor="{AppThemeBinding Light=Black, Dark=White}"
               HorizontalOptions="Center"
               VerticalOptions="Center" />

        <Button Text="Check Internet"
                Clicked="OnCheckInternetClicked"
                HorizontalOptions="Center"
                VerticalOptions="Center" />

        <Button Text="Toggle Theme"
                Clicked="OnToggleThemeClicked"
                HorizontalOptions="Center"
                VerticalOptions="Center" />
    </StackLayout>
</ContentPage>
public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
        MonitorConnectivity();
    }

    private void OnCheckInternetClicked(object sender, EventArgs e)
    {
        InternetStatusLabel.Text = IsInternetAvailable() ? "Connected" : "Disconnected";
    }

    private void OnToggleThemeClicked(object sender, EventArgs e)
    {
        if (Application.Current.UserAppTheme == AppTheme.Dark)
        {
            Application.Current.UserAppTheme = AppTheme.Light;
        }
        else
        {
            Application.Current.UserAppTheme = AppTheme.Dark;
        }
    }

    private bool IsInternetAvailable()
    {
        var current = Connectivity.NetworkAccess;
        return current == NetworkAccess.Internet;
    }

    private void MonitorConnectivity()
    {
        Connectivity.ConnectivityChanged += ConnectivityChangedHandler;
    }

    private void ConnectivityChangedHandler(object sender, ConnectivityChangedEventArgs e)
    {
        MainThread.BeginInvokeOnMainThread(() =>
        {
            InternetStatusLabel.Text = e.NetworkAccess == NetworkAccess.Internet ? "Connected" : "Disconnected";
        });
    }
}
  • Internet Connection: Use the Connectivity API to check and monitor network access.
  • Dark/Light Theming: Use AppThemeBinding and Application.Current.UserAppTheme to implement dynamic theming.
  • Combining Features: Integrate both features into your app for a seamless user experience.

28. The differences between ObservableObject and INotifyPropertyChanged in .NET 9, and comprehensive examples for each, adhering to nullable standards.

ObservableObject Example

ObservableObject is part of the CommunityToolkit.Mvvm package and provides a base implementation for INotifyPropertyChanged and INotifyPropertyChanging. It simplifies the process of creating observable properties.

Here's an example of using ObservableObject in a .NET MAUI application:

  1. Install the CommunityToolkit.Mvvm NuGet package:

    dotnet add package CommunityToolkit.Mvvm
    public static class MauiProgram
    {
       public static MauiApp CreateMauiApp()
       {
           var builder = MauiApp.CreateBuilder();
           builder
               .UseMauiApp<App>()
    +          .UseMauiCommunityToolkit()
               .ConfigureFonts(fonts =>
               {
                   fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                   fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
               });
    
    #if DEBUG
           builder.Logging.AddDebug();
    #endif
    
           return builder.Build();
       }
    }
  2. Create a ViewModel using ObservableObject:

    using CommunityToolkit.Mvvm.ComponentModel;
    using CommunityToolkit.Mvvm.Input;
    
    public partial class MainPageViewModel : ObservableObject
    {
        [ObservableProperty]
        private string? _username;
    
        [ObservableProperty]
        private bool _isBusy;
    
        [RelayCommand]
        private void Submit()
        {
            IsBusy = true;
            // Simulate a long-running task
            Task.Delay(2000).ContinueWith(_ => IsBusy = false);
        }
    }
  3. Bind the ViewModel to the View:

    <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 xmlns:vm="clr-namespace:YourNamespace"
                 x:Class="YourNamespace.MainPage">
    
        <ContentPage.BindingContext>
            <vm:MainPageViewModel />
        </ContentPage.BindingContext>
    
        <StackLayout>
            <Entry Text="{Binding Username}" Placeholder="Enter your username" />
            <Button Text="Submit" Command="{Binding SubmitCommand}" IsEnabled="{Binding IsBusy, Converter={StaticResource InverseBooleanConverter}}" />
        </StackLayout>
    </ContentPage>

INotifyPropertyChanged Example

INotifyPropertyChanged is an interface that you implement to notify clients that a property value has changed. This is the traditional way to implement property change notifications.

Here's an example of using INotifyPropertyChanged in a .NET MAUI application:

  1. Create a ViewModel implementing INotifyPropertyChanged:

    using System.ComponentModel;
    using System.Runtime.CompilerServices;
    
    public class MainPageViewModel : INotifyPropertyChanged
    {
        private string? _username;
        public string? Username
        {
            get => _username;
            set
            {
                if (_username != value)
                {
                    _username = value;
                    OnPropertyChanged();
                }
            }
        }
    
        private bool _isBusy;
        public bool IsBusy
        {
            get => _isBusy;
            set
            {
                if (_isBusy != value)
                {
                    _isBusy = value;
                    OnPropertyChanged();
                }
            }
        }
    
        public event PropertyChangedEventHandler? PropertyChanged;
    
        protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    
        public void Submit()
        {
            IsBusy = true;
            // Simulate a long-running task
            Task.Delay(2000).ContinueWith(_ => IsBusy = false);
        }
    }
  2. Bind the ViewModel to the View:

    <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 xmlns:vm="clr-namespace:YourNamespace"
                 x:Class="YourNamespace.MainPage">
    
        <ContentPage.BindingContext>
            <vm:MainPageViewModel />
        </ContentPage.BindingContext>
    
        <StackLayout>
            <Entry Text="{Binding Username}" Placeholder="Enter your username" />
            <Button Text="Submit" Command="{Binding SubmitCommand}" IsEnabled="{Binding IsBusy, Converter={StaticResource InverseBooleanConverter}}" />
        </StackLayout>
    </ContentPage>

Key Differences

  • ObservableObject: Simplifies the implementation by reducing boilerplate code. It uses attributes like [ObservableProperty] and [RelayCommand] to generate properties and commands automatically.
  • INotifyPropertyChanged: Requires manual implementation of property change notifications, which can be more verbose and error-prone.

Both approaches are valid, but ObservableObject can significantly reduce the amount of code you need to write and maintain.

Below is a comprehensive example of a .NET MAUI application that features a login screen using ObservableObject, adheres to nullable standards, and includes command parameters. After a successful login, the user is navigated to a view notes screen.

Usage Cases

Create a comprehensive example in .NET 9 MAUI that features an ObservableObject for a login screen, following nullable standards. Additionally, include an example of command parameters and a view notes screen that appears after logging in.

1. Model: User

This class represents the user model.

public class User
{
    public string? Username { get; set; }
    public string? Password { get; set; }
}
2. ViewModel: LoginViewModel

This class implements ObservableObject and handles the login logic.

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;

public partial class LoginViewModel : ObservableObject
{
    public LoginViewModel()
    {
        PropertyChanged += (s, e) =>
        {
            if (e.PropertyName == nameof(Username))
            {
                LoginCommand.NotifyCanExecuteChanged(); // Renamed from LoginAsyncCommand to LoginCommand
            }
            else if (e.PropertyName == nameof(Password))
            {
                LoginCommand.NotifyCanExecuteChanged(); // Renamed from LoginAsyncCommand to LoginCommand
            }
        };
    }

    [ObservableProperty]
    [Required(ErrorMessage = "Username is required")]
    private string? _username;

    [ObservableProperty]
    [Required(ErrorMessage = "Password is required")]
    private string? _password;

    [ObservableProperty]
    private string? _errorMessage;
  
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(LoginAsyncCommand))]
    private bool _allowLogin = true;
  
    // This method determines whether the LoginCommand can execute
    private bool CanLoginExecute() => !string.IsNullOrWhiteSpace(Username) && !string.IsNullOrWhiteSpace(Password) && AllowLogin.Equals(true);

    [RelayCommand(CanExecute = nameof(CanLoginExecute))]
    private async Task LoginAsync()
    {
        // Simulate a login process
        if (Username == "admin" && Password == "password")
        {
            ErrorMessage = null;
            // await Shell.Current.GoToAsync("//notes");
            await Shell.Current.Navigation.PushAsync(new Views.NotesPage());
        }
        else
        {
            ErrorMessage = "Invalid username or password.";
        }
    }

    // This method is called whenever Username or Password changes
    partial void OnUsernameChanged(string? value) => LoginCommand.NotifyCanExecuteChanged();
    partial void OnPasswordChanged(string? value) => LoginCommand.NotifyCanExecuteChanged();
}
3. View: LoginPage.xaml

This is the XAML for the login screen.

<?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:YourNamespace.ViewModels"
             x:Class="YourNamespace.Views.LoginPage"
             Title="Login">

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

    <StackLayout Padding="20">
        <Entry Placeholder="Username" Text="{Binding Username}" />
        <Entry Placeholder="Password" Text="{Binding Password}" IsPassword="True" />
        <Label Text="{Binding ErrorMessage}" TextColor="Red" />
        <Button Text="Login" Command="{Binding LoginCommand}" />
    </StackLayout>
</ContentPage>
4. ViewModel: NotesViewModel

This class handles the notes screen logic.

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;

public partial class NotesViewModel : ObservableObject
{
    public NotesViewModel()
    {
        PropertyChanged += (s, e) =>
        {
            if (e.PropertyName == nameof(CurrentNote))
            {
                AddNoteCommand.NotifyCanExecuteChanged();
            }
        };
    }

    // private List<int> list { get; set; } = new List<int> { 1, 2, 3, 4, 5 };
    // [ObservableProperty]
    // ObservableCollection<int> _observableCollection = new ObservableCollection<int>(list);
    // ObservableCollection<int> _observableCollection = new(list);

    [ObservableProperty]
    private string? _currentNote;

    [ObservableProperty]
    private ObservableCollection<string> _notes = new ObservableCollection<string>
    {
        "Note 1",
        "Note 2",
        "Note 3"
    };

    // This method determines whether the AddNoteCommand can execute
    private bool CanAddNoteExecute() => !string.IsNullOrEmpty(CurrentNote);

    [RelayCommand(CanExecute = nameof(CanAddNoteExecute))]
    private void AddNote(string? note)
    {
        if (!string.IsNullOrWhiteSpace(note))
        {
            Notes.Add(note);
        }
    }
}
5. View: NotesPage.xaml

This is the XAML for the notes screen.

<?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:YourNamespace.ViewModels"
             x:Class="YourNamespace.Views.NotesPage"
             Title="Notes">

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

    <StackLayout Padding="20">
        <ListView ItemsSource="{Binding Notes}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <Label Text="{Binding .}" />
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <Entry Placeholder="Add a new note" x:Name="NewNoteEntry" Text="{Binding CurrentNote}" />
        <Button Text="Add Note" Command="{Binding AddNoteCommand}" CommandParameter="{Binding Source={x:Reference NewNoteEntry}, Path=Text}" />
    </StackLayout>
</ContentPage>
6. Summary`
  1. CanExecute in LoginViewModel:

    • The CanLoginExecute method checks if both Username and Password are not null or empty.
    • The LoginCommand is updated to include CanExecute = nameof(CanLoginExecute).
    • The OnUsernameChanged and OnPasswordChanged methods notify the LoginCommand to re-evaluate its CanExecute state whenever the Username or Password properties change.
  2. CanExecute in NotesViewModel:

    • The CanAddNoteExecute method checks if the note parameter is not null or empty.
    • The AddNoteCommand is updated to include CanExecute = nameof(CanAddNoteExecute).
  3. Automatic Button State:

    • The Login button will be enabled only when both Username and Password are filled.
    • The Add Note button will be enabled only when the Entry field is not empty.
7. Nullable Standards

To adhere to nullable standards, ensure that your .csproj file includes the following property:

<PropertyGroup>
    <Nullable>enable</Nullable>
</PropertyGroup>
9. Running the Application
  • When the application starts, the user is presented with the login screen.
  • The Login button will be disabled until both Username and Password fields are filled.
  • After entering the correct credentials (admin / password), the user is navigated to the notes screen.
  • On the notes screen, the user can view existing notes and add new ones.
  • The Add Note button will be disabled until the Entry field has some text.
10. Dependencies

Make sure you have the following NuGet packages installed:

  • CommunityToolkit.Mvvm (for ObservableObject, RelayCommand, etc.)
Building a .NET MAUI App with MVVM: Using Partial Views and Community Toolkit

Creating a .NET MAUI application using the MVVM (Model-View-ViewModel) pattern with a list that uses a partial view for each row and a button that sends the row object as a parameter can be achieved by following these steps. We'll use the Community Toolkit for MVVM to simplify the process.

Step 1: Setup the Project

  1. Create a new .NET MAUI project:

    dotnet new maui -n MauiListApp
    cd MauiListApp
  2. Install the Community Toolkit MVVM package:

    dotnet add package CommunityToolkit.Maui
    dotnet add package CommunityToolkit.Mvvm
    dotnet add package Newtonsoft.Json

Step 2: Define the Model

Create a simple model class that represents the data for each row.

// Models/Item.cs
using Newtonsoft.Json;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace MauiListApp.Models
{
    public class Item
    {
        [JsonIgnore]
        private string? _name;
        [JsonIgnore]
        public string? Name
        {
            get => _name;
            set
            {
                if (_name != value)
                {
                    _name = value;
                    OnPropertyChanged();
                }
            }
        }

        [JsonIgnore]
        private string? _description;
        [JsonIgnore]
        public string? Description
        {
            get => _description;
            set
            {
                if (_description != value)
                {
                    _description = value;
                    OnPropertyChanged();
                }
            }
        }

        public event PropertyChangedEventHandler? PropertyChanged;
        protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Step 3: Define the ViewModel

Create a ViewModel that will handle the logic for the list and the button click.

// ViewModels/ItemViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MauiListApp.Models;
using System.Collections.ObjectModel;

namespace MauiListApp.ViewModels
{
    public partial class ItemViewModel : ObservableObject
    {
        [ObservableProperty]
        private ObservableCollection<Item> _items;

        [ObservableProperty]
        private bool _isRefresh;

        public ItemViewModel()
        {
            Items = new ObservableCollection<Item>
            {
                new Item { Name = "Item 1", Description = "Description 1" },
                new Item { Name = "Item 2", Description = "Description 2" },
                new Item { Name = "Item 3", Description = "Description 3" }
            };
        }

        [RelayCommand]
        private async Task OnItemClicked(Item item)
        {
            // Handle the button click event
            // For example, display an alert with the item's name
            await Task.Delay(1000);
            await Shell.Current.DisplayAlert("Item Clicked", $"You clicked {item.Name}", "OK");
        }

        [RelayCommand]
        private async Task Refresh()
        {
            IsRefresh = true;
            // Handle the refresh event
            await Task.Delay(1000);
            IsRefresh = false;
        }
    }
}

Step 4: Create the Partial View for Each Row

Create a partial view (DataTemplate) for each row in the list.

<!-- Views/ItemView.xaml -->
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:viewModels="clr-namespace:MauiListApp.ViewModels"
             x:Class="MauiListApp.Views.ItemView">
    <Grid Padding="10">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <VerticalStackLayout>
            <Label Text="{Binding Name}" FontSize="16" FontAttributes="Bold" />
            <Label Text="{Binding Description}" FontSize="14" />
        </VerticalStackLayout>

        <Button Grid.Column="1"
                Text="Click Me"
                Command="{Binding Source={RelativeSource AncestorType={x:Type viewModels:ItemViewModel}}, Path=OnItemClickedCommand}"
                CommandParameter="{Binding .}" />
    </Grid>
</ContentView>

Step 5: Create the Main View

Create the main view that will display the list of items using the partial view.

<!-- Views/MainPage.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:MauiListApp.ViewModels"
             xmlns:views="clr-namespace:MauiListApp.Views"
             x:Class="MauiListApp.MainPage"
             Title="MAUI List App">

    <ContentPage.BindingContext>
        <viewModels:ItemViewModel />
    </ContentPage.BindingContext>

    <RefreshView IsRefreshing="{Binding IsRefresh}" Command="{Binding RefreshCommand}">
        <CollectionView ItemsSource="{Binding Items}">
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <views:ItemView />
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </RefreshView>
</ContentPage>
// Views/MainPage.xaml.cs
using MauiListApp.ViewModels;

namespace MauiListApp.Views
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();

            ItemViewModel ItemViewModel = IPlatformApplication.Current!.Services.GetService<ItemViewModel>();
            BindingContext = ItemViewModel;
        }
    }
}

Step 6: Register the Views and ViewModels

In the MauiProgram.cs, register the views and viewModels if necessary.

// MauiProgram.cs
using CommunityToolkit.Maui;
using MauiListApp.ViewModels;
using MauiListApp.Views;

namespace MauiListApp;

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

        // Register ViewModels and Views
        builder.Services.AddSingleton<ItemViewModel>();
        builder.Services.AddSingleton<MainPage>();

        return builder.Build();
    }
}

Step 7: Run the Application

Now, you can run the application. The main page will display a list of items, and each item will have a button that, when clicked, will display an alert with the item's name.

Summary

  • Model: Represents the data (Item).
  • ViewModel: Manages the list of items and handles the button click (ItemViewModel).
  • View: The main view (MainPage.xaml) and the partial view for each row (ItemView.xaml).
  • Community Toolkit MVVM: Used for simplifying the MVVM pattern with ObservableObject, ObservableProperty, and RelayCommand.

This setup provides a clean separation of concerns and leverages the power of the Community Toolkit to simplify the MVVM implementation in .NET MAUI.

Comprehensive Guide to developing a .NET MAUI MVVM Todo Application with REST API CRUD Operations

Creating a .NET MAUI MVVM Todo Application with REST API CRUD operations involves several steps. Below is a comprehensive guide to help you build the application, including folder structure, login page, todo detail page, and HTTP requests.

Prerequisites

  • .NET 9 SDK installed
  • Visual Studio 2022 or later with MAUI workload installed
  • Basic understanding of C#, MVVM pattern, and REST APIs

Step 1: Create a New .NET MAUI Project

  1. Open Visual Studio.
  2. Create a new project.
  3. Select .NET MAUI App (Preview).
  4. Name your project (e.g., MauiTodoApp).
  5. Click Create.

Step 2: Folder Structure

Organize your project with the following folder structure:

MauiTodoApp/
│
├── Converters/
│   └── InverseBooleanConverter.cs
│
├── Helpers/
│   └── Settings.cs
|
├── Models/
│   └── TodoItem.cs
│   └── TokenResponse.cs
│   ├── User.cs
│   └── UserResponse.cs
│
├── ViewModels/
│   ├── LoginViewModel.cs
│   ├── TodoListViewModel.cs
│   └── TodoDetailViewModel.cs
│
├── Views/
│   ├── LoginPage.xaml
│   ├── TodoListPage.xaml
│   └── TodoDetailPage.xaml
│
├── Services/
│   └── TodoService.cs
│
├── App.xaml
├── App.xaml.cs
├── AppShell.xaml
├── AppShell.xaml.cs
└── MauiProgram.cs

Step 3: Define the Converters, Heplers and Models

Create a InverseBooleanConverter, Settings, CommandExtensions, TokenResponse, TodoItem, User, and UserResponse, model in the Models folder.

// Converters/InverseBooleanConverter.cs
using System.Globalization;

namespace MauiTodoApp.Converters
{
    public class InverseBooleanConverter : IValueConverter
    {
        public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
        {
            if (value is bool boolValue)
            {
                return !boolValue;
            }
            return false;
        }

        public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
        {
            if (value is bool boolValue)
            {
                return !boolValue;
            }
            return false;
        }
    }
}
// Helpers/Settings.cs
namespace MauiTodoApp.Helpers
{
    public static class Settings
    {
        public static string Username
        {
            get
            {
                return Preferences.Default.Get("Username", "");
            }
            set
            {
                Preferences.Default.Set("Username", value);
            }
        }

        public static string Password
        {
            get
            {
                return Preferences.Default.Get("Password", "");
            }
            set
            {
                Preferences.Default.Set("Password", value);
            }
        }

        public static string AccessToken
        {
            get
            {
                return Preferences.Default.Get("AccessToken", "");
            }
            set
            {
                Preferences.Default.Set("AccessToken", value);
            }
        }

        public static DateTime AccessTokenExpiration
        {
            get
            {
                return Preferences.Default.Get("AccessTokenExpiration", DateTime.UtcNow);
            }
            set
            {
                Preferences.Default.Set("AccessTokenExpiration", value);
            }
        }
    }
}
// Helpers/CommandExtensions.cs
using System.Windows.Input;

namespace MauiTodoApp.Helpers
{
    public static class CommandExtensions
    {
        public static ICommand ToSafeCommand(this ICommand command)
        {
            return command;
        }
    }
}
// Models/TokenResponse.cs
using Newtonsoft.Json;

namespace MauiTodoApp.Models
{
    public class TokenResponse
    {
        [JsonProperty("token_type")]
        public string? TokenType { get; set; }

        [JsonProperty("expires_in")]
        public int ExpiresIn { get; set; }

        [JsonProperty("ext_expires_in")]
        public int ExtExpiresIn { get; set; }

        [JsonProperty("access_token")]
        public string? AccessToken { get; set; }
    }
}
// Models/TodoItem.cs
namespace MauiTodoApp.Models
{
    public class TodoItem
    {
        public int Id { get; set; }
        public int UserId { get; set; }
        public string? Title { get; set; }
        public bool Completed { get; set; }
    }
}
// Models/User.cs
namespace MauiTodoApp.Models
{
    public class User
    {
        public int Id { get; set; }
        public string? Username { get; set; }
        public string? Password { get; set; }
        public string? Email { get; set; }
    }
}
// Models/UserResponse.cs
using System.Collections.Generic;

namespace MauiTodoApp.Models
{
    public class UserResponse
    {
        public required List<User> Users { get; set; }
    }
}

Step 4: Create the TodoService

Create a TodoService class in the Services folder to handle HTTP requests.

// Services/TodoService.cs
using MauiTodoApp.Models;
using System.Net.Http.Headers;
using System.Net.Http.Json;

namespace MauiTodoApp.Services
{
    public class TodoService
    {
        private readonly HttpClient _httpClient;

        public TodoService(HttpClient httpClient)
        {
            _httpClient = httpClient;
        }

        // public static async Task<bool> LoginAsync(string username, string password)
        // {
        //     var userModel = new User { Username = username, Password = password, Email = username };
        //     var json = JsonConvert.SerializeObject(userModel);
        //     HttpContent content = new StringContent(json);
        //     content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
        //     var response = await new HttpClient().PostAsync("https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token", content);
        //     var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(await response.Content.ReadAsStringAsync());
        //     Settings.AccessToken = tokenResponse!.AccessToken!;
        //     Settings.AccessTokenExpiration = DateTime.Now.AddSeconds(tokenResponse!.ExtExpiresIn!);
        //     return string.IsNullOrEmpty(Settings.AccessToken).Equals(false);
        // }

        // public async Task<bool> LoginAsync(string username, string password)
        // {
        //     var loginRequest = new { username, password };
        //     var response = await _httpClient.PostAsJsonAsync("https://jsonplaceholder.typicode.com/users", loginRequest);

        //     return response.IsSuccessStatusCode;
        // }

        public async Task<bool> LoginAsync(string username, string password)
        {
            try
            {
                var response = await _httpClient.GetFromJsonAsync<UserResponse>("https://dummyjson.com/users");

                if (response?.Users != null)
                {
                    var user = response.Users.FirstOrDefault(u =>
                        u.Username!.Equals(username, StringComparison.OrdinalIgnoreCase) &&
                        u.Password == password);

                    return user != null;
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Login failed: {ex.Message}");
            }

            return false;
        }

        public async Task<List<TodoItem>> GetTodosAsync()
        {
            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "token");
            return await _httpClient.GetFromJsonAsync<List<TodoItem>>("https://jsonplaceholder.typicode.com/todos") ?? new();
        }

        public async Task<TodoItem> GetTodoAsync(int id)
        {
            return await _httpClient.GetFromJsonAsync<TodoItem>($"https://jsonplaceholder.typicode.com/todos/{id}") ?? new();
        }

        public async Task<TodoItem> CreateTodoAsync(TodoItem todo)
        {
            var response = await _httpClient.PostAsJsonAsync("https://jsonplaceholder.typicode.com/posts", todo);
            return await response.Content.ReadFromJsonAsync<TodoItem>() ?? new();
        }

        public async Task UpdateTodoAsync(int id, TodoItem todo)
        {
            await _httpClient.PutAsJsonAsync($"https://jsonplaceholder.typicode.com/todos/{id}", todo);
        }

        public async Task DeleteTodoAsync(int id)
        {
            await _httpClient.DeleteAsync($"https://jsonplaceholder.typicode.com/todos/{id}");
        }
    }
}

Step 5: Create ViewModels

Create ViewModels for Login, TodoList, and TodoDetail.

LoginViewModel

// ViewModels/LoginViewModel.cs
using MauiTodoApp.Helpers;
using MauiTodoApp.Services;
using MauiTodoApp.Views;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace MauiTodoApp.ViewModels
{
    public partial class LoginViewModel : INotifyPropertyChanged
    {
        public string? Username { get; set; }
        public string? Password { get; set; }

        private bool _isLoading;

        private readonly TodoService _todoService = new(new HttpClient());

        public bool IsLoading
        {
            get => _isLoading;
            set
            {
                _isLoading = value;
                OnPropertyChanged();
            }
        }

        public ICommand LoginCommand
        {
            get
            {
                return new Command(async () =>
                {
                    if (string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password))
                    {
                        await Shell.Current.DisplayAlert("Login failed", "Username and password are required.", "OK");
                        return;
                    }

                    IsLoading = true;
                    var isAuthenticated = await _todoService.LoginAsync(Username!, Password!);

                    if (isAuthenticated)
                    {
                        await Shell.Current.GoToAsync(nameof(TodoListPage));
                        IsLoading = false;
                    }
                    else
                    {
                        IsLoading = false;
                        await Shell.Current.DisplayAlert("Login failed", "Invalid username or password.", "OK");
                    }
                }).ToSafeCommand();
            }
        }

        public event PropertyChangedEventHandler? PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

TodoListViewModel

// ViewModels/TodoListViewModel.cs
using MauiTodoApp.Models;
using MauiTodoApp.Services;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace MauiTodoApp.ViewModels
{
    public partial class TodoListViewModel : INotifyPropertyChanged
    {
        private readonly TodoService _todoService = new(new HttpClient());
        private List<TodoItem>? _todos;
        private bool _isLoading;

        public List<TodoItem> Todos
        {
            get => _todos!;
            set
            {
                _todos = value;
                OnPropertyChanged();
            }
        }

        public bool IsLoading
        {
            get => _isLoading;
            set
            {
                _isLoading = value;
                OnPropertyChanged();
            }
        }

        public event PropertyChangedEventHandler? PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        public async Task LoadTodosAsync()
        {
            IsLoading = true;
            Todos = await _todoService.GetTodosAsync();
            IsLoading = false;
        }
    }
}

TodoDetailViewModel

// ViewModels/TodoDetailViewModel.cs
using MauiTodoApp.Helpers;
using MauiTodoApp.Models;
using MauiTodoApp.Views;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace MauiTodoApp.ViewModels
{
    public partial class TodoDetailViewModel : INotifyPropertyChanged
    {
        private TodoItem? _todo;

        public TodoItem Todo
        {
            get { return _todo!; }
            set
            {
                _todo = value;
                OnPropertyChanged();
            }
        }

        public ICommand BackCommand
        {
            get
            {
                return new Command(async () =>
                {
                    await Shell.Current.GoToAsync(nameof(TodoListPage));
                }).ToSafeCommand();
            }
        }

        public event PropertyChangedEventHandler? PropertyChanged;
        protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Step 6: Create Views

Create XAML pages for Login, TodoList, and TodoDetail.

LoginPage

<!-- Views/LoginPage.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:local="clr-namespace:MauiTodoApp"
             x:Class="MauiTodoApp.Views.LoginPage"
             xmlns:viewModels="clr-namespace:MauiTodoApp.ViewModels"
             x:DataType="viewModels:LoginViewModel"
             Shell.NavBarIsVisible="false"
             Title="Login">

    <ContentPage.BindingContext>
        <viewModels:LoginViewModel />
    </ContentPage.BindingContext>

    <StackLayout Orientation="Vertical">
        <ActivityIndicator IsRunning="{Binding IsLoading}" IsVisible="{Binding IsLoading}" VerticalOptions="Center" HorizontalOptions="Center" />

        <Entry Text="{Binding Username}" Placeholder="username" IsVisible="{Binding IsLoading, Converter={StaticResource InverseBooleanConverter}}" />
        <Entry Text="{Binding Password}" Placeholder="password" IsPassword="True" IsVisible="{Binding IsLoading, Converter={StaticResource InverseBooleanConverter}}" />
        <Button Command="{Binding LoginCommand}" Text="Login" IsVisible="{Binding IsLoading, Converter={StaticResource InverseBooleanConverter}}" />
    </StackLayout>
</ContentPage>
// Views/LoginPage.xaml.cs -->
using MauiTodoApp.Models;

namespace MauiTodoApp.Views
{
    public partial class LoginPage : ContentPage
    {
        public LoginPage()
        {
            InitializeComponent();
        }
    }
}

TodoListPage

<!-- Views/TodoListPage.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:local="clr-namespace:MauiTodoApp"
             x:Class="MauiTodoApp.Views.TodoListPage"
             xmlns:models="clr-namespace:MauiTodoApp.Models"
             xmlns:viewModels="clr-namespace:MauiTodoApp.ViewModels"
             x:DataType="viewModels:TodoListViewModel"
             Title="Todo List">

    <ContentPage.BindingContext>
        <viewModels:TodoListViewModel />
    </ContentPage.BindingContext>

    <Shell.BackButtonBehavior>
        <BackButtonBehavior IsVisible="False" IsEnabled="False"/>
    </Shell.BackButtonBehavior>

    <StackLayout Orientation="Vertical">
        <ActivityIndicator IsRunning="{Binding IsLoading}" IsVisible="{Binding IsLoading}" VerticalOptions="Center" HorizontalOptions="Center" />

        <ListView ItemsSource="{Binding Todos}" ItemTapped="GoToTodoDetailPage_ItemTapped" IsVisible="{Binding IsLoading, Converter={StaticResource InverseBooleanConverter}}">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="models:TodoItem">
                    <ViewCell>
                        <StackLayout>
                            <Label Text="{Binding Title}"/>
                            <Label Text="{Binding Completed, StringFormat='Completed: {0}'}" />
                        </StackLayout>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>
</ContentPage>
// Views/TodoListPage.xaml.cs -->
using MauiTodoApp.Models;
using MauiTodoApp.ViewModels;

namespace MauiTodoApp.Views
{
    public partial class TodoListPage : ContentPage
    {
        public TodoListPage()
        {
            InitializeComponent();
        }

        protected override async void OnAppearing()
        {
            base.OnAppearing();
            if (BindingContext is TodoListViewModel viewModel)
            {
                await viewModel.LoadTodosAsync();
            }
        }

        private async void GoToTodoDetailPage_ItemTapped(object sender, ItemTappedEventArgs e)
        {
            var todo = e.Item as TodoItem;
            //var navigationParameter = new ShellNavigationQueryParameters
            //{
            //    { "Todo", todo! }
            //};
            //await Shell.Current.GoToAsync(nameof(TodoDetailPage), navigationParameter);
            await Navigation.PushAsync(new TodoDetailPage(todo));
        }
    }
}

TodoDetailPage

<!-- Views/TodoDetailPage.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"
             x:Class="MauiTodoApp.Views.TodoDetailPage"
             xmlns:viewModels="clr-namespace:MauiTodoApp.ViewModels"
             x:DataType="viewModels:TodoDetailViewModel"
             Title="Todo Details">

    <ContentPage.BindingContext>
        <viewModels:TodoDetailViewModel />
    </ContentPage.BindingContext>

    <Shell.BackButtonBehavior>
        <BackButtonBehavior Command="{Binding BackCommand}" />
    </Shell.BackButtonBehavior>

    <StackLayout Orientation="Vertical">
        <Entry Text="{Binding Todo.Title}" />
    </StackLayout>
</ContentPage>
// Views/TodoDetailPage.xaml.cs -->
using MauiTodoApp.Models;
using MauiTodoApp.ViewModels;

namespace MauiTodoApp.Views
{
    public partial class TodoDetailPage : ContentPage, IQueryAttributable
    {
        public TodoDetailPage(TodoItem? todoItem = null)
        {
            InitializeComponent();

            if (todoItem != null)
            {
                var todoDetailViewModel = new TodoDetailViewModel
                {
                    Todo = todoItem!
                };
                BindingContext = todoDetailViewModel;
            }
        }

        public void ApplyQueryAttributes(IDictionary<string, object> query)
        {
            if (query.TryGetValue("Todo", out object? value))
            {
                var todoItem = value as TodoItem;

                var todoDetailViewModel = new TodoDetailViewModel
                {
                    Todo = todoItem!
                };
                BindingContext = todoDetailViewModel;
            }
        }
    }
}

Step 7: Register Services, ViewModels, and Views

In MauiProgram.cs, register your Services, ViewModels, and Views.

// MauiProgram.cs
using MauiTodoApp.Services;
using MauiTodoApp.ViewModels;
using MauiTodoApp.Views;
using Microsoft.Extensions.Logging;

namespace MauiTodoApp
{
    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");
                });

            builder.Services.AddTransient<LoginViewModel>();
            builder.Services.AddTransient<TodoListViewModel>();
            builder.Services.AddTransient<TodoDetailViewModel>();

            builder.Services.AddTransient<LoginPage>();
            builder.Services.AddTransient<TodoListPage>();
            builder.Services.AddTransient<TodoDetailPage>();

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

            return builder.Build();
        }
    }
}

Step 8: Register the converters in your App.xaml:

<!-- App.xaml -->
<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MauiTodoApp"
             x:Class="MauiTodoApp.App"
             xmlns:viewModels="clr-namespace:MauiTodoApp.ViewModels"
             xmlns:converters="clr-namespace:MauiTodoApp.Converters">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Resources/Styles/Colors.xaml" />
                <ResourceDictionary Source="Resources/Styles/Styles.xaml" />
            </ResourceDictionary.MergedDictionaries>
            <converters:InverseBooleanConverter x:Key="InverseBooleanConverter" />
        </ResourceDictionary>
    </Application.Resources>
</Application>

Step 9: Create AppShell

Create an AppShell to handle navigation.

<!-- 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:local="clr-namespace:MauiTodoApp"
       xmlns:views="clr-namespace:MauiTodoApp.Views"
       x:Class="MauiTodoApp.AppShell">
    <ShellContent Title="Login" ContentTemplate="{DataTemplate views:LoginPage}" Route="LoginPage" />
    <ShellContent Title="Todo List" ContentTemplate="{DataTemplate views:TodoListPage}" Route="TodoListPage" />
    <ShellContent Title="Todo Detail" ContentTemplate="{DataTemplate views:TodoDetailPage}" Route="TodoDetailPage" />
</Shell>
// AppShell.xaml.cs
using MauiTodoApp.Views;

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

            Routing.RegisterRoute(nameof(LoginPage), typeof(LoginPage));
            Routing.RegisterRoute(nameof(TodoListPage), typeof(TodoListPage));
            Routing.RegisterRoute(nameof(TodoDetailPage), typeof(TodoDetailPage));
        }
    }
}
Comprehensive Guide to implement service interface injection in Xamarin and .NET MAUI for Android and iOS platforms

To implement service interface injection in Xamarin and .NET MAUI for Android and iOS platforms, you can use DependencyService in Xamarin or Dependency Injection in .NET MAUI. Below is an example of how to achieve this:

Xamarin Example

1. Define the Interface

public interface IFileHelper
{
    Task<string> GetDocumentsPath();
    Task WriteToFile(string filename, string content);
    Task ShareFile(string filename); // Added for iOS file sharing
}

2. Implement the Interface for Each Platform

  • Android:

    [assembly: Dependency(typeof(FileHelperAndroid))]
    namespace YourNamespace.Droid
    {
        public class FileHelperAndroid : IFileHelper
        {
            public Task<string> GetDocumentsPath()
            {
                // Save to external storage (Downloads folder)
                var path = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDownloads).AbsolutePath;
                return Task.FromResult(path);
            }
    
            public async Task WriteToFile(string filename, string content)
            {
                try
                {
                    string path = await GetDocumentsPath();
                    string filePath = Path.Combine(path, filename);
    
                    Debug.WriteLine($"Writing to file: {filePath}");
    
                    if (!Directory.Exists(path))
                    {
                        Directory.CreateDirectory(path);
                    }
    
                    using (StreamWriter writer = new StreamWriter(filePath, append: true))
                    {
                        await writer.WriteLineAsync(content);
                    }
    
                    Debug.WriteLine($"File written successfully: {filePath}");
                }
                catch (UnauthorizedAccessException ex)
                {
                    Debug.WriteLine($"Permission error: {ex.Message}");
                }
                catch (IOException ex)
                {
                    Debug.WriteLine($"IO error: {ex.Message}");
                }
                catch (Exception ex)
                {
                    Debug.WriteLine($"Unexpected error: {ex.Message}");
                }
            }
    
            public Task ShareFile(string filename)
            {
                // Not needed for Android since files are saved to external storage
                return Task.CompletedTask;
            }
        }
    }
  • iOS:

    [assembly: Dependency(typeof(FileHelperIOS))]
    namespace YourNamespace.iOS
    {
        public class FileHelperIOS : IFileHelper
        {
            public Task<string> GetDocumentsPath()
            {
                // Save to app's Documents directory
                var documents = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
                return Task.FromResult(documents);
            }
    
            public async Task WriteToFile(string filename, string content)
            {
                try
                {
                    string path = await GetDocumentsPath();
                    string filePath = Path.Combine(path, filename);
    
                    Debug.WriteLine($"Writing to file: {filePath}");
    
                    if (!Directory.Exists(path))
                    {
                        Directory.CreateDirectory(path);
                    }
    
                    using (StreamWriter writer = new StreamWriter(filePath, append: true))
                    {
                        await writer.WriteLineAsync(content);
                    }
    
                    Debug.WriteLine($"File written successfully: {filePath}");
                }
                catch (UnauthorizedAccessException ex)
                {
                    Debug.WriteLine($"Permission error: {ex.Message}");
                }
                catch (IOException ex)
                {
                    Debug.WriteLine($"IO error: {ex.Message}");
                }
                catch (Exception ex)
                {
                    Debug.WriteLine($"Unexpected error: {ex.Message}");
                }
            }
    
            public async Task ShareFile(string filename)
            {
                try
                {
                    string path = await GetDocumentsPath();
                    string filePath = Path.Combine(path, filename);
    
                    if (!File.Exists(filePath))
                    {
                        Debug.WriteLine("File does not exist.");
                        return;
                    }
    
                    // Use Xamarin.Essentials to share the file
                    await Share.RequestAsync(new ShareFileRequest
                    {
                        Title = "Share File",
                        File = new ShareFile(filePath)
                    });
                }
                catch (Exception ex)
                {
                    Debug.WriteLine($"Error sharing file: {ex.Message}");
                }
            }
        }
    }

3. Use the Service in Your Shared Code

public class SomeService
{
    private readonly IFileHelper _fileHelper;

    public SomeService()
    {
        _fileHelper = DependencyService.Get<IFileHelper>();
    }

    public async Task DoSomething()
    {
        string path = await _fileHelper.GetDocumentsPath();
        await _fileHelper.WriteToFile("test.txt", "Hello, World!");

        #if IOS
        await _fileHelper.ShareFile("test.txt"); // Share the file on iOS
        #endif
    }
}

.NET MAUI Example

1. Define the Interface

public interface IFileHelper
{
    Task<string> GetDocumentsPath();
    Task WriteToFile(string filename, string content);
    Task ShareFile(string filename); // Added for iOS file sharing
}

2. Implement the Interface for Each Platform

  • Android:

    namespace YourNamespace.Platforms.Android
    {
        public class FileHelperAndroid : IFileHelper
        {
            public Task<string> GetDocumentsPath()
            {
                // Save to external storage (Downloads folder)
                var path = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDownloads).AbsolutePath;
                return Task.FromResult(path);
            }
    
            public async Task WriteToFile(string filename, string content)
            {
                try
                {
                    string path = await GetDocumentsPath();
                    string filePath = Path.Combine(path, filename);
    
                    Debug.WriteLine($"Writing to file: {filePath}");
    
                    if (!Directory.Exists(path))
                    {
                        Directory.CreateDirectory(path);
                    }
    
                    using (StreamWriter writer = new StreamWriter(filePath, append: true))
                    {
                        await writer.WriteLineAsync(content);
                    }
    
                    Debug.WriteLine($"File written successfully: {filePath}");
                }
                catch (UnauthorizedAccessException ex)
                {
                    Debug.WriteLine($"Permission error: {ex.Message}");
                }
                catch (IOException ex)
                {
                    Debug.WriteLine($"IO error: {ex.Message}");
                }
                catch (Exception ex)
                {
                    Debug.WriteLine($"Unexpected error: {ex.Message}");
                }
            }
    
            public Task ShareFile(string filename)
            {
                // Not needed for Android since files are saved to external storage
                return Task.CompletedTask;
            }
        }
    }
  • iOS:

    namespace YourNamespace.Platforms.iOS
    {
        public class FileHelperIOS : IFileHelper
        {
            public Task<string> GetDocumentsPath()
            {
                // Save to app's Documents directory
                var documents = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
                return Task.FromResult(documents);
            }
    
            public async Task WriteToFile(string filename, string content)
            {
                try
                {
                    string path = await GetDocumentsPath();
                    string filePath = Path.Combine(path, filename);
    
                    Debug.WriteLine($"Writing to file: {filePath}");
    
                    if (!Directory.Exists(path))
                    {
                        Directory.CreateDirectory(path);
                    }
    
                    using (StreamWriter writer = new StreamWriter(filePath, append: true))
                    {
                        await writer.WriteLineAsync(content);
                    }
    
                    Debug.WriteLine($"File written successfully: {filePath}");
                }
                catch (UnauthorizedAccessException ex)
                {
                    Debug.WriteLine($"Permission error: {ex.Message}");
                }
                catch (IOException ex)
                {
                    Debug.WriteLine($"IO error: {ex.Message}");
                }
                catch (Exception ex)
                {
                    Debug.WriteLine($"Unexpected error: {ex.Message}");
                }
            }
    
            public async Task ShareFile(string filename)
            {
                try
                {
                    string path = await GetDocumentsPath();
                    string filePath = Path.Combine(path, filename);
    
                    if (!File.Exists(filePath))
                    {
                        Debug.WriteLine("File does not exist.");
                        return;
                    }
    
                    // Use Xamarin.Essentials to share the file
                    await Share.RequestAsync(new ShareFileRequest
                    {
                        Title = "Share File",
                        File = new ShareFile(filePath)
                    });
                }
                catch (Exception ex)
                {
                    Debug.WriteLine($"Error sharing file: {ex.Message}");
                }
            }
        }
    }

3. Register the Service in MauiProgram.cs

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 ANDROID
        builder.Services.AddSingleton<IFileHelper, FileHelperAndroid>();
        #elif IOS
        builder.Services.AddSingleton<IFileHelper, FileHelperIOS>();
        #endif

        return builder.Build();
    }
}

4. Use the Service in Your Shared Code

public class SomeService
{
    private readonly IFileHelper _fileHelper;

    public SomeService(IFileHelper fileHelper)
    {
        _fileHelper = fileHelper;
    }

    public async Task DoSomething()
    {
        string path = await _fileHelper.GetDocumentsPath();

        // Dictionary<string, object> model = new Dictionary<string, object>();
        // model.Add("Url", "https://gist.github.com/");
        // model.Add("DateTime", DateTime.Now);
        // model.Add("Request", "<csharp-request-object>");
        // model.Add("Response", "<csharp-response-object>");
        // // model.Add("Response", JsonConvert.DeserializeObject<T>("<http-response-string>"));
        // // // model.Add("Response", JsonConvert.DeserializeObject<object>("<http-response-string>"));
        // System.Diagnostics.Debug.WriteLine(JsonConvert.SerializeObject(model));
        // await _fileHelper.WriteToFile("test.json", JsonConvert.SerializeObject(model));
        await _fileHelper.WriteToFile("test.txt", "Hello, World!");

        #if IOS
        await _fileHelper.ShareFile("test.txt"); // Share the file on iOS
        #endif
    }
}

In both Xamarin and .NET MAUI, you can now inject the IFileHelper service into your classes and use it to perform file operations. The key difference is that Xamarin uses DependencyService for service location, while .NET MAUI uses dependency injection through MauiProgram.cs.

Comprehensive Guide to implement .NET MAUI service for handling notifications (toasts, push notifications, alerts, etc.)

To create a .NET MAUI service for handling notifications (toasts, push notifications, alerts, etc.), you can use dependency injection to make the service available throughout your application. Below is an example of how to implement such a service and inject it into your MAUI application.

Step 1: Create the Notification Service Interface

First, define an interface for the notification service. This will allow you to easily mock or replace the service if needed.

public interface INotificationService
{
    Task ShowToast(string message, ToastDuration toastDuration);
    Task ShowAlertAsync(string title, string message, string cancel = "OK");
    Task<bool> ShowConfirmationAsync(string title, string message, string accept = "Yes", string cancel = "No");
    Task SendPushNotification(string title, string message);
    Task SendLocalNotification(string title, string message);
}

Step 2: Implement the Notification Service

Next, implement the interface. This implementation will use MAUI's built-in capabilities for toasts, alerts, and push notifications.

using Microsoft.Maui.Controls;

public class NotificationService : INotificationService
{
    public async Task ShowToast(string message, ToastDuration toastDuration)
    {
        await CommunityToolkit.Maui.Alerts.Toast.Make(message, toastDuration).Show();
    }

    public async Task ShowAlertAsync(string title, string message, string cancel)
    {
        await Application.Current!.Windows[0].Page!.DisplayAlert(title, message, cancel);
    }

    public async Task<bool> ShowConfirmationAsync(string title, string message, string accept, string cancel)
    {
        return await Application.Current!.Windows[0].Page!.DisplayAlert(title, message, accept, cancel);
    }

    public async Task SendPushNotification(string title, string message)
    {
        // Implement push notification logic here
        // This could involve using a third-party service like Firebase Cloud Messaging (FCM)
        // or a platform-specific notification API.

        await Task.Delay(1000);
    }

    public async Task SendLocalNotification(string title, string message)
    {
        // Implement push notification logic here
        // This could involve using a third-party service like Firebase Cloud Messaging (FCM)
        // or a platform-specific notification API.

        await Task.Delay(1000);
    }
}

Step 3: Register the Service in MAUI Dependency Injection

In your MauiProgram.cs, register the NotificationService as a singleton so that it can be injected wherever needed.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Controls.Hosting;
using Microsoft.Maui.Hosting;

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

+       builder.Services.AddTransient<MainViewModel>();
+       builder.Services.AddTransient<MainPage>();

+       builder.Services.AddSingleton<INotificationService, NotificationService>();

        return builder.Build();
    }
}

Step 4: Inject and Use the Notification Service

Now you can inject the INotificationService into your ViewModels, Pages, or other services.

public class MainViewModel
{
    private readonly INotificationService _notificationService;

    public MainViewModel(INotificationService notificationService)
    {
        _notificationService = notificationService;
    }

    public async Task ShowNotification()
    {
        _notificationService.ShowToast("This is a toast message!");
        await _notificationService.ShowAlertAsync("Alert", "This is an alert message.");
        bool result = await _notificationService.ShowConfirmationAsync("Confirm", "Are you sure?");
        if (result)
        {
            await _notificationService.SendPushNotification("Notification", "You confirmed!");
            await _notificationService.SendLocalNotification("Notification", "You confirmed!");
        }
    }
}

Step 5: Use the ViewModel in Your Page

Finally, bind the ViewModel to your page and use the notification service.

public partial class MainPage : ContentPage
{
    public MainPage(MainViewModel viewModel)
    {
        InitializeComponent();
        BindingContext = viewModel;
    }

    private async void OnButtonClicked(object sender, EventArgs e)
    {
        var viewModel = BindingContext as MainViewModel;
        await viewModel.ShowNotification();
    }
}

Step 6: Add the CommunityToolkit.Maui Package (Optional)

If you want to use the CommunityToolkit.Maui.Alerts for toasts, you need to add the CommunityToolkit.Maui package to your project.

dotnet add package CommunityToolkit.Maui

Then, configure it in your MauiProgram.cs:

using CommunityToolkit.Maui;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
+           .UseMauiCommunityToolkit() // Add this line
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

        builder.Services.AddTransient<MainViewModel>();
        builder.Services.AddTransient<MainPage>();

        builder.Services.AddSingleton<INotificationService, NotificationService>();

        return builder.Build();
    }
}
Accessing the IsBusy property directly within the Microsoft.Maui.Controls.Xaml.ReferenceExtension

Accessing the IsBusy property directly within the Microsoft.Maui.Controls.Xaml.ReferenceExtension or the VerticalStackLayout context is functioning as intended. This typically happens when attempting to bind to a property on the page itself with the use of {x:Reference Page}.

To address this issue, ensure that:

  1. The IsBusy property is correctly defined in the ContentPage code-behind.
  2. The reference to the page (x:Reference Page) is valid.

XAML

Ensure the x:Name is set on your ContentPage so it can be referenced in XAML:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="YourNamespace.YourPage"
+            x:Name="Page">
    <ContentPage.Resources>
        <ResourceDictionary>
            <local:InverseBooleanConverter x:Key="InverseBooleanConverter" />
        </ResourceDictionary>
    </ContentPage.Resources>

    <VerticalStackLayout IsVisible="{Binding Source={x:Reference Page}, Path=IsBusy, Converter={StaticResource InverseBooleanConverter}}">
        <ActivityIndicator IsRunning="{Binding Source={x:Reference Page}, Path=IsBusy}" />
        <Switch x:Name="SettingSwitch" />
    </VerticalStackLayout>
</ContentPage>

Code-Behind

In the ContentPage code-behind, ensure the IsBusy property is defined as follows:

public partial class YourPage : ContentPage
{
    public YourPage()
    {
        InitializeComponent()
        SettingSwitch.IsToggled = false;
        SettingSwitch.Toggled += async (sender, e) => await OnSettingSwitchToggled(sender!, e);
    }

    private async Task OnSettingSwitchToggled(object sender, ToggledEventArgs e)
    {
+       this.IsBusy = true;
        await Task.Delay(1000);
        Console.WriteLine($"Setting switched to: {e.Value}");
+       this.IsBusy = false;
    }

Explanation:

  1. The x:Name="Page" on the ContentPage allows the x:Reference binding in XAML to access the IsBusy property.
  2. The IsBusy property is implemented in the code-behind so that changes are reflected in the UI.

Notes:

  • Ensure that the InverseBooleanConverter is implemented and added to the resource dictionary.
  • If you still encounter binding errors, double-check that x:Reference Page matches the x:Name specified on the ContentPage.
Various Documentation

.NET MAUI WiKi

Common Source Code
using System;
using System.Collections.Generic;
using System.Linq;	
	
// Example 1: Combining collections using Concat (simulate spread operator)
System.Collections.Generic.List<int> listA = new List<int> { 1, 2 };
System.Collections.Generic.List<int> listB = new List<int> { 3, 4 };
var combinedList = listA.Concat(listB).ToList();
Console.WriteLine("Combined List using Concat (simulate spread operator): " +
				  string.Join(", ", combinedList));
// Output: 1, 2, 3, 4

// Example 2: Updating a list using .Select with a return statement in a lambda block.
List<int> numbers = new List<int> { 10, 20, 30 };
var mappedList = numbers.Select(num =>
				{
					// Using block syntax with an explicit return statement to create an anonymous object.
					return new { Original = num, Double = num * 2 };
				}).ToList();
Console.WriteLine("\nMapped List using Select with a return key:");
foreach (var item in mappedList)
{
	Console.WriteLine($"Original: {item.Original}, Double: {item.Double}");
}
// Output: Anonymous objects printed for each number.

// Example 3: Chained syntax "Where().All().ToList()"
// Wrap the list into our chainable type to allow a chainable All.
List<int> numberList = new List<int> { 2, 4, 6, 8 };
var filteredList = numberList
	.Where(n => n > 0)                      // Filter: keep numbers > 0
	.ToList()
	.All(n => n % 2 == 0);                   // Check: if all numbers are even, keep the chain; else, empty

Console.WriteLine("\nFiltered List using Where().All().ToList(): " +
				  string.Join(", ", filteredList));
// Output: Filtered List using Where().All().ToList(): True

// Example 4: Using a lambda with fallback logic
string result = new Func<string>(() => { try { /*throw new Exception();*/ return mappedList.FirstOrDefault().ToString() ?? "Alternate value"; } catch { return "Alternate value"; } })();
Console.WriteLine($"\n{result}");
// Output: Either "{ Original = 10, Double = 20 }" or "Alternate value" based on the state and exception.
	
// Example 5: Manually Creating the Logger
using Microsoft.Extensions.Logging;

var loggerFactory = LoggerFactory.Create(builder =>
{
    // Configure logging providers
    builder.AddConsole();
    // Optionally add more providers here (e.g., Debug, EventSource)
});

ILogger<MyClassService> logger = loggerFactory.CreateLogger<MyClassService>();
// ILogger<MyClassService> logger = new LoggerFactory().CreateLogger<MyClassService>();

// Now you can pass this logger to your service
var myClassService = new MyClassService(logger);
Case Details Framework

To develop the .NET MAUI 9 Case Details framework, we will focus on creating a robust structure using the MVVM (Model-View-ViewModel) pattern and the MS Community Toolkit. This framework will allow users to view different case categories, their corresponding steps, and navigate to the appropriate views based on the step's status and visibility.

AI Prompt

Please create a detailed guide on developing the .NET MAUI 9 Case Details framework, which will facilitate the viewing of various case categories and their corresponding steps, utilizing MVVM and the MS Community Toolkit. Concentrate solely on the XAML views and the framework itself. I currently have a page that lists different case categories, but I require a framework that will present all the steps and details associated with each category. Each step will have its own view, so when a case category is selected, the case details view should include a header section displaying the available steps for that category. Upon selecting a step, its respective view should be displayed. For instance, if the first step is "Select Supplier," the application should navigate to the "Select Supplier" view when this step is chosen on the case details page.

Please note that each step has a status indicating whether it is not started, in progress, finished, or canceled. Only steps marked as in progress can be accessed, while those with other statuses should be locked. Additionally, each case step will have an indicator to determine its visibility and another indicator of string type to specify which view should be displayed for that particular step within the application.

Avoid using: await Shell.Current.GoToAsync(nameof(NewPage));

Instead, use: await Shell.Current.Navigation.PushAsync(new NewPage());

Also, refrain from using: public App() { InitializeComponent(); MainPage = new MainPage(); }

Instead, implement: public partial class App : Application { public App() { InitializeComponent(); }

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

}

Step 1: Define the Models

First, let's define the models that will represent the case categories and steps.

public class CaseCategory
{
    public string Name { get; set; }
    public List<CaseStep> Steps { get; set; }
}

public class CaseStep
{
    public string Name { get; set; }
    public StepStatus Status { get; set; }
    public bool IsVisible { get; set; }
    public string ViewName { get; set; }
}

public enum StepStatus
{
    NotStarted,
    InProgress,
    Finished,
    Canceled
}

Step 2: Create the ViewModels

Next, we'll create the ViewModels that will handle the logic for displaying the case categories and steps.

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;

public partial class CaseDetailsViewModel : ObservableObject
{
    [ObservableProperty]
    private CaseCategory selectedCategory;

    [ObservableProperty]
    private CaseStep selectedStep;

    public ObservableCollection<CaseCategory> Categories { get; set; } = new ObservableCollection<CaseCategory>();

    public CaseDetailsViewModel()
    {
        // Initialize with sample data
        Categories.Add(new CaseCategory
        {
            Name = "Category 1",
            Steps = new List<CaseStep>
            {
                new CaseStep { Name = "Select Supplier", Status = StepStatus.InProgress, IsVisible = true, ViewName = "SelectSupplierView" },
                new CaseStep { Name = "Review Contract", Status = StepStatus.NotStarted, IsVisible = true, ViewName = "ReviewContractView" }
            }
        });

        // Add more categories and steps as needed
    }

    [RelayCommand]
    private async Task NavigateToStepAsync()
    {
        if (SelectedStep != null && SelectedStep.Status == StepStatus.InProgress && SelectedStep.IsVisible)
        {
            var viewType = Type.GetType($"YourNamespace.{SelectedStep.ViewName}");
            if (viewType != null)
            {
                var page = Activator.CreateInstance(viewType) as Page;
                if (page != null)
                {
                    await Shell.Current.Navigation.PushAsync(page);
                }
            }
        }
    }
}

Step 3: Create the XAML Views

Now, let's create the XAML views for displaying the case categories and steps.

CaseCategoriesView.xaml

This view will list all the case categories.

<?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="YourNamespace.CaseCategoriesView"
             Title="Case Categories">
    <CollectionView ItemsSource="{Binding Categories}"
                    SelectionMode="Single"
                    SelectionChanged="OnCategorySelected">
        <CollectionView.ItemTemplate>
            <DataTemplate>
                <Label Text="{Binding Name}" FontSize="Large" Padding="10" />
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</ContentPage>

Add the SelectionChanged Event Handler in the Code-Behind

In the CaseCategoriesView.xaml.cs, we will handle the SelectionChanged event to navigate to the CaseDetailsView and pass the selected CaseCategory as a parameter.

using Microsoft.Maui.Controls;

namespace YourNamespace
{
    public partial class CaseCategoriesView : ContentPage
    {
        public CaseCategoriesView()
        {
            InitializeComponent();
            BindingContext = new CaseDetailsViewModel(); // Set the ViewModel
        }

        private async Task OnCategorySelected(object sender, SelectionChangedEventArgs e)
        {
            if (e.CurrentSelection.FirstOrDefault() is CaseCategory selectedCategory)
            {
                // Navigate to the CaseDetailsView and pass the selected category
                var caseDetailsView = new CaseDetailsView
                {
                    BindingContext = new CaseDetailsViewModel { SelectedCategory = selectedCategory }
                };

                await Shell.Current.Navigation.PushAsync(caseDetailsView);
            }

            // Deselect the item
            ((CollectionView)sender).SelectedItem = null;
        }
    }
}

CaseDetailsView.xaml

This view will display the steps for the selected case category.

<?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="YourNamespace.CaseDetailsView"
             Title="Case Details">
    <StackLayout>
        <Label Text="{Binding SelectedCategory.Name}" FontSize="Large" Padding="10" />
        <CollectionView ItemsSource="{Binding SelectedCategory.Steps}" SelectionMode="Single" SelectedItem="{Binding SelectedStep}">
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <StackLayout>
                        <Label Text="{Binding Name}" FontSize="Medium" Padding="10" />
                        <Label Text="{Binding Status}" FontSize="Small" Padding="10" />
                        <Button Text="Go to Step"
                                Command="{Binding Source={RelativeSource AncestorType={x:Type local:CaseDetailsViewModel}}, Path=NavigateToStepCommand}"
                                IsEnabled="{Binding Status, Converter={StaticResource StepStatusToBoolConverter}}" />
                    </StackLayout>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </StackLayout>
</ContentPage>

Step 4: Create the Step Views

For each step, create a corresponding view. For example, SelectSupplierView.xaml and ReviewContractView.xaml.

SelectSupplierView.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"
             x:Class="YourNamespace.SelectSupplierView"
             Title="Select Supplier">
    <StackLayout>
        <Label Text="Select Supplier View" FontSize="Large" Padding="10" />
        <!-- Add more UI elements as needed -->
    </StackLayout>
</ContentPage>

ReviewContractView.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"
             x:Class="YourNamespace.ReviewContractView"
             Title="Review Contract">
    <StackLayout>
        <Label Text="Review Contract View" FontSize="Large" Padding="10" />
        <!-- Add more UI elements as needed -->
    </StackLayout>
</ContentPage>

Step 5: Implement the Step Status Converter

Create a converter to determine if a step is accessible based on its status.

using System.Globalization;

public class StepStatusToBoolConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is StepStatus status)
        {
            return status == StepStatus.InProgress;
        }
        return false;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Step 6: Register the Converter in XAML

Register the converter in your App.xaml or the specific page where it's needed.

<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="YourNamespace.App">
    <Application.Resources>
        <ResourceDictionary>
            <local:StepStatusToBoolConverter x:Key="StepStatusToBoolConverter" />
        </ResourceDictionary>
    </Application.Resources>
</Application>

Step 7: Navigation Setup

Ensure that your App.xaml.cs is set up correctly for navigation.

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

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

Step 8: Binding Context in Views

Finally, set the binding context for your views in the code-behind.

CaseCategoriesView.xaml.cs

public partial class CaseCategoriesView : ContentPage
{
    public CaseCategoriesView()
    {
        InitializeComponent();
        BindingContext = new CaseDetailsViewModel();
    }
}

CaseDetailsView.xaml.cs

public partial class CaseDetailsView : ContentPage
{
    public CaseDetailsView()
    {
        InitializeComponent();
        BindingContext = new CaseDetailsViewModel();
    }
}

Conclusion

This framework provides a structured approach to managing case categories and their steps in a .NET MAUI application. By leveraging the MVVM pattern and the MS Community Toolkit, you can ensure a clean separation of concerns and maintainable code. The XAML views are designed to be intuitive and user-friendly, allowing users to navigate through case steps seamlessly.

Wizard (Step-wise Forms / MultiStep Form) Framework Implementation

Alex - Maui Wizard Demo Redth - Building a Step-by-Step / Wizard Control in .NET MAUI

This guide provides a detailed explanation of the MAUI Wizard implementation, which demonstrates how to create a multi-step wizard interface in .NET MAUI. The wizard allows users to navigate through different content pages with smooth animations and maintains state between steps.

Overview

The implementation consists of several key components:

  1. MauiProgram.cs - Sets up the MAUI application and dependency injection
  2. WizardStepsControl - The core control that manages wizard navigation and animation
  3. WizardView - A base class for wizard step content views
  4. MainViewModel - Provides data and state management for the wizard
  5. MainPage - The host page that contains the wizard and navigation buttons
  6. Content Pages - Sample wizard steps (NewContent1, NewContent2, NewContent3)

Detailed Component Breakdown

1. MauiProgram.cs

The entry point for the MAUI application configuration:

using Microsoft.Extensions.Logging;

namespace MauiWizard
{
    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

            builder.Services.AddSingleton<IPageService, PageService>();
            builder.Services.AddSingleton<IShellNavigationService, ShellNavigationService>();
            builder.Services.AddTransient<MainViewModel>();
            builder.Services.AddTransient<MainPage>();
            builder.Services.AddTransient<NewPage2>();

            return builder.Build();
        }
    }
}

This class:

  • Creates the MAUI application builder
  • Configures default fonts
  • Enables debug logging in DEBUG mode
  • Registers the MainViewModel and MainPage with the dependency injection container

2. WizardStepsControl

The core control that manages wizard navigation:

namespace MauiWizard
{
    public class StepChangedEventArgs(int previousStepIndex, int stepIndex) : EventArgs()
    {
        public int PreviousStepIndex { get; } = previousStepIndex;
        public int StepIndex { get; } = stepIndex;
    }

    public partial class WizardStepsControl : Grid
    {
        public static readonly BindableProperty TitleProperty =
            BindableProperty.Create(nameof(Title), typeof(string), typeof(WizardStepsControl), string.Empty);

        public string Title
        {
            get { return (string)GetValue(TitleProperty); }
            set { SetValue(TitleProperty, value); }
        }

        public event EventHandler<StepChangedEventArgs>? StepChanged;

        public int GetCurrentIndex()
        {
            for (var i = 0; i < Children.Count; i++)
            {
                if (Children[i] is VisualElement ve && ve.IsVisible)
                    return i;
            }

            return -1;
        }

        public bool CanGoBack()
        {
            var currentIndex = GetCurrentIndex();

            return currentIndex > 0;
        }

        public bool CanGoForward()
        {
            var currentIndex = GetCurrentIndex();
            var nextIndex = currentIndex + 1;

            return nextIndex < Children.Count;
        }

        private void SetCurrentTitle(IView view)
        {
            if (view is WizardView wizardView)
                Title = wizardView.Title!;
        }

        protected override void OnChildAdded(Element child)
        {
            SetCurrentTitle(Children.FirstOrDefault()!);

            if (child is VisualElement ve)
            {
                ve.IsVisible = false;

                if (GetCurrentIndex() < 0)
                {
                    ve.IsVisible = true;
                }
            }

            base.OnChildAdded(child);
        }

        public async Task Back()
        {
            var c = GetCurrentIndex();

            var currentIndex = c;
            var nextIndex = c - 1;

            if (nextIndex < 0)
                return;
            if (currentIndex == nextIndex)
                return;

            var currentView = Children[currentIndex] as VisualElement;
            var nextView = Children[nextIndex] as VisualElement;

            if (currentView is WizardView currentWizardView)
            {
                await currentWizardView.OnStepDisappearingAsync();
            }

            nextView!.TranslationX = -1 * this.Width;
            nextView!.IsVisible = true;

            await Task.WhenAll(
                nextView!.TranslateTo(0, 0, 500, Easing.CubicInOut),
                currentView!.TranslateTo(this.Width, 0, 500, Easing.CubicInOut));

            currentView!.IsVisible = false;
            currentView!.TranslationX = 0;

            if (nextView is WizardView nextWizardView)
            {
                await nextWizardView.OnStepAppearingAsync();
            }

            SetCurrentTitle(nextView);
            StepChanged?.Invoke(this, new StepChangedEventArgs(currentIndex, nextIndex));
        }

        public async Task Forward()
        {
            var c = GetCurrentIndex();

            var currentIndex = c;
            var nextIndex = c + 1;

            if (nextIndex >= Children.Count)
                nextIndex = 0;
            if (currentIndex == nextIndex)
                return;

            var currentView = Children[currentIndex] as VisualElement;
            var nextView = Children[nextIndex] as VisualElement;

            if (currentView is WizardView currentWizardView)
            {
                await currentWizardView.OnStepDisappearingAsync();
            }

            nextView!.TranslationX = this.Width;
            nextView!.IsVisible = true;

            await Task.WhenAll(
                nextView!.TranslateTo(0, 0, 500, Easing.CubicInOut),
                currentView!.TranslateTo(-1 * this.Width, 0, 500, Easing.CubicInOut));

            currentView!.IsVisible = false;
            currentView!.TranslationX = 0;

            if (nextView is WizardView nextWizardView)
            {
                await nextWizardView.OnStepAppearingAsync();
            }

            SetCurrentTitle(nextView);
            StepChanged?.Invoke(this, new StepChangedEventArgs(currentIndex, nextIndex));
        }

        public async Task GoToFirstPage()
        {
            var currentIndex = GetCurrentIndex();
            if (currentIndex <= 0) return;

            var firstPageIndex = 0;
            await GoToPage(firstPageIndex);
        }

        public async Task GoToPage(int pageIndex)
        {
            if (pageIndex < 0 || pageIndex >= Children.Count)
                throw new ArgumentOutOfRangeException(nameof(pageIndex), "Page index is out of range");

            var currentIndex = GetCurrentIndex();
            if (currentIndex == pageIndex) return;

            var currentView = Children[currentIndex] as VisualElement;
            var nextView = Children[pageIndex] as VisualElement;

            if (currentView is WizardView currentWizardView)
            {
                await currentWizardView.OnStepDisappearingAsync();
            }

            nextView!.TranslationX = pageIndex > currentIndex ? this.Width : -1 * this.Width;
            nextView!.IsVisible = true;

            await Task.WhenAll(
                nextView!.TranslateTo(0, 0, 500, Easing.CubicInOut),
                currentView!.TranslateTo(
                    pageIndex > currentIndex ? -1 * this.Width : this.Width,
                    0,
                    500,
                    Easing.CubicInOut));

            currentView!.IsVisible = false;
            currentView!.TranslationX = 0;

            if (nextView is WizardView nextWizardView)
            {
                await nextWizardView.OnStepAppearingAsync();
            }

            SetCurrentTitle(nextView);
            StepChanged?.Invoke(this, new StepChangedEventArgs(currentIndex, pageIndex));
        }
    }
}

Key features:

  • Inherits from Grid to host multiple wizard steps
  • Manages navigation between steps with Back() and Forward() methods
  • Provides smooth slide animations when transitioning between steps
  • Tracks current step index and validates navigation
  • Exposes StepChanged event to notify when steps change
  • Manages the wizard title based on current step
  • Handles visibility of child views automatically

3. WizardView

The base class for wizard step content:

namespace MauiWizard
{
    public partial class WizardView : ContentView
    {
        public string? Title { get; set; }

        public virtual Task OnStepAppearingAsync()
        {
            return Task.CompletedTask;
        }

        public virtual Task OnStepDisappearingAsync()
        {
            return Task.CompletedTask;
        }
    }
}

This simple class:

  • Inherits from ContentView
  • Adds a Title property to identify each wizard step

4. MainViewModel

The view model for the main page:

using System.Windows.Input;

namespace MauiWizard
{
    public static class CommandExtensions
    {
        public static ICommand ToSafeCommand(this ICommand command)
        {
            return command;
        }
    }
}
namespace MauiWizard
{
    public interface IPageService
    {
        Task<List<IView>> GetPagesAsync();
    }
}
namespace MauiWizard
{
    public class PageService : IPageService
    {
        Task<List<IView>> IPageService.GetPagesAsync()
        {
            List<IView> views = [new NewContent1(), new NewContent2(), new NewContent3()];

            return Task.FromResult(views);
        }
    }
}
namespace MauiWizard
{
    public interface IShellNavigationService
    {
        Task NavigateToPage(string route);
        Task GoBack();
    }
}
namespace MauiWizard
{
    public class ShellNavigationService : IShellNavigationService
    {
        public async Task NavigateToPage(string route)
        {
            await Shell.Current.GoToAsync(route);
        }

        public async Task GoBack()
        {
            await Shell.Current.GoToAsync("..");
        }
    }
}
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace MauiWizard
{
    public partial class MainViewModel : INotifyPropertyChanged
    {
        private readonly IPageService _pageService;
        private readonly IShellNavigationService _shellNavigationService;

        public MainViewModel(IPageService pageService, IShellNavigationService shellNavigationService)
        {
            _pageService = pageService;
            _shellNavigationService = shellNavigationService;

            BackCommand = new Command(async () =>
            {
                if (OnBack is not null)
                    await OnBack.Invoke();
            }).ToSafeCommand();

            ForwardCommand = new Command(async () =>
            {
                if (OnForward is not null)
                    await OnForward.Invoke();
            }).ToSafeCommand();

            GoToFirstPageCommand = new Command(async () =>
            {
                if (OnGoToFirstPage is not null)
                    await OnGoToFirstPage.Invoke();
            }).ToSafeCommand();

            GoToPageCommand = new Command<int>(async (pageIndex) =>
            {
                if (OnGoToPage is not null)
                    await OnGoToPage.Invoke(pageIndex);
            }).ToSafeCommand();

            ShellNavigateToPage = new Command<string>(async (route) =>
            {
                await _shellNavigationService.NavigateToPage(route);
            }).ToSafeCommand();
        }

        public Func<Task>? OnBack { get; set; }
        public Func<Task>? OnForward { get; set; }
        public Func<Task>? OnGoToFirstPage { get; set; }
        public Func<int, Task>? OnGoToPage { get; set; }

        public ICommand BackCommand { get; }
        public ICommand ForwardCommand { get; }
        public ICommand GoToFirstPageCommand { get; }
        public ICommand GoToPageCommand { get; }
        public ICommand ShellNavigateToPage { get; }

        private List<IView>? _views;
        public List<IView>? Views
        {
            get => _views;
            set
            {
                if (_views != value)
                {
                    _views = value;
                    OnPropertyChanged();
                }
            }
        }

        public async Task Initialize()
        {
            Views = await _pageService.GetPagesAsync();
        }

        public async Task ShellNavigateToPageAsync(string route)
        {
            await _shellNavigationService.NavigateToPage(route);
        }

        public event PropertyChangedEventHandler? PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

This view model:

  • Implements INotifyPropertyChanged for data binding
  • Maintains a collection of wizard steps (Views)
  • Initializes with three sample wizard steps
  • Provides property change notifications

5. MainPage

The host page for the wizard:

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

        protected override async void OnAppearing()
        {
            base.OnAppearing();

            if (BindingContext is MainViewModel viewModel)
            {
                await viewModel.Initialize();

                StepControl.Children.Clear();

                foreach (IView view in viewModel.Views!)
                {
                    StepControl.Add(view);
                }

                viewModel.OnBack = async () => await StepControl.Back();
                viewModel.OnForward = async () => await StepControl.Forward();
                viewModel.OnGoToFirstPage = async () => await StepControl.GoToFirstPage();
                viewModel.OnGoToPage = async (pageIndex) => await StepControl.GoToPage(pageIndex);

                Title = StepControl.Title;
                BackButton.IsEnabled = StepControl.CanGoBack();
                ForwardButton.IsEnabled = StepControl.CanGoForward();
                StepControl.StepChanged += StepControl_StepChanged!;

                //await Dispatcher.DispatchAsync(async () =>
                //{
                if (StepControl.Children.FirstOrDefault() is WizardView initialView)
                {
                    await initialView.OnStepAppearingAsync();
                }
                //});
            }
        }

        protected override void OnDisappearing()
        {
            base.OnDisappearing();
            StepControl.StepChanged -= StepControl_StepChanged!;
        }

        private void StepControl_StepChanged(object sender, StepChangedEventArgs e)
        {
            Title = StepControl.Title;
            BackButton.IsEnabled = StepControl.CanGoBack();
            ForwardButton.IsEnabled = StepControl.CanGoForward();
        }
    }
}

The MainPage:

  • Hosts the WizardStepsControl
  • Adds all wizard steps from the view model
  • Manages navigation button states
  • Handles navigation button clicks
  • Updates the page title based on current step

6. MainPage XAML

The UI definition for the main page:

<?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:local="clr-namespace:MauiWizard"
             x:DataType="local:MainViewModel"
             x:Class="MauiWizard.MainPage">

    <Grid Padding="20">
        <ScrollView>
            <local:WizardStepsControl x:Name="StepControl" x:FieldModifier="public">
                <!--<local:NewContent1 />
                <local:NewContent2 />
                <local:NewContent3 />-->
            </local:WizardStepsControl>
        </ScrollView>

        <Grid VerticalOptions="End">
            <Button x:Name="BackButton" 
                    Text="Back"
                    HeightRequest="20"
                    FontAttributes="Bold"
                    HorizontalOptions="Start"
                    Command="{Binding BackCommand}" />
            <Button x:Name="ForwardButton" 
                    Text="Next"
                    HeightRequest="20"
                    FontAttributes="Bold"
                    HorizontalOptions="End"
                    Command="{Binding ForwardCommand}" />
        </Grid>
    </Grid>
</ContentPage>

This XAML:

  • Contains a WizardStepsControl in a ScrollView
  • Includes navigation buttons at the bottom
  • Uses a Grid layout for proper positioning
  • Comments show alternative static content approach

7. Wizard Step Content Views

Three sample wizard step implementations:

NewContent1

using System.Diagnostics;

namespace MauiWizard;

public partial class NewContent1 : WizardView
{
	public NewContent1()
	{
        InitializeComponent();
        btnGoTo3rdView.Clicked += async (sender, e) => await GotTo3rdView(sender!, e);
    }

    public override async Task OnStepAppearingAsync()
    {
        await base.OnStepAppearingAsync();

        // Your async initialization code here
        await Task.Delay(500); // Simulate async work
        Debug.WriteLine("NewContent1 is now appearing!");

        // You can update UI here too
        await MainThread.InvokeOnMainThreadAsync(async () =>
        {
            await Task.Delay(0);
            // Update UI elements
            //await Dispatcher.DispatchAsync(async () =>
            //{
            //    await Shell.Current.CurrentPage.DisplayAlert("Test", "From 1", "Okay");
            //});
        });
    }

    public override async Task OnStepDisappearingAsync()
    {
        await base.OnStepDisappearingAsync();

        // Cleanup code here
        Debug.WriteLine("NewContent1 is disappearing!");
    }

    private async Task GotTo3rdView(object sender, EventArgs e)
    {
        if (BindingContext is MainViewModel viewModel)
        {
            if (viewModel.OnGoToPage != null)
                await viewModel.OnGoToPage.Invoke(2);
        }
    }
}
<?xml version="1.0" encoding="utf-8" ?>
<local:WizardView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MauiWizard"
             x:Class="MauiWizard.NewContent1"
             Title="NewContent1">
    <VerticalStackLayout>
        <Label 
            Text="Welcome to .NET MAUI 1!"
            VerticalOptions="Center" 
            HorizontalOptions="Center" />
        <Button 
            x:Name="btnGoTo3rdView"
            Text="Go to 3rd View" />
        <Button 
            Text="Go to Content Page 2"
            Command="{Binding ShellNavigateToPage}"
            CommandParameter="//Page2" />
    </VerticalStackLayout>
</local:WizardView>

NewContent2

using System.Diagnostics;

namespace MauiWizard;

public partial class NewContent2 : WizardView
{
    public NewContent2()
    {
        InitializeComponent();
    }

    public override async Task OnStepAppearingAsync()
    {
        await base.OnStepAppearingAsync();

        // Your async initialization code here
        await Task.Delay(500); // Simulate async work
        Debug.WriteLine("NewContent2 is now appearing!");

        // You can update UI here too
        await MainThread.InvokeOnMainThreadAsync(async () =>
        {
            // Update UI elements
            await Shell.Current.CurrentPage.DisplayAlert("Test", "From 2", "Okay");
        });
    }

    public override async Task OnStepDisappearingAsync()
    {
        await base.OnStepDisappearingAsync();

        // Cleanup code here
        Debug.WriteLine("NewContent2 is disappearing!");
    }
}
<?xml version="1.0" encoding="utf-8" ?>
<local:WizardView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MauiWizard"
             x:Class="MauiWizard.NewContent2"
             Title="NewContent2">
    <VerticalStackLayout>
        <Label 
            Text="Welcome to .NET MAUI 2!"
            VerticalOptions="Center" 
            HorizontalOptions="Center" />
    </VerticalStackLayout>
</local:WizardView>

NewContent3

using System.Diagnostics;

namespace MauiWizard;

public partial class NewContent3 : WizardView
{
	public NewContent3()
	{
        InitializeComponent();
        btnGoTo1stView.Clicked += async (sender, e) => await GotTo1stView(sender!, e);
    }

    public override async Task OnStepAppearingAsync()
    {
        await base.OnStepAppearingAsync();

        // Your async initialization code here
        await Task.Delay(500); // Simulate async work
        Debug.WriteLine("NewContent3 is now appearing!");

        // You can update UI here too
        await MainThread.InvokeOnMainThreadAsync(async () =>
        {
            // Update UI elements
            await Shell.Current.CurrentPage.DisplayAlert("Test", "From 3", "Okay");
        });
    }

    public override async Task OnStepDisappearingAsync()
    {
        await base.OnStepDisappearingAsync();

        // Cleanup code here
        Debug.WriteLine("NewContent3 is disappearing!");
    }

    private async Task GotTo1stView(object sender, EventArgs e)
    {
        if (BindingContext is MainViewModel viewModel)
        {
            if (viewModel.OnGoToFirstPage != null)
                await viewModel.OnGoToFirstPage.Invoke();
        }
    }
}
<?xml version="1.0" encoding="utf-8" ?>
<local:WizardView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MauiWizard"
             x:Class="MauiWizard.NewContent3"
             Title="NewConten3">
    <VerticalStackLayout>
        <Label 
            Text="Welcome to .NET MAUI 3!"
            VerticalOptions="Center" 
            HorizontalOptions="Center" />
        <Button 
            x:Name="btnGoTo1stView"
            Text="Go to 1st View" />
    </VerticalStackLayout>
</local:WizardView>

8. Content Pages

AppShell

<?xml version="1.0" encoding="UTF-8" ?>
<Shell
    x:Class="MauiWizard.AppShell"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:MauiWizard"
    Shell.FlyoutBehavior="Flyout"
    Title="MauiWizard">
    
    <Shell.FlyoutHeaderTemplate>
        <DataTemplate>
            <Grid HeightRequest="200">
                <Label Text="Maui Wizard App" 
                   FontAttributes="Bold" 
                   HorizontalTextAlignment="Center"
                   VerticalTextAlignment="Center" />
            </Grid>
        </DataTemplate>
    </Shell.FlyoutHeaderTemplate>

    <Shell.FlyoutFooterTemplate>
        <DataTemplate>
            <StackLayout>
                <Label Text="Maui Wizard"
                       FontAttributes="Bold"
                       HorizontalOptions="Center" />
            </StackLayout>
        </DataTemplate>
    </Shell.FlyoutFooterTemplate>

    <FlyoutItem Route="App"
                FlyoutDisplayOptions="AsMultipleItems">

        <Tab Title="Home"
             Route="Home">
            <ShellContent
                Title="Home"
                ContentTemplate="{DataTemplate local:MainPage}"
                Route="Page0" />
        </Tab>

        <ShellContent Route="Page1"
                      Title="New Page 1"
                      ContentTemplate="{DataTemplate local:NewPage1}" />
        <ShellContent Route="Page2"
                      Title="New Page 2"
                      ContentTemplate="{DataTemplate local:NewPage2}" />
    </FlyoutItem>

</Shell>
namespace MauiWizard
{
    public partial class AppShell : Shell
    {
        public AppShell()
        {
            InitializeComponent();
            //Navigated += OnShellNavigated!;
        }

        /*private void OnShellNavigated(object sender, ShellNavigatedEventArgs e)
        {
            if (e.Current?.Location?.OriginalString == "//App/Home/Page0")
            {
                Current.CurrentItem = Current.Items[0];
            }
        }*/
    }
}

NewPage1

<?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="MauiWizard.NewPage1"
             Title="NewPage1">
    <VerticalStackLayout>
        <Label 
            Text="Welcome to .NET MAUI Page 1!"
            VerticalOptions="Center" 
            HorizontalOptions="Center" />
    </VerticalStackLayout>
</ContentPage>

NewPage2

namespace MauiWizard;

public partial class NewPage2 : ContentPage
{
	public NewPage2(MainViewModel viewModel)
    {
        InitializeComponent();
        BindingContext = viewModel;
    }
}
<?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:local="clr-namespace:MauiWizard"
             x:DataType="local:MainViewModel"
             x:Class="MauiWizard.NewPage2"
             Title="NewPage2">
    <VerticalStackLayout>
        <Label 
            Text="Welcome to .NET MAUI Page 2!"
            VerticalOptions="Center" 
            HorizontalOptions="Center" />
        <Button 
            Text="Go to Content Main Page"
            Command="{Binding ShellNavigateToPage}"
            CommandParameter="//App/Home/Page0" />
    </VerticalStackLayout>
</ContentPage>

These content pages:

  • Inherit from WizardView
  • Define simple content for each wizard step
  • Each has a unique title
  • Use VerticalStackLayout for simple content arrangement

Key Implementation Details

  1. Navigation Animation:

    • The WizardStepsControl implements smooth slide animations when moving between steps
    • Uses TranslateTo for animating views horizontally
    • Manages view visibility during transitions
  2. State Management:

    • Tracks current step index
    • Validates navigation (can go back/forward)
    • Updates UI state (button enabled states) accordingly
  3. Event Handling:

    • Provides StepChanged event to notify consumers of navigation
    • Includes previous and current step indices in event args
  4. Flexible Content:

    • Supports adding wizard steps either in XAML or code-behind
    • Content pages are simple to create by inheriting from WizardView
  5. Responsive Design:

    • Uses MAUI layout controls for proper sizing and positioning
    • Includes a ScrollView for content that might exceed screen size

Usage Patterns

  1. Adding Wizard Steps:

    • Steps can be added in XAML (commented out in MainPage.xaml)
    • Or programmatically via view model (current implementation)
  2. Navigation Control:

    • Call Forward() or Back() to navigate
    • Check CanGoForward() or CanGoBack() to validate navigation
  3. Customizing Content:

    • Create new classes inheriting from WizardView
    • Add any MAUI controls as content
    • Set the Title property to identify the step
  4. Responding to Changes:

    • Subscribe to StepChanged event to react to navigation
    • Update UI based on current step

Extensibility Points

  1. Custom Animations:

    • Modify the Forward() and Back() methods to implement different animations
    • Adjust duration or easing functions
  2. Additional Navigation Controls:

    • Add a step progress indicator
    • Implement direct navigation to specific steps
  3. Validation:

    • Add step validation before allowing navigation
    • Show validation errors
  4. Data Persistence:

    • Maintain state across wizard steps
    • Collect data from all steps upon completion

This implementation provides a solid foundation for building wizard-style interfaces in .NET MAUI with smooth animations and clean separation of concerns. The component-based design makes it easy to extend and customize for specific application requirements.

Scanning and Decoding South African License Disc Barcodes with ZXing.Net.Maui

This guide provides a complete implementation for scanning and decoding South African vehicle license disc barcodes (PDF-417 format) in .NET MAUI applications using ZXing.Net.Maui.

Setup and Configuration

1. Install Required Packages

dotnet add package ZXing.Net.Maui
dotnet add package ZXing.Net.Maui.Controls

2. Configure MauiProgram.cs

using ZXing.Net.Maui.Controls;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .UseBarcodeReader(); // Initialize ZXing.Net.Maui
            
        return builder.Build();
    }
}

Data Model

public class RSALicenseDisc
{
    public string? DocumentType { get; set; }
    public string? UnknownField1 { get; set; }
    public string? LicenseNumber { get; set; }
    public string? UnknownField2 { get; set; }
    public string? ControlNumber { get; set; }
    public string? LicenceNumber { get; set; }
    public string? VehicleRegistrationNumber { get; set; }
    public string? VehicleDescription { get; set; }
    public string? Make { get; set; }
    public string? Model { get; set; }
    public string? Color { get; set; }
    public string? VIN { get; set; }
    public string? EngineNumber { get; set; }
    public DateTime? ExpiryDate { get; set; }
}

Scanning Implementation

Barcode Scan Page

public partial class BarCodeScanPage : ContentPage
{
    private TaskCompletionSource<RSALicenseDisc>? _taskCompletionSource;

    public BarCodeScanPage(TaskCompletionSource<RSALicenseDisc> taskCompletionSource)
    {
        InitializeComponent();
        _taskCompletionSource = taskCompletionSource;

        CameraBarcodeReaderView.BarcodesDetected += async (sender, e) => await OnBarcodesDetected(sender!, e);
        CameraBarcodeReaderView.Options = new BarcodeReaderOptions
        {
            Formats = BarcodeFormats.TwoDimensional, // Focus on PDF-417 format
            AutoRotate = true,
            Multiple = true,
            TryHarder = true
        };
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();
        CameraBarcodeReaderView.IsDetecting = true;
    }
    
    protected override void OnDisappearing()
    {
        CameraBarcodeReaderView.IsDetecting = false;
        CameraBarcodeReaderView.BarcodesDetected -= async (sender, e) => await OnBarcodesDetected(sender!, e);
        base.OnDisappearing();
    }

    private async Task OnBarcodesDetected(object sender, BarcodeDetectionEventArgs e)
    {
        try
        {
            RSALicenseDisc rsaLicenseDisc = new();

            foreach (var barcode in e.Results)
            {
                if (barcode.Format == BarcodeFormat.Pdf417)
                {
                    rsaLicenseDisc = await ParseRSALicenseDisc(barcode.Value);
                    if (IsValidLicenseDisc(rsaLicenseDisc))
                    {
                        break;
                    }
                }
            }

            _taskCompletionSource?.TrySetResult(rsaLicenseDisc);
            await Navigation.PopAsync();
        }
        catch (Exception ex)
        {
            await DisplayAlert("Error", ex.Message, "OK");
        }
    }

    private bool IsValidLicenseDisc(RSALicenseDisc disc)
    {
        return !string.IsNullOrEmpty(disc.VehicleRegistrationNumber) &&
               !string.IsNullOrEmpty(disc.Make) &&
               !string.IsNullOrEmpty(disc.Color) &&
               disc.ExpiryDate > DateTime.MinValue;
    }

    private async Task<RSALicenseDisc> ParseRSALicenseDisc(string barcodeData)
    {
        try
        {
            var result = new RSALicenseDisc();
            var segments = barcodeData.Split('%')
                .Where(s => !string.IsNullOrEmpty(s))
                .ToArray();

            if (segments.Length >= 14)
            {
                result.DocumentType = segments[0];
                result.UnknownField1 = segments[1];
                result.LicenseNumber = segments[2];
                result.UnknownField2 = segments[3];
                result.ControlNumber = segments[4];
                result.LicenceNumber = segments[5];
                result.VehicleRegistrationNumber = segments[6];
                result.VehicleDescription = segments[7];
                result.Make = segments[8];
                result.Model = segments[9];
                result.Color = segments[10];
                result.VIN = segments[11];
                result.EngineNumber = segments[12];
                
                if (DateTime.TryParseExact(segments[13], "yyyy-MM-dd",
                    CultureInfo.InvariantCulture, DateTimeStyles.None, out var expiryDate))
                {
                    result.ExpiryDate = expiryDate;
                }
            }

            return result;
        }
        catch (Exception ex)
        {
            await DisplayAlert("Error", ex.Message, "OK");
            return new RSALicenseDisc();
        }
    }
}

XAML for Scan Page

<?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:zxing="clr-namespace:ZXing.Net.Maui.Controls;assembly=ZXing.Net.Maui"
             x:Class="YourNamespace.BarCodeScanPage"
             Title="Scan License Disc">
    
    <Grid>
        <zxing:CameraBarcodeReaderView x:Name="CameraBarcodeReaderView"
                                      IsDetecting="True" />
        
        <Grid HorizontalOptions="Center" 
              VerticalOptions="Center"
              WidthRequest="300"
              HeightRequest="150">

            <BoxView Color="Red" 
                    HeightRequest="2"
                    VerticalOptions="Start"
                    HorizontalOptions="Fill"/>

            <BoxView Color="Red" 
                    WidthRequest="2"
                    VerticalOptions="Fill"
                    HorizontalOptions="Start"/>

            <BoxView Color="Red" 
                    WidthRequest="2"
                    VerticalOptions="Fill"
                    HorizontalOptions="End"/>

            <BoxView Color="Red" 
                    HeightRequest="2"
                    VerticalOptions="End"
                    HorizontalOptions="Fill"/>
        </Grid>
    </Grid>
</ContentPage>

Integration with Parent ViewModel

[RelayCommand]
public async Task ScanBarCode()
{
    try
    {
        LicenseDiscDetails = await GetBarCodeValues();
        
        // Process the scanned data
        if (!string.IsNullOrEmpty(LicenseDiscDetails?.VehicleRegistrationNumber))
        {
            // Update UI or process the data further
        }
    }
    catch (Exception ex)
    {
        await Shell.Current.CurrentPage.DisplayAlert("Error", ex.Message, "OK");
    }
}

public async Task<RSALicenseDisc> GetBarCodeValues()
{
    try
    {
        var taskCompletionSource = new TaskCompletionSource<RSALicenseDisc>();
        await Shell.Current.Navigation.PushAsync(new BarCodeScanPage(taskCompletionSource));
        return await taskCompletionSource.Task;
    }
    catch (Exception ex)
    {
        await Shell.Current.CurrentPage.DisplayAlert("Error", ex.Message, "OK");
        return new RSALicenseDisc();
    }
}

Permission Configuration

Android

Add to AndroidManifest.xml:

<uses-permission android:name="android.permission.CAMERA" />

iOS

Add to Info.plist:

<key>NSCameraUsageDescription</key>
<string>Camera access is required to scan license disc barcodes</string>

Key Features

  1. Targeted Scanning: Focuses specifically on PDF-417 barcode format used by SA license discs
  2. Structured Data: Parses barcode data into a strongly-typed RSALicenseDisc object
  3. Validation: Includes validation to ensure essential fields are present
  4. Async Pattern: Uses TaskCompletionSource for clean async integration
  5. Error Handling: Comprehensive error handling throughout the scanning process

Sample Barcode Data Structure

South African license disc barcodes typically contain:

  • Vehicle registration number
  • License disc number
  • Vehicle make and model
  • Color
  • VIN and engine numbers
  • Expiry date
  • Additional control numbers and document identifiers

The data is separated by percentage signs (%) with the following structure:

%DocumentType%Unknown1%LicenseNum%Unknown2%ControlNum%LicenceNum%RegNum%Description%Make%Model%Color%VIN%EngineNum%ExpiryDate%

This implementation provides a complete solution for scanning and processing South African vehicle license discs in .NET MAUI applications.

.NET MAUI File Helper for reducing file sizes and converting PNG images to JPEG format.
using SkiaSharp;
using Microsoft.Maui.Graphics.Platform;

namespace Maui.Helpers
{
    public static class FileHelper
    {
        public static async Task<byte[]?> DownSizeFileStream(FileResult? file)
        {
            try
            {
                if (file == null)
                    return null;

                byte[]? result = null;

                using (Stream stream = await file.OpenReadAsync())
                {
                    if (stream.Length > Settings.FileMaxSizeBytes)
                    {
                        Microsoft.Maui.Graphics.IImage image = PlatformImage.FromStream(stream);
                        if (image != null && image.Width > Settings.FileMaxWidthResolution && image.Height > Settings.FileMaxHeightResolution)
                        {
                            Microsoft.Maui.Graphics.IImage newImage = image.Downsize(Settings.FileMaxWidthResolution, Settings.FileMaxHeightResolution, false);
                            result = await newImage.AsBytesAsync();
                        }
                        else
                        {
                            result = await image.AsBytesAsync();
                        }
                    }
                    else
                    {
                        result = await PlatformImage.FromStream(stream).AsBytesAsync();
                    }
                }

                return result;
            }
            catch (Exception ex)
            {
                await Shell.Current.CurrentPage.DisplayAlert(Constants.Error, ex.Message, Constants.Okay);
                return null;
            }
        }

        public static async Task<byte[]?> DownSizeFileStream(Stream? stream)
        {
            try
            {
                if (stream == null)
                    return null;

                byte[]? result = null;

                using (stream)
                {
                    if (stream.Length > Settings.FileMaxSizeBytes)
                    {
                        Microsoft.Maui.Graphics.IImage image = PlatformImage.FromStream(stream);
                        if (image != null && image.Width > Settings.FileMaxWidthResolution && image.Height > Settings.FileMaxHeightResolution)
                        {
                            Microsoft.Maui.Graphics.IImage newImage = image.Downsize(Settings.FileMaxWidthResolution, Settings.FileMaxHeightResolution, false);
                            result = await newImage.AsBytesAsync();
                        }
                        else
                        {
                            result = await image.AsBytesAsync();
                        }
                    }
                    else
                    {
                        result = await PlatformImage.FromStream(stream).AsBytesAsync();
                    }
                }

                return result;
            }
            catch (Exception ex)
            {
                await Shell.Current.CurrentPage.DisplayAlert(Constants.Error, ex.Message, Constants.Okay);
                return null;
            }
        }

        public static async Task<byte[]?> ConvertPngToJpegByteArray(Stream pngStream, float quality = 1f)
        {
            try
            {
                if (pngStream == null || pngStream.Length == 0)
                    return null;

                Microsoft.Maui.Graphics.IImage image = PlatformImage.FromStream(pngStream);
                if (image == null)
                    return null;

                MemoryStream jpegStream = new MemoryStream();
                await image.SaveAsync(jpegStream, ImageFormat.Jpeg, quality: quality);
                jpegStream.Position = 0;

                return jpegStream.ToArray();
            }
            catch (Exception ex)
            {
                await Shell.Current.CurrentPage.DisplayAlert(Constants.Error, ex.Message, Constants.Okay);
                return null;
            }
        }

        public static async Task<MemoryStream?> ConvertPngToJpegStream(Stream pngStream, float quality = 1f)
        {
            try
            {
                if (pngStream == null || pngStream.Length == 0)
                    return null;

                Microsoft.Maui.Graphics.IImage image = PlatformImage.FromStream(pngStream);
                if (image == null)
                    return null;

                MemoryStream jpegStream = new MemoryStream();
                await image.SaveAsync(jpegStream, ImageFormat.Jpeg, quality: quality);

                return jpegStream;
            }
            catch (Exception ex)
            {
                await Shell.Current.CurrentPage.DisplayAlert(Constants.Error, ex.Message, Constants.Okay);
                return null;
            }
        }

        public static async Task<byte[]?> ConvertPngToJpeg(FileResult? pngFile, float quality = 1f)
        {
            try
            {
                if (pngFile == null)
                    return null;

                Stream stream = await pngFile.OpenReadAsync();
                return await ConvertPngToJpegByteArray(stream, quality);
            }
            catch (Exception ex)
            {
                await Shell.Current.CurrentPage.DisplayAlert(Constants.Error, ex.Message, Constants.Okay);
                return null;
            }
        }

        public static async Task<MemoryStream?> DrawTextOnImageAsync(FileResult? imageFile, string[] textLines)
        {
            try
            {
                if (imageFile == null)
                    return null;

                using var stream = await imageFile.OpenReadAsync();
                using var originalBitmap = SKBitmap.Decode(stream);
                if (originalBitmap == null)
                    return null;

                int width = originalBitmap.Width;
                int height = originalBitmap.Height;

                var imageInfo = new SKImageInfo(width, height);
                using var surface = SKSurface.Create(imageInfo);
                SKCanvas canvas = surface.Canvas;

                canvas.Clear(SKColors.White);
                canvas.DrawBitmap(originalBitmap, 0, 0);

                float fontSize = 14f;
                float margin = 20f;
                float lineSpacing = 4f;

                using var font = new SKFont(SKTypeface.Default, fontSize)
                {
                    Edging = SKFontEdging.Antialias
                };

                using var fillPaint = new SKPaint
                {
                    Color = SKColors.White,
                    IsAntialias = true,
                    Style = SKPaintStyle.Fill
                };

                using var strokePaint = new SKPaint
                {
                    Color = SKColors.Black,
                    IsAntialias = true,
                    Style = SKPaintStyle.Stroke,
                    StrokeWidth = 2
                };

                float textY = height - margin;

                for (int i = textLines.Length - 1; i >= 0; i--)
                {
                    string line = textLines[i];
                    using var blob = SKTextBlob.Create(line, font);
                    var bounds = blob!.Bounds;
                    float textX = width - margin;

                    textY -= bounds.Height + lineSpacing;

                    canvas.DrawText(line, textX, textY, SKTextAlign.Right, font, strokePaint);
                    canvas.DrawText(line, textX, textY, SKTextAlign.Right, font, fillPaint);
                }

                canvas.Flush();

                using var image = surface.Snapshot();
                using var data = image.Encode(SKEncodedImageFormat.Png, 100);

                if (Settings.MediaSetting.Equals(true))
                {
                    var fileName = imageFile.FileName;
                    var filePath = Path.Combine(FileSystem.Current.AppDataDirectory, fileName);
                    using (var fileStream = File.OpenWrite(filePath))
                    {
                        data.SaveTo(fileStream);
                    }
                }

                var ms = new MemoryStream();
                data.SaveTo(ms);
                ms.Seek(0, SeekOrigin.Begin);

                return ms;
            }
            catch (Exception ex)
            {
                await Shell.Current.CurrentPage.DisplayAlert(Constants.Error, ex.Message, Constants.Okay);
                return null;
            }
        }
        
        public static async Task<MemoryStream?> DrawTextOnStreamAsync(Stream? stream, string[] textLines, string fileName = "")
        {
            try
            {
                if (stream == null)
                    return null;

                using var originalBitmap = SKBitmap.Decode(stream);
                if (originalBitmap == null)
                    return null;

                int width = originalBitmap.Width;
                int height = originalBitmap.Height;

                var imageInfo = new SKImageInfo(width, height);
                using var surface = SKSurface.Create(imageInfo);
                SKCanvas canvas = surface.Canvas;

                canvas.Clear(SKColors.White);
                canvas.DrawBitmap(originalBitmap, 0, 0);

                float fontSize = 14f;
                float margin = 20f;
                float lineSpacing = 4f;

                using var font = new SKFont(SKTypeface.Default, fontSize)
                {
                    Edging = SKFontEdging.Antialias
                };

                using var fillPaint = new SKPaint
                {
                    Color = SKColors.White,
                    IsAntialias = true,
                    Style = SKPaintStyle.Fill
                };

                using var strokePaint = new SKPaint
                {
                    Color = SKColors.Black,
                    IsAntialias = true,
                    Style = SKPaintStyle.Stroke,
                    StrokeWidth = 2
                };

                float textY = height - margin;

                for (int i = textLines.Length - 1; i >= 0; i--)
                {
                    string line = textLines[i];
                    using var blob = SKTextBlob.Create(line, font);
                    var bounds = blob!.Bounds;
                    float textX = width - margin;

                    textY -= bounds.Height + lineSpacing;

                    canvas.DrawText(line, textX, textY, SKTextAlign.Right, font, strokePaint);
                    canvas.DrawText(line, textX, textY, SKTextAlign.Right, font, fillPaint);
                }

                canvas.Flush();

                using var image = surface.Snapshot();
                using var data = image.Encode(SKEncodedImageFormat.Png, 100);

                if (Settings.MediaSetting.Equals(true) && string.IsNullOrEmpty(fileName).Equals(false))
                {
                    var filePath = Path.Combine(FileSystem.Current.AppDataDirectory, fileName);
                    using (var fileStream = File.OpenWrite(filePath))
                    {
                        data.SaveTo(fileStream);
                    }
                }

                var ms = new MemoryStream();
                data.SaveTo(ms);
                ms.Seek(0, SeekOrigin.Begin);

                return ms;
            }
            catch (Exception ex)
            {
                await Shell.Current.CurrentPage.DisplayAlert(Constants.Error, ex.Message, Constants.Okay);
                return null;
            }
        }
    }
}
.NET MAUI Image Save Helper for saving image files.
using System.Diagnostics;
using System.Net.Mime;

namespace Maui.Helpers
{
    public class ImageSaveHelper
    {
        private static string GetMimeTypeFromName(string fileName)
        {
            string extension = Path.GetExtension(fileName).ToLowerInvariant();

            return extension switch
            {
                ".txt" => MediaTypeNames.Text.Plain,
                ".html" or ".htm" => MediaTypeNames.Text.Html,
                ".css" => "text/css",
                ".js" => "application/javascript",
                ".json" => "application/json",
                ".xml" => MediaTypeNames.Text.Xml,
                ".png" => "image/png",
                ".jpg" or ".jpeg" => MediaTypeNames.Image.Jpeg,
                ".gif" => "image/gif",
                ".bmp" => "image/bmp",
                ".tiff" or ".tif" => "image/tiff",
                ".svg" => "image/svg+xml",
                ".mp3" => "audio/mpeg",
                ".wav" => "audio/wav",
                ".mp4" => "video/mp4",
                ".mpeg" or ".mpg" => "video/mpeg",
                ".avi" => "video/x-msvideo",
                ".pdf" => "application/pdf",
                ".zip" => "application/zip",
                ".doc" => "application/msword",
                ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                ".xls" => "application/vnd.ms-excel",
                ".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                ".ppt" => "application/vnd.ms-powerpoint",
                ".pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
                _ => "application/octet-stream",
            };
        }

        private static string GetMimeTypeFromPath(string filePath)
        {
            return GetMimeTypeFromName(Path.GetFileName(filePath));
        }

        public static Task SaveImage(byte[] bytes, string fileName)
        {
            try
            {
#if WINDOWS
                System.IO.File.WriteAllBytes(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), fileName), bytes);
#elif ANDROID
                var context = Platform.CurrentActivity;

                if (OperatingSystem.IsAndroidVersionAtLeast(29))
                {
                    Android.Content.ContentResolver resolver = context.ContentResolver;
                    Android.Content.ContentValues contentValues = new();
                    contentValues.Put(Android.Provider.MediaStore.IMediaColumns.DisplayName, fileName);
                    contentValues.Put(Android.Provider.MediaStore.IMediaColumns.MimeType, GetMimeTypeFromPath(fileName));
                    contentValues.Put(Android.Provider.MediaStore.IMediaColumns.RelativePath, string.Format("DCIM/{0}", AppInfo.Current.Name));
                    Android.Net.Uri imageUri = resolver.Insert(Android.Provider.MediaStore.Images.Media.ExternalContentUri, contentValues);
                    var os = resolver.OpenOutputStream(imageUri);
                    Android.Graphics.BitmapFactory.Options options = new();
                    options.InJustDecodeBounds = true;
                    MemoryStream memoryStream = new MemoryStream(bytes);
                    memoryStream.Position = 0;
                    var bitmap = Android.Graphics.BitmapFactory.DecodeStream(new MemoryStream(bytes));
                    bitmap.Compress(Android.Graphics.Bitmap.CompressFormat.Png, 100, os);
                    os.Flush();
                    os.Close();
                }
                else
                {
                    Java.IO.File storagePath = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryPictures);
                    string path = System.IO.Path.Combine(storagePath.ToString(), fileName);
                    System.IO.File.WriteAllBytes(path, bytes);
                    var mediaScanIntent = new Android.Content.Intent(Android.Content.Intent.ActionMediaScannerScanFile);
                    mediaScanIntent.SetData(Android.Net.Uri.FromFile(new Java.IO.File(path)));
                    context.SendBroadcast(mediaScanIntent);
                }
#elif IOS || MACCATALYST
                var image = new UIKit.UIImage(Foundation.NSData.FromArray(bytes));
                image.SaveToPhotosAlbum((image, error) =>
                {
                });
#endif
                return Task.CompletedTask;
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
                return Task.CompletedTask;
            }
        }
    }
}
.NET MAUI Community Toolkit Drawing View
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:mct="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             xmlns:viewModels="clr-namespace:.."
             x:Class="clr-namespace:..">
    <StackLayout Orientation="Vertical">
        <Border Stroke="Black" 
                Background="White"
                StrokeThickness="2"
                HeightRequest="105"
                WidthRequest="105">
            <mct:DrawingView x:Name="MctDrawing"
                IsMultiLineModeEnabled="True"
                ShouldClearOnFinish="False"
                Background="Transparent"
                BackgroundColor="Transparent"
                LineColor="Black"
                LineWidth="3"
                HeightRequest="100"
                WidthRequest="100"
                HorizontalOptions="Fill"
                VerticalOptions="Fill" />
        </Border>
        <Button Text="Clear" />
        <Button Text="Save"
                Command="{Binding Source={RelativeSource AncestorType={x:Type viewModels:..}}, Path=SaveDrawingCommand}"
                CommandParameter="{Binding Source={x:Reference MctDrawing}}">
        </Button>
    </StackLayout>
</ContentView>
.NET MAUI Displaying Enum Display Values.

To display the [Display(Name = "...")] attribute values of an enum in your .NET MAUI XAML page, you'll need to create a value converter. Here's how to implement this:

1. Create a Value Converter

First, create a converter that will transform the enum value to its display name:

using System.ComponentModel.DataAnnotations;
using System.Globalization;

public sealed class EnumDisplayConverter : IValueConverter
{
    public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        if (value is null)
        {
            return string.Empty;
        }

        if (!value.GetType().IsEnum)
        {
            return value.ToString() ?? string.Empty;
        }

        var fieldInfo = value.GetType().GetField(value.ToString() ?? string.Empty);
        if (fieldInfo is null)
        {
            return value.ToString() ?? string.Empty;
        }

        var displayAttribute = fieldInfo
            .GetCustomAttributes(typeof(DisplayAttribute), false)
            .FirstOrDefault() as DisplayAttribute;

        return displayAttribute?.Name ?? value.ToString() ?? string.Empty;
    }

    public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        return Activator.CreateInstance(targetType) ?? throw new InvalidOperationException("Cannot create default enum value");
    }
}

2. Add the Converter to Your Resources

In your XAML page or App.xaml, add the converter as a resource:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:YourNamespace"
             x:Class="YourNamespace.YourPage">
    
    <ContentPage.Resources>
        <ResourceDictionary>
            <local:EnumDisplayConverter x:Key="EnumDisplayConverter" />
        </ResourceDictionary>
    </ContentPage.Resources>
    
    <!-- Your content here -->
</ContentPage>

3. Use the Converter in Your XAML

Now you can bind to enum values and display their display names:

<Label Text="{Binding YourStatusProperty, Converter={StaticResource EnumDisplayConverter}}" />
.NET MAUI Application Insights

1. Setting Up on Azure Portal

Before integrating Application Insights into your application, create an Application Insights resource in the Azure portal:

  1. Navigate to the Azure portal:
    Open Azure Portal in your browser.

  2. Create a New Resource:
    Click on Create a resource. In the New window, search for "Application Insights".

  3. Select and Create:
    Select the Application Insights option, then click Create.
    Enter your desired settings (such as subscription, resource group, region, etc.), and press Review + Create, then click Create again.

  4. Save the Connection String:
    Once created, the Application Insights resource will generate a Connection String. This string is essential later when configuring your .NET MAUI application.


2. Install the ApplicationInsights SDK

To integrate Application Insights, you need the appropriate SDK in your .NET MAUI solution:

  • Using NuGet Package Manager:
    Open the NuGet Package Manager in Visual Studio, search for Microsoft.Extensions.Logging.ApplicationInsights, and install it in your solution.

  • Or add the package reference directly:
    Include the following in your project file:

    <PackageReference Include="Microsoft.Extensions.Logging.ApplicationInsights" Version="2.22.0-beta3" />

This package integrates Application Insights logging with .NET's logging framework.


3. Initialize Application Insights

Configure Application Insights in your .NET MAUI application using the connection string obtained from Azure. In your MauiProgram.cs (or equivalent startup file), add the following code:

builder.Logging.AddApplicationInsights(configuration =>
{
    // Add a custom telemetry initializer to attach additional context data
    configuration.TelemetryInitializers.Add(new ApplicationInitializer());
    
    // Set the connection string from your Azure Application Insights resource
    configuration.ConnectionString = "YOUR-CONNECTION-STRING";
}, options =>
{
    // Option to include scopes for richer logging context
    options.IncludeScopes = true;
});

Replace "YOUR-CONNECTION-STRING" with the actual connection string from your Azure resource.


4. Include Additional Properties

To enrich your telemetry data with context like session details and device information, implement the ITelemetryInitializer interface. Create a class like the one below:

public class ApplicationInitializer : ITelemetryInitializer
{
    public string SessionId { get; } = Guid.NewGuid().ToString();
    public string? DeviceOperationSystem { get; } = DeviceInfo.Current.Platform.ToString();
    public string DeviceOemName { get; } = DeviceInfo.Current.Manufacturer;
    public string DeviceModel { get; } = DeviceInfo.Current.Model;
    public string ComponentVersion { get; } = AppInfo.Current.VersionString;

    public void Initialize(ITelemetry telemetry)
    {
        telemetry.Context.Session.Id = SessionId;
        telemetry.Context.Device.OperatingSystem = DeviceOperationSystem;
        telemetry.Context.Device.OemName = DeviceOemName;
        telemetry.Context.Device.Model = DeviceModel;
        telemetry.Context.Component.Version = ComponentVersion;
    }
}

Explanation:
This initializer attaches details such as a unique session ID, device operating system, manufacturer, model, and application version to every telemetry event, enabling deeper diagnostics and analysis.


5. Track Events, Exceptions, and Dependencies

With Application Insights initialized, you can now effectively log events, exceptions, and dependencies throughout your application.

Tracking an Event

Here’s an example using the ILogger interface to log a custom event:

private readonly ILogger<MyService> logger;

public MyService(ILogger<MyService> logger)
{
    logger = logger;
}

public void MyAction(string parameter)
{
    // Log information when an action is executed
    logger.LogInformation("My action executed with parameter: {Parameter}", parameter);
}

Tracking an Exception

To log exceptions and capture error details:

try
{
    // Code that might throw an exception
}
catch (Exception ex)
{
    // Log the exception details using the logger
    logger.LogError(ex, "Something went wrong");
}
Fix SSH Not Working in macOS

If you’re running into a compatibility issue between Visual Studio’s SSH pairing tool and the updated SSH setup in macOS Sequoia. Even though Remote Login is enabled for all users, recent macOS releases (like Sequoia and Ventura) ship with newer builds of OpenSSH that disable legacy algorithms—most notably, RSA signatures using SHA‑1 are now off by default. This means that Visual Studio’s connection, which often expects those older protocols, might be rejected even when everything “looks” correctly configured on your Mac.

Here are some steps to try and resolve the issue:

  1. Verify Remote Login and Firewall Settings
    – Double‑check your Mac’s System Settings (or System Preferences) under Sharing to confirm that “Remote Login” is turned on and that it’s set to allow access for the correct user accounts.
    – Review your firewall settings to ensure that incoming connections on port 22 (the default SSH port) aren’t being blocked.

  2. Edit the SSH Daemon Configuration
    With the update to OpenSSH (likely something like OpenSSH_9.0p1 on Sequoia), you may need to explicitly allow the older algorithms that Visual Studio relies on.
    – Open Terminal on your Mac and run:

    sudo nano /etc/ssh/sshd_config

    – Add the following lines at the end of the file:

    HostkeyAlgorithms +ssh-rsa
    PubkeyAcceptedAlgorithms +ssh-rsa
    

    – Save the file (in nano, press Ctrl+O to write and Ctrl+X to exit).

  3. Restart the SSH Service
    To apply the changes, restart SSH. You can do this by unloading and loading the SSH daemon:

    sudo launchctl unload /System/Library/LaunchDaemons/ssh.plist
    sudo launchctl load -w /System/Library/LaunchDaemons/ssh.plist

    Alternatively, a full system reboot will also do the trick.

  4. Test the Connection
    Before trying Visual Studio again, use an SSH client from another machine (or from a Windows terminal if you have something like PuTTY or Windows Subsystem for Linux installed) to connect to your Mac:

    ssh your_username@your_mac_ip

    If you can log in without issues, that’s a good sign that the server side is now accepting the connection.

  5. Update Visual Studio / Workloads
    Microsoft has been releasing updates to address connectivity and compatibility issues with the latest macOS releases. Make sure your Visual Studio on Windows is updated to the latest version so that any improvements in its SSH handling are in place. In some cases, updating the relevant workload (for example, the Xamarin or MAUI build workloads) on your Windows machine has resolved similar issues.

This solution has worked for others encountering similar SSH connection errors after updating to macOS Sequoia or Ventura . In addition, if you still experience issues when pairing from Visual Studio, consider clearing any cached SSH host keys on your Windows machine or checking for network isolation between your Windows computer and the Mac.

Beyond these steps, if connectivity remains an issue, it might be worth exploring whether Visual Studio’s internal SSH client (or the way it invokes SSH for pairing) is misbehaving. Sometimes, reinstalling or repairing the Visual Studio installation (or the specific iOS build components) can help clear out stale settings that interfere with the connection.

Feel free to ask more if you run into other issues or need further detail on any of these steps!It sounds like you’re running into a compatibility issue between Visual Studio’s SSH pairing tool and the updated SSH setup in macOS Sequoia. Even though Remote Login is enabled for all users, recent macOS releases (like Sequoia and Ventura) ship with newer builds of OpenSSH that disable legacy algorithms—most notably, RSA signatures using SHA‑1 are now off by default. This means that Visual Studio’s connection, which often expects those older protocols, might be rejected even when everything “looks” correctly configured on your Mac.

Here are some steps to try and resolve the issue:

  1. Verify Remote Login and Firewall Settings
    – Double‑check your Mac’s System Settings (or System Preferences) under Sharing to confirm that “Remote Login” is turned on and that it’s set to allow access for the correct user accounts.
    – Review your firewall settings to ensure that incoming connections on port 22 (the default SSH port) aren’t being blocked.

  2. Edit the SSH Daemon Configuration
    With the update to OpenSSH (likely something like OpenSSH_9.0p1 on Sequoia), you may need to explicitly allow the older algorithms that Visual Studio relies on.
    – Open Terminal on your Mac and run:

    sudo nano /etc/ssh/sshd_config

    – Add the following lines at the end of the file:

    HostkeyAlgorithms +ssh-rsa
    PubkeyAcceptedAlgorithms +ssh-rsa
    

    – Save the file (in nano, press Ctrl+O to write and Ctrl+X to exit).

  3. Restart the SSH Service
    To apply the changes, restart SSH. You can do this by unloading and loading the SSH daemon:

    sudo launchctl unload /System/Library/LaunchDaemons/ssh.plist
    sudo launchctl load -w /System/Library/LaunchDaemons/ssh.plist

    Alternatively, a full system reboot will also do the trick.

  4. Test the Connection
    Before trying Visual Studio again, use an SSH client from another machine (or from a Windows terminal if you have something like PuTTY or Windows Subsystem for Linux installed) to connect to your Mac:

    ssh your_username@your_mac_ip

    If you can log in without issues, that’s a good sign that the server side is now accepting the connection.

  5. Update Visual Studio / Workloads
    Microsoft has been releasing updates to address connectivity and compatibility issues with the latest macOS releases. Make sure your Visual Studio on Windows is updated to the latest version so that any improvements in its SSH handling are in place. In some cases, updating the relevant workload (for example, the Xamarin or MAUI build workloads) on your Windows machine has resolved similar issues.

.NET MAUI Deployment

Android YML Deployment
  • Double click the Properties folder of your app, then you will open a page.
  • Select Android option on the left -> Options-> set Android package format to apk.
trigger:
  - main

variables:
  - group: versioning
  - group: mono
  - group: ios-signing
  - group: android-signing
  - name: BuildConfiguration
    value: Release
  - name: DotNetVersion
    value: 9.0.100
  - name: VSVERSION
    value: 17/pre

stages:
  - stage: BuildAndroid
    jobs:
      - job: BuildMAUIApps
        displayName: Build Weather21 App
        pool:
          vmImage: "windows-2022"
#         vmImage: "macOS-15"
          demands:
            - MSBuild

        steps:
#       - script: sudo xcode-select -s /Applications/Xcode_16.3.app
#         displayName: 'Select Xcode 16.3'

          - task: UseDotNet@2
            displayName: .NET Version
            inputs:
              packageType: "sdk"
              version: "$(DotNetVersion)"

          - task: Bash@3
            displayName: Install MAUI
            inputs:
              targetType: "inline"
              script: |
                dotnet nuget locals all --clear
                dotnet workload install maui --source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet7/nuget/v3/index.json --source https://api.nuget.org/v3/index.json
                dotnet workload install android ios maccatalyst tvos macos maui wasm-tools --source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet7/nuget/v3/index.json --source https://api.nuget.org/v3/index.json

          - task: Bash@3
            displayName: Restore nuget
            inputs:
              targetType: "inline"
              script: |
                cd 9.0/Apps/WeatherTwentyOne/src/
                dotnet restore WeatherTwentyOne.sln
              # cd 9.0/Apps/WeatherTwentyOne/src/WeatherTwentyOne
              # dotnet restore WeatherTwentyOne.csproj

          # https://docs.microsoft.com/en-us/dotnet/maui/android/deployment/overview
          - task: Bash@3
            displayName: Build Android App
            inputs:
              targetType: "inline"
              script: |
                cd 9.0/Apps/WeatherTwentyOne/src
                dotnet build -f net9.0-android -c Release
              # cd 9.0/Apps/WeatherTwentyOne/src/WeatherTwentyOne
              # dotnet build WeatherTwentyOne.csproj -f net9.0-android35.0 -c Release

          # Signing Android App : https://docs.microsoft.com/en-us/azure/devops/pipelines/apps/mobile/app-signing?view=azure-devops&tabs=apple-install-during-build#sign-your-android-app
          - task: AndroidSigning@3
            displayName: "Signing and aligning APK file(s) **/*.apk"
            inputs:
              apkFiles: "**/*.apk"
              apksign: true
              apksignerKeystoreFile: "android.keystore"
              apksignerKeystorePassword: $(keystore.password)
              apksignerKeystoreAlias: $(keystore.alias)
              apksignerKeyPassword: $(key.password)
              zipalign: true

          - task: Bash@3
            displayName: Rename Signed APK
            inputs:
              targetType: "inline"
              script: |
                cd 9.0/Apps/WeatherTwentyOne/src/WeatherTwentyOne/bin/droid/Release/net9.0-android/
                mv com.companyname.WeatherTwentyOne-Signed.apk com.companyname.WeatherTwentyOne.apk
              # cd 9.0/Apps/WeatherTwentyOne/src/WeatherTwentyOne/bin/droid/Release/net9.0-android35.0/

          - task: CopyFiles@2
            inputs:
              Contents: |
                **\**\9.0\Apps\WeatherTwentyOne\src\WeatherTwentyOne\bin\Release\net9.0-android\*.apk
                9.0\Apps\WeatherTwentyOne\src\WeatherTwentyOne\bin\Release\net9.0-android\*.apk
                **\**\9.0\Apps\WeatherTwentyOne\src\WeatherTwentyOne\bin\Release\net9.0-android35.0\*.apk
                9.0\Apps\WeatherTwentyOne\src\WeatherTwentyOne\bin\Release\net9.0-android35.0\*.apk
                **/*.apk
                **/*.ipa
              TargetFolder: "$(Build.ArtifactStagingDirectory)"

          - task: PublishBuildArtifacts@1
            inputs:
              PathtoPublish: "$(Build.ArtifactStagingDirectory)"
              ArtifactName: "drop_android"
              publishLocation: "Container"

Comments are disabled for this gist.