Shaunebu.MAUI.StateManager 1.0.0

dotnet add package Shaunebu.MAUI.StateManager --version 1.0.0
                    
NuGet\Install-Package Shaunebu.MAUI.StateManager -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Shaunebu.MAUI.StateManager" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Shaunebu.MAUI.StateManager" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Shaunebu.MAUI.StateManager" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Shaunebu.MAUI.StateManager --version 1.0.0
                    
#r "nuget: Shaunebu.MAUI.StateManager, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Shaunebu.MAUI.StateManager@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Shaunebu.MAUI.StateManager&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Shaunebu.MAUI.StateManager&version=1.0.0
                    
Install as a Cake Tool

Shaunebu.MAUI.StateManager 📗

A comprehensive UI state management library for .NET MAUI applications that provides a unified and consistent way to handle common UI states across your application. It eliminates repetitive boilerplate code and ensures a consistent user experience.

NuGet Version

NET Support Support

The Problem

Traditional MAUI applications often have repetitive and inconsistent state management code:

// ❌ Traditional repetitive code
IsBusy = true;
IsError = false;
IsEmpty = false;
// ... business logic ...
IsBusy = false;

The Solution

// ✅ With StateManager
using (_stateManager.LoadingScope("Loading users..."))
{
    var users = await userService.GetUsersAsync();
    _stateManager.CurrentState = users.Any() 
        ? UIState.Success() 
        : UIState.Empty("No users found");
}

🚀 Features

Core Features

  • Unified State Management: Centralized handling of Loading, Success, Empty, Error, Offline, and Custom states

  • Multi-State Support: Independent state management for different UI sections

  • Declarative API: Clean, fluent API for state transitions

  • MVVM Ready: Full compatibility with CommunityToolkit.MVVM

  • Customizable Templates: Fully customizable state templates

  • Global Configuration: Application-wide state configuration

Advanced Features

  • State Data & Parameters: Attach additional data to states

  • Automatic Retry Logic: Built-in retry mechanisms for error states

  • Minimum Loading Time: Prevent flickering with configurable loading durations

  • Network Awareness: Built-in offline state detection (extensible)

  • Analytics Ready: Hook for state change tracking

  • Localization Support: Easy integration with localization systems

📦 Installation

Package References

Add the following packages to your .NET MAUI project:

<PackageReference Include="Shaunebu.MAUI.StateManager" Version="1.0.0" />

Service Registration

In your 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");
            });

        // Basic registration
        builder.Services.AddStateManager();

        // Or with configuration
        builder.Services.AddStateManager(options =>
        {
            options.DefaultLoadingMessage = "Loading...";
            options.DefaultEmptyMessage = "No data available";
            options.DefaultErrorMessage = "An error occurred";
            options.PrimaryColor = Colors.Blue;
            options.MinimumLoadingTime = TimeSpan.FromMilliseconds(500);
            options.EnableLogging = true;
        });

        return builder.Build();
    }
}

🏃 Quick Start

1. Basic Usage in ViewModel

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Shaunebu.MAUI.StateManager;

public partial class UserViewModel : ObservableObject
{
    private readonly IStateManager _stateManager;
    private readonly IUserService _userService;

    [ObservableProperty]
    private List<User> _users = new();

    public IStateManager StateManager => _stateManager;

    public UserViewModel(IStateManager stateManager, IUserService userService)
    {
        _stateManager = stateManager;
        _userService = userService;
    }

    [RelayCommand]
    private async Task LoadUsersAsync()
    {
        try
        {
            using (_stateManager.LoadingScope("Loading users..."))
            {
                var users = await _userService.GetUsersAsync();
                Users = users;
                
                if (!users.Any())
                {
                    _stateManager.CurrentState = UIState.Empty("No users found", "📭");
                }
            }
        }
        catch (Exception ex)
        {
            _stateManager.CurrentState = UIState.Error(ex.Message, showRetry: true);
        }
    }
}

2. Basic Usage in 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:sm="clr-namespace:Shaunebu.MAUI.StateManager.Controls;assembly=Shaunebu.MAUI.StateManager"
             x:Class="MyApp.MainPage">
    
    <Grid>
        
        <CollectionView ItemsSource="{Binding Users}" 
                       IsVisible="{Binding StateManager.CurrentState.IsSuccess}" />
        
        
        <sm:StateOverlayView State="{Binding StateManager.CurrentState}" 
                           ZIndex="1000" />
    </Grid>
</ContentPage>

🎯 Core Concepts

UIState Class

The UIState class represents the current state of your UI with the following properties:

public partial class UIState : ObservableObject
{
    public StateType CurrentState { get; }      // Loading, Success, Empty, Error, Offline, Custom
    public string Message { get; }              // Contextual message
    public string Image { get; }                // Icon/illustration
    public bool ShowRetry { get; }              // Show retry button
    public Action RetryAction { get; }          // Retry action
    public object Data { get; }                 // Additional data
    public Dictionary<string, object> Parameters { get; } // Dynamic parameters
}

StateType Enum

public enum StateType
{
    Loading,    // Data loading
    Success,    // Data loaded successfully
    Empty,      // No data to display
    Error,      // Operation error
    Offline,    // No internet connection
    NoResults,  // Search without results
    Custom      // Custom state
}

Available States

Loading State
// Basic loading
_stateManager.CurrentState = UIState.Loading();

// Custom message
_stateManager.CurrentState = UIState.Loading("Loading users...");

// With data and parameters
_stateManager.CurrentState = UIState.Loading(
    "Processing your request", 
    data: processingData,
    parameters: new Dictionary<string, object> { ["OperationId"] = Guid.NewGuid() }
);
Success State
// Basic success
_stateManager.CurrentState = UIState.Success();

// With data
_stateManager.CurrentState = UIState.Success(data: "Operation completed successfully");

// With parameters
_stateManager.CurrentState = UIState.Success(
    parameters: new Dictionary<string, object> 
    { 
        ["ProcessedItems"] = 42,
        ["Timestamp"] = DateTime.Now
    }
);
Empty State
// Basic empty
_stateManager.CurrentState = UIState.Empty();

// Custom message and image
_stateManager.CurrentState = UIState.Empty("No users found", "👥");

// With retry action
_stateManager.CurrentState = UIState.Empty(
    "No data available", 
    showRetry: true, 
    onRetry: () => LoadDataCommand.Execute(null)
);
Error State
// Basic error
_stateManager.CurrentState = UIState.Error("Something went wrong");

// With retry
_stateManager.CurrentState = UIState.Error(
    "Network error", 
    showRetry: true, 
    onRetry: () => RetryOperation()
);

// With custom data
_stateManager.CurrentState = UIState.Error(
    "Validation failed",
    parameters: new Dictionary<string, object>
    {
        ["ErrorCode"] = "VALIDATION_001",
        ["FieldErrors"] = new List<string> { "Email is required" }
    }
);
Offline State
// Basic offline
_stateManager.CurrentState = UIState.Offline();

// Custom message
_stateManager.CurrentState = UIState.Offline("You appear to be offline");

// With retry and data
_stateManager.CurrentState = UIState.Offline(
    "No internet connection",
    data: lastSyncTime,
    parameters: new Dictionary<string, object> { ["LastSync"] = DateTime.Now.AddHours(-1) }
);
Custom State
// Basic custom
_stateManager.CurrentState = UIState.Custom("Maintenance in progress");

// With image and retry
_stateManager.CurrentState = UIState.Custom(
    "Feature coming soon!", 
    "🚧", 
    showRetry: true,
    onRetry: () => CheckForUpdates()
);

// Full custom state
_stateManager.CurrentState = UIState.Custom(
    "System maintenance",
    "🔧",
    showRetry: true,
    onRetry: () => RefreshApp(),
    data: maintenanceSchedule,
    parameters: new Dictionary<string, object>
    {
        ["MaintenanceEnd"] = DateTime.Now.AddHours(2),
        ["AffectedServices"] = new List<string> { "User Profiles", "Notifications" }
    }
);

📚 API Reference

IStateManager Interface

public interface IStateManager
{
    // Properties
    UIState CurrentState { get; set; }
    event EventHandler<UIState> StateChanged;
    
    // Methods
    IDisposable LoadingScope(string message = "Loading...");
    Task ExecuteWithStateAsync(Func<Task> operation, string loadingMessage = null);
}

IMultiStateManager Interface

public interface IMultiStateManager
{
    // Indexer for quick access
    IStateManager this[string key] { get; }
    
    // Methods
    IStateManager GetManager(string key);
    IStateManager GetOrCreateManager(string key);
    bool RemoveManager(string key);
    IEnumerable<string> GetManagerKeys();
    void ClearAllManagers();
    
    // Events
    event EventHandler<ManagerChangedEventArgs> ManagerChanged;
}

Extension Methods

public static class StateManagerExtensions
{
    // Execute operation with automatic state handling
    public static async Task ExecuteWithStateAsync(
        this IStateManager stateManager,
        Func<Task> operation,
        string loadingMessage = null);
    
    // Execute operation with return value
    public static async Task<T> ExecuteWithStateAsync<T>(
        this IStateManager stateManager,
        Func<Task<T>> operation,
        string loadingMessage = null);
    
    // Loading scope for using statements
    public static IDisposable LoadingScope(
        this IStateManager stateManager, 
        string message = "Loading...");
    
    // Success scope
    public static IDisposable SuccessScope(
        this IStateManager stateManager, 
        string message = "");
    
    // Temporary success state
    public static void SetTemporarySuccess(
        this IStateManager stateManager, 
        string message, 
        TimeSpan duration);
}

Controls

StateContainer

A content view that displays different templates based on the current state. Properties:

  • CurrentState: The current UIState to display

  • SuccessContent: Content to show when state is Success

  • Templates: Collection of StateDataTemplate for custom state rendering

Usage:

<sm:StateContainer CurrentState="{Binding CurrentState}">
    <sm:StateContainer.SuccessContent>
        <Label Text="This is success content!" />
    </sm:StateContainer.SuccessContent>
    
    <sm:StateContainer.Templates>
        <sm:StateDataTemplate StateType="Loading">
            <DataTemplate>
                <VerticalStackLayout>
                    <ActivityIndicator IsRunning="True" />
                    <Label Text="{Binding Message}" />
                </VerticalStackLayout>
            </DataTemplate>
        </sm:StateDataTemplate>
    </sm:StateContainer.Templates>
</sm:StateContainer>
StateOverlayView

A full-screen overlay that displays state information. Properties:

  • State: The UIState to display

  • Automatically handles visibility based on state

Usage:

<sm:StateOverlayView State="{Binding CurrentState}" ZIndex="1000" />

🚀 Advanced Usage

Multi-State Management

Manage independent states for different UI sections:

public class DashboardViewModel : ObservableObject
{
    private readonly IMultiStateManager _multiStateManager;
    
    // Different state managers for different sections
    public IStateManager HeaderState => _multiStateManager["Header"];
    public IStateManager ContentState => _multiStateManager["Content"];
    public IStateManager SidebarState => _multiStateManager["Sidebar"];
    public IStateManager FooterState => _multiStateManager["Footer"];
    
    public DashboardViewModel(IMultiStateManager multiStateManager)
    {
        _multiStateManager = multiStateManager;
    }
    
    [RelayCommand]
    private async Task LoadDashboardAsync()
    {
        // Header can show progress while content loads
        HeaderState.CurrentState = UIState.Loading("Loading dashboard...");
        
        // Content loads independently
        await ContentState.ExecuteWithStateAsync(async () =>
        {
            var data = await dashboardService.GetDataAsync();
            // Process data...
        }, "Loading content...");
        
        // Sidebar loads separately
        await SidebarState.ExecuteWithStateAsync(async () =>
        {
            var sidebarData = await sidebarService.GetDataAsync();
            // Process sidebar data...
        }, "Loading sidebar...");
        
        HeaderState.CurrentState = UIState.Success("Dashboard ready!");
    }
}

State with Data and Parameters

public class AdvancedViewModel : ObservableObject
{
    private readonly IStateManager _stateManager;
    
    public AdvancedViewModel(IStateManager stateManager)
    {
        _stateManager = stateManager;
    }
    
    [RelayCommand]
    private async Task ProcessDataAsync()
    {
        var processId = Guid.NewGuid();
        
        using (_stateManager.LoadingScope("Processing data...", 
               parameters: new Dictionary<string, object> { ["ProcessId"] = processId }))
        {
            try
            {
                var result = await dataService.ProcessAsync();
                
                _stateManager.CurrentState = UIState.Success(
                    data: result,
                    parameters: new Dictionary<string, object>
                    {
                        ["ProcessedItems"] = result.Items.Count,
                        ["ProcessingTime"] = result.Duration,
                        ["ProcessId"] = processId
                    }
                );
                
                // Access data later
                var processedData = _stateManager.CurrentState.GetData<ProcessResult>();
                var itemCount = _stateManager.CurrentState.GetParameter<int>("ProcessedItems");
            }
            catch (Exception ex)
            {
                _stateManager.CurrentState = UIState.Error(
                    "Processing failed",
                    showRetry: true,
                    onRetry: () => ProcessDataAsync(),
                    parameters: new Dictionary<string, object>
                    {
                        ["ErrorCode"] = ex.HResult,
                        ["ErrorType"] = ex.GetType().Name,
                        ["ProcessId"] = processId
                    }
                );
            }
        }
    }
}

Custom State Templates

Create custom templates for different states:

<sm:StateContainer CurrentState="{Binding CurrentState}">
    <sm:StateContainer.SuccessContent>
        
        <CollectionView ItemsSource="{Binding Items}" />
    </sm:StateContainer.SuccessContent>
    
    <sm:StateContainer.Templates>
        
        <sm:StateDataTemplate StateType="Loading">
            <DataTemplate>
                <Frame BackgroundColor="White" Padding="20">
                    <VerticalStackLayout Spacing="15">
                        <ActivityIndicator Color="{StaticResource PrimaryColor}" 
                                         IsRunning="True" 
                                         Scale="1.5" />
                        <Label Text="{Binding Message}" 
                               HorizontalOptions="Center"
                               TextColor="{StaticResource PrimaryColor}" />
                        <ProgressBar Progress="0.5" />
                    </VerticalStackLayout>
                </Frame>
            </DataTemplate>
        </sm:StateDataTemplate>
        
        
        <sm:StateDataTemplate StateType="Error">
            <DataTemplate>
                <Frame BackgroundColor="#FFF5F5" Padding="20">
                    <VerticalStackLayout Spacing="15">
                        <Label Text="🚨" FontSize="40" HorizontalOptions="Center" />
                        <Label Text="{Binding Message}" 
                               HorizontalOptions="Center"
                               TextColor="#D32F2F" />
                        <Button Text="Try Again" 
                                Command="{Binding RetryAction, Converter={x:StaticResource ActionToCommandConverter}}"
                                BackgroundColor="#D32F2F"
                                TextColor="White" />
                    </VerticalStackLayout>
                </Frame>
            </DataTemplate>
        </sm:StateDataTemplate>
        
        
        <sm:StateDataTemplate StateType="Empty">
            <DataTemplate>
                <Frame BackgroundColor="#F3F4F6" Padding="20">
                    <VerticalStackLayout Spacing="15">
                        <Label Text="📭" FontSize="40" HorizontalOptions="Center" />
                        <Label Text="{Binding Message}" 
                               HorizontalOptions="Center"
                               TextColor="#6B7280" />
                        <Button Text="Add New Item" 
                                Command="{Binding Source={RelativeSource AncestorType={x:Type local:MyPage}}, Path=BindingContext.AddItemCommand}"
                                BackgroundColor="{StaticResource PrimaryColor}"
                                TextColor="White" />
                    </VerticalStackLayout>
                </Frame>
            </DataTemplate>
        </sm:StateDataTemplate>
    </sm:StateContainer.Templates>
</sm:StateContainer>

Global Configuration

Configure default behaviors application-wide:

builder.Services.AddStateManager(options =>
{
    // Default messages
    options.DefaultLoadingMessage = "Please wait...";
    options.DefaultEmptyMessage = "Nothing to display";
    options.DefaultErrorMessage = "Something went wrong";
    options.DefaultOfflineMessage = "You're offline";
    options.DefaultNoResultsMessage = "No results found";
    
    // Colors
    options.PrimaryColor = Color.FromArgb("#512BD4");
    options.SecondaryColor = Colors.Gray;
    options.TextColor = Colors.Gray;
    
    // Default templates
    options.LoadingTemplate = new DataTemplate(() => new CustomLoadingView());
    options.ErrorTemplate = new DataTemplate(() => new CustomErrorView());
    
    // Behavior
    options.MinimumLoadingTime = TimeSpan.FromSeconds(1);
    options.EnableLogging = Debugger.IsAttached;
    
    // Default icons
    options.DefaultLoadingIcon = "⏳";
    options.DefaultEmptyIcon = "📭";
    options.DefaultErrorIcon = "❌";
    options.DefaultOfflineIcon = "📶";
    options.DefaultNoResultsIcon = "🔍";
    options.DefaultCustomIcon = "⚙️";
});

🎨 Customization

Custom State Manager

Create custom state managers for specialized behavior:

public class LoggingStateManager : IStateManager
{
    private readonly IStateManager _innerStateManager;
    private readonly ILogger<LoggingStateManager> _logger;
    
    public LoggingStateManager(IStateManager stateManager, ILogger<LoggingStateManager> logger)
    {
        _innerStateManager = stateManager;
        _logger = logger;
        
        _innerStateManager.StateChanged += OnStateChanged;
    }
    
    public UIState CurrentState 
    { 
        get => _innerStateManager.CurrentState; 
        set => _innerStateManager.CurrentState = value; 
    }
    
    public event EventHandler<UIState> StateChanged
    {
        add => _innerStateManager.StateChanged += value;
        remove => _innerStateManager.StateChanged -= value;
    }
    
    private void OnStateChanged(object sender, UIState state)
    {
        _logger.LogInformation("UI State changed to {StateType}: {Message}", 
            state.CurrentState, state.Message);
    }
    
    public IDisposable LoadingScope(string message = "Loading...")
    {
        _logger.LogInformation("Starting loading scope: {Message}", message);
        return _innerStateManager.LoadingScope(message);
    }
    
    public async Task ExecuteWithStateAsync(Func<Task> operation, string loadingMessage = null)
    {
        _logger.LogInformation("Executing operation with state: {Message}", loadingMessage);
        await _innerStateManager.ExecuteWithStateAsync(operation, loadingMessage);
    }
}

// Registration
builder.Services.AddStateManager<LoggingStateManager>();

Network-Aware State Manager

public class NetworkAwareStateManager : IStateManager
{
    private readonly IStateManager _innerStateManager;
    private readonly IConnectivity _connectivity;
    private UIState _previousState;
    
    public NetworkAwareStateManager(IStateManager stateManager, IConnectivity connectivity)
    {
        _innerStateManager = stateManager;
        _connectivity = connectivity;
        
        connectivity.ConnectivityChanged += OnConnectivityChanged;
        CheckInitialConnectivity();
    }
    
    public UIState CurrentState 
    { 
        get => _innerStateManager.CurrentState; 
        set
        {
            _previousState = _innerStateManager.CurrentState;
            _innerStateManager.CurrentState = value;
        }
    }
    
    public event EventHandler<UIState> StateChanged
    {
        add => _innerStateManager.StateChanged += value;
        remove => _innerStateManager.StateChanged -= value;
    }
    
    private void CheckInitialConnectivity()
    {
        if (_connectivity.NetworkAccess != NetworkAccess.Internet)
        {
            CurrentState = UIState.Offline();
        }
    }
    
    private void OnConnectivityChanged(object sender, ConnectivityChangedEventArgs e)
    {
        if (e.NetworkAccess != NetworkAccess.Internet)
        {
            _previousState = CurrentState;
            CurrentState = UIState.Offline();
        }
        else if (CurrentState.CurrentState == StateType.Offline)
        {
            CurrentState = _previousState ?? UIState.Success();
        }
    }
    
    public IDisposable LoadingScope(string message = "Loading...")
    {
        return _innerStateManager.LoadingScope(message);
    }
    
    public async Task ExecuteWithStateAsync(Func<Task> operation, string loadingMessage = null)
    {
        if (_connectivity.NetworkAccess != NetworkAccess.Internet)
        {
            CurrentState = UIState.Offline();
            return;
        }
        
        await _innerStateManager.ExecuteWithStateAsync(operation, loadingMessage);
    }
    
    public void Dispose()
    {
        _connectivity.ConnectivityChanged -= OnConnectivityChanged;
    }
}

📋 Best Practices

1. State Management Patterns

Use LoadingScope for Automatic Cleanup
// ✅ Good - automatic state cleanup
using (_stateManager.LoadingScope("Loading data..."))
{
    var data = await service.GetDataAsync();
    // State automatically returns to Success
}

// ❌ Avoid - manual state management
try
{
    _stateManager.CurrentState = UIState.Loading("Loading data...");
    var data = await service.GetDataAsync();
    _stateManager.CurrentState = UIState.Success();
}
catch (Exception ex)
{
    _stateManager.CurrentState = UIState.Error(ex.Message);
}
Use ExecuteWithStateAsync for Simple Operations
// ✅ Good - concise and safe
await _stateManager.ExecuteWithStateAsync(async () =>
{
    await service.ProcessDataAsync();
}, "Processing data...");

// ❌ Avoid - repetitive error handling
try
{
    _stateManager.CurrentState = UIState.Loading("Processing data...");
    await service.ProcessDataAsync();
    _stateManager.CurrentState = UIState.Success();
}
catch (Exception ex)
{
    _stateManager.CurrentState = UIState.Error(ex.Message);
}

2. Multi-State Organization

Logical Section Separation
public class ComplexPageViewModel
{
    private readonly IMultiStateManager _multiState;
    
    // Organize by UI sections
    public IStateManager HeaderState => _multiState["Header"];
    public IStateManager MainContentState => _multiState["MainContent"];
    public IStateManager SidebarState => _multiState["Sidebar"];
    public IStateManager ActionsState => _multiState["Actions"];
    
    // Organize by functionality
    public IStateManager DataState => _multiState["Data"];
    public IStateManager FormState => _multiState["Form"];
    public IStateManager UploadState => _multiState["Upload"];
}

3. Error Handling

Provide Meaningful Error Messages
// ✅ Good - specific and actionable
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
    _stateManager.CurrentState = UIState.Error(
        "Resource not found. Please check the URL and try again.",
        showRetry: true,
        onRetry: () => LoadDataCommand.Execute(null)
    );
}

catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
    _stateManager.CurrentState = UIState.Error(
        "Authentication required. Please log in again.",
        showRetry: false
    );
    
    // Navigate to login
    await Shell.Current.GoToAsync("//login");
}

// ❌ Avoid - generic error messages
catch (Exception ex)
{
    _stateManager.CurrentState = UIState.Error("An error occurred");
}

4. Performance Considerations

Use MinimumLoadingTime to Prevent Flickering
builder.Services.AddStateManager(options =>
{
    options.MinimumLoadingTime = TimeSpan.FromMilliseconds(500);
});

// For fast operations, this ensures users see the loading state
using (_stateManager.LoadingScope("Processing..."))
{
    await fastOperation(); // Might complete in 100ms
    // Loading state will show for at least 500ms
}
Debounce Rapid State Changes
public class DebouncedStateManager : IStateManager
{
    private readonly IStateManager _innerStateManager;
    private readonly TimeSpan _debounceTime;
    private CancellationTokenSource _debounceCts;
    
    public DebouncedStateManager(IStateManager stateManager, TimeSpan debounceTime)
    {
        _innerStateManager = stateManager;
        _debounceTime = debounceTime;
    }
    
    public UIState CurrentState 
    { 
        get => _innerStateManager.CurrentState; 
        set => DebouncedSetState(value); 
    }
    
    private async void DebouncedSetState(UIState state)
    {
        _debounceCts?.Cancel();
        _debounceCts = new CancellationTokenSource();
        
        try
        {
            await Task.Delay(_debounceTime, _debounceCts.Token);
            _innerStateManager.CurrentState = state;
        }
        catch (TaskCanceledException)
        {
            // Debounce was cancelled - new state change occurred
        }
    }
    
    // ... rest of implementation
}

🔧 Troubleshooting

Common Issues

1. State Not Updating in UI

Problem: State changes in ViewModel but UI doesn't update. Solution: Ensure your ViewModel implements INotifyPropertyChanged and state properties use observable properties.

// ✅ Correct
public partial class MyViewModel : ObservableObject
{
    private readonly IStateManager _stateManager;
    public IStateManager StateManager => _stateManager;
}

// ❌ Incorrect - no property change notification
public class MyViewModel
{
    private readonly IStateManager _stateManager;
    public IStateManager StateManager => _stateManager;
}
2. Multiple StateOverlayViews Interfering

Problem: Multiple overlays showing at same time with wrong z-order. Solution: Use proper ZIndex values and consider using MultiStateManager for independent sections.


<sm:StateOverlayView State="{Binding StateManager.CurrentState}" ZIndex="2000" />


<sm:StateOverlayView State="{Binding ListState.CurrentState}" ZIndex="1000" />
3. Retry Action Not Working

Problem: Retry button in error state doesn't execute the action. Solution: Ensure the retry action is properly converted to a command.

<ContentPage.Resources>
    <converters:ActionToCommandConverter x:Key="ActionToCommandConverter" />
</ContentPage.Resources>


<Button Command="{Binding RetryAction, Converter={x:StaticResource ActionToCommandConverter}}" />

Debugging Tips

Enable State Logging
builder.Services.AddStateManager(options =>
{
    options.EnableLogging = true;
});
Add State Debug Information
<Border BackgroundColor="LightYellow" Padding="5">
    <VerticalStackLayout>
        <Label Text="Debug State Info" FontAttributes="Bold" />
        <Label Text="{Binding StateManager.CurrentState.CurrentState}" />
        <Label Text="{Binding StateManager.CurrentState.Message}" />
        <Label Text="{Binding StateManager.CurrentState.Data}" />
    </VerticalStackLayout>
</Border>

📱 Examples

Complete Example: User Management App

ViewModel
public partial class UserManagementViewModel : ObservableObject
{
    private readonly IMultiStateManager _multiStateManager;
    private readonly IUserService _userService;
    
    [ObservableProperty]
    private List<User> _users = new();
    
    [ObservableProperty]
    private User _selectedUser;
    
    // State managers for different sections
    public IStateManager GlobalState => _multiStateManager["Global"];
    public IStateManager ListState => _multiStateManager["List"];
    public IStateManager FormState => _multiStateManager["Form"];
    public IStateManager ActionsState => _multiStateManager["Actions"];
    
    public UserManagementViewModel(IMultiStateManager multiStateManager, IUserService userService)
    {
        _multiStateManager = multiStateManager;
        _userService = userService;
        
        InitializeStates();
        LoadUsersCommand.ExecuteAsync(null);
    }
    
    private void InitializeStates()
    {
        GlobalState.CurrentState = UIState.Loading("Initializing user management...");
        ListState.CurrentState = UIState.Loading("Loading users...");
        FormState.CurrentState = UIState.Success();
        ActionsState.CurrentState = UIState.Success();
    }
    
    [RelayCommand]
    private async Task LoadUsersAsync()
    {
        try
        {
            using (ListState.LoadingScope("Fetching users..."))
            {
                await Task.Delay(1000); // Simulate API call
                var users = await _userService.GetUsersAsync();
                Users = users;
                
                if (users.Any())
                {
                    GlobalState.CurrentState = UIState.Success(data: $"{users.Count} users loaded");
                }
                else
                {
                    ListState.CurrentState = UIState.Empty(
                        "No users found", 
                        "👥",
                        showRetry: true,
                        onRetry: () => LoadUsersCommand.Execute(null)
                    );
                }
            }
        }
        catch (Exception ex)
        {
            GlobalState.CurrentState = UIState.Error("Failed to load users");
            ListState.CurrentState = UIState.Error(
                ex.Message,
                showRetry: true,
                onRetry: () => LoadUsersCommand.Execute(null)
            );
        }
    }
    
    [RelayCommand]
    private async Task SaveUserAsync(User user)
    {
        if (user == null) return;
        
        try
        {
            using (FormState.LoadingScope("Saving user..."))
            {
                await Task.Delay(500); // Simulate API call
                await _userService.SaveUserAsync(user);
                
                FormState.CurrentState = UIState.Success(data: "User saved successfully");
                ActionsState.CurrentState = UIState.Custom("Changes saved", "✅");
                
                // Brief success message
                await Task.Delay(2000);
                ActionsState.CurrentState = UIState.Success();
            }
        }
        catch (Exception ex)
        {
            FormState.CurrentState = UIState.Error(
                "Failed to save user",
                showRetry: true,
                onRetry: () => SaveUserCommand.Execute(user)
            );
        }
    }
    
    [RelayCommand]
    private void AddNewUser()
    {
        SelectedUser = new User();
        FormState.CurrentState = UIState.Custom("Creating new user", "➕");
    }
    
    [RelayCommand]
    private async Task DeleteUserAsync(User user)
    {
        if (user == null) return;
        
        bool confirm = await Application.Current.MainPage.DisplayAlert(
            "Confirm Delete",
            $"Are you sure you want to delete {user.Name}?",
            "Delete",
            "Cancel");
            
        if (!confirm) return;
        
        try
        {
            using (ActionsState.LoadingScope("Deleting user..."))
            {
                await Task.Delay(500); // Simulate API call
                await _userService.DeleteUserAsync(user.Id);
                Users.Remove(user);
                
                ActionsState.CurrentState = UIState.Success(data: "User deleted");
                ListState.CurrentState = UIState.Success(data: $"{Users.Count} users remaining");
            }
        }
        catch (Exception ex)
        {
            ActionsState.CurrentState = UIState.Error(
                "Failed to delete user",
                showRetry: true,
                onRetry: () => DeleteUserCommand.Execute(user)
            );
        }
    }
}
XAML View
<?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:sm="clr-namespace:Shaunebu.MAUI.StateManager.Controls;assembly=Shaunebu.MAUI.StateManager"
             x:Class="MyApp.UserManagementPage"
             Title="User Management">
    
    <Grid RowDefinitions="Auto,*,Auto,Auto">
        
        
        <Border Grid.Row="0" BackgroundColor="#512BD4" Padding="15">
            <Grid ColumnDefinitions="*,Auto">
                <VerticalStackLayout Grid.Column="0">
                    <Label Text="User Management" 
                           FontSize="20" 
                           FontAttributes="Bold" 
                           TextColor="White" />
                    <Label Text="{Binding GlobalState.CurrentState.Data, StringFormat='{0}'}"
                           FontSize="12"
                           TextColor="White"
                           Opacity="0.8" />
                </VerticalStackLayout>
                
                <sm:StateContainer Grid.Column="1"
                                 CurrentState="{Binding GlobalState.CurrentState}"
                                 HorizontalOptions="End">
                    <sm:StateContainer.SuccessContent>
                        <Label Text="✅" FontSize="16" TextColor="White" />
                    </sm:StateContainer.SuccessContent>
                </sm:StateContainer>
            </Grid>
        </Border>
        
        
        <Grid Grid.Row="1" ColumnDefinitions="*,300">
            
            
            <Grid Grid.Column="0" Margin="10">
                
                
                <CollectionView ItemsSource="{Binding Users}"
                              IsVisible="{Binding ListState.CurrentState.IsSuccess}"
                              SelectedItem="{Binding SelectedUser}"
                              SelectionMode="Single">
                    <CollectionView.ItemTemplate>
                        <DataTemplate>
                            <Grid Padding="10" ColumnDefinitions="Auto,*,Auto">
                                <Label Grid.Column="0" Text="{Binding Avatar}" FontSize="20" />
                                <VerticalStackLayout Grid.Column="1">
                                    <Label Text="{Binding Name}" FontAttributes="Bold" />
                                    <Label Text="{Binding Email}" FontSize="12" TextColor="Gray" />
                                </VerticalStackLayout>
                                <Button Grid.Column="2" 
                                        Text="🗑️" 
                                        Command="{Binding Source={RelativeSource AncestorType={x:Type local:UserManagementPage}}, Path=BindingContext.DeleteUserCommand}"
                                        CommandParameter="{Binding .}"
                                        BackgroundColor="Transparent" />
                            </Grid>
                        </DataTemplate>
                    </CollectionView.ItemTemplate>
                </CollectionView>
                
                
                <sm:StateOverlayView State="{Binding ListState.CurrentState}" />
                
            </Grid>
            
            
            <Border Grid.Column="1" BackgroundColor="#F8F9FA" Padding="15">
                <Grid RowDefinitions="Auto,*,Auto">
                    
                    <Label Grid.Row="0" Text="User Details" FontSize="16" FontAttributes="Bold" />
                    
                    
                    <VerticalStackLayout Grid.Row="1" Spacing="10">
                        <Entry Placeholder="Name" Text="{Binding SelectedUser.Name}" />
                        <Entry Placeholder="Email" Text="{Binding SelectedUser.Email}" />
                        <Entry Placeholder="Phone" Text="{Binding SelectedUser.Phone}" />
                    </VerticalStackLayout>
                    
                    
                    <sm:StateContainer Grid.Row="1"
                                     CurrentState="{Binding FormState.CurrentState}"
                                     VerticalOptions="Center">
                        <sm:StateContainer.SuccessContent>
                            
                        </sm:StateContainer.SuccessContent>
                        <sm:StateContainer.Templates>
                            <sm:StateDataTemplate StateType="Loading">
                                <DataTemplate>
                                    <ActivityIndicator IsRunning="True" HorizontalOptions="Center" />
                                </DataTemplate>
                            </sm:StateDataTemplate>
                        </sm:StateContainer.Templates>
                    </sm:StateContainer>
                    
                    
                    <VerticalStackLayout Grid.Row="2" Spacing="10">
                        <Button Text="Save User" 
                                Command="{Binding SaveUserCommand}"
                                CommandParameter="{Binding SelectedUser}"
                                BackgroundColor="#512BD4"
                                TextColor="White" />
                                
                        <Button Text="New User" 
                                Command="{Binding AddNewUserCommand}"
                                BackgroundColor="#6C757D"
                                TextColor="White" />
                    </VerticalStackLayout>
                    
                </Grid>
            </Border>
            
            
            <sm:StateOverlayView State="{Binding GlobalState.CurrentState}" 
                               Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
                               ZIndex="2000" />
                               
        </Grid>
        
        
        <Border Grid.Row="2" BackgroundColor="#E9ECEF" Padding="10">
            <Grid ColumnDefinitions="*,Auto">
                <Label Grid.Column="0" 
                       Text="Quick Actions" 
                       FontSize="12" 
                       TextColor="#6C757D"
                       VerticalOptions="Center" />
                       
                <sm:StateContainer Grid.Column="1"
                                 CurrentState="{Binding ActionsState.CurrentState}"
                                 HorizontalOptions="End">
                    <sm:StateContainer.SuccessContent>
                        <HorizontalStackLayout Spacing="10">
                            <Button Text="Refresh" 
                                    Command="{Binding LoadUsersCommand}"
                                    BackgroundColor="#512BD4"
                                    TextColor="White"
                                    Padding="10,5" />
                        </HorizontalStackLayout>
                    </sm:StateContainer.SuccessContent>
                    <sm:StateContainer.Templates>
                        <sm:StateDataTemplate StateType="Loading">
                            <DataTemplate>
                                <ActivityIndicator Color="#512BD4" IsRunning="True" />
                            </DataTemplate>
                        </sm:StateDataTemplate>
                        <sm:StateDataTemplate StateType="Custom">
                            <DataTemplate>
                                <Label Text="{Binding Message}" TextColor="#512BD4" />
                            </DataTemplate>
                        </sm:StateDataTemplate>
                    </sm:StateContainer.Templates>
                </sm:StateContainer>
            </Grid>
        </Border>
        
        
        <Border Grid.Row="3" BackgroundColor="#512BD4" Padding="8">
            <Grid ColumnDefinitions="*,*,*,*" ColumnSpacing="15">
                <VerticalStackLayout Grid.Column="0" Spacing="2">
                    <Label Text="Global" FontSize="9" TextColor="White" Opacity="0.7" />
                    <Label Text="{Binding GlobalState.CurrentState.CurrentState}" 
                           FontSize="10" TextColor="White" />
                </VerticalStackLayout>
                
                <VerticalStackLayout Grid.Column="1" Spacing="2">
                    <Label Text="List" FontSize="9" TextColor="White" Opacity="0.7" />
                    <Label Text="{Binding ListState.CurrentState.CurrentState}" 
                           FontSize="10" TextColor="White" />
                </VerticalStackLayout>
                
                <VerticalStackLayout Grid.Column="2" Spacing="2">
                    <Label Text="Form" FontSize="9" TextColor="White" Opacity="0.7" />
                    <Label Text="{Binding FormState.CurrentState.CurrentState}" 
                           FontSize="10" TextColor="White" />
                </VerticalStackLayout>
                
                <VerticalStackLayout Grid.Column="3" Spacing="2">
                    <Label Text="Actions" FontSize="9" TextColor="White" Opacity="0.7" />
                    <Label Text="{Binding ActionsState.CurrentState.CurrentState}" 
                           FontSize="10" TextColor="White" />
                </VerticalStackLayout>
            </Grid>
        </Border>
        
    </Grid>
    
</ContentPage>

🔄 Migration Guide

From Manual State Management

Before (Manual State Management)
public class OldViewModel : INotifyPropertyChanged
{
    private bool _isLoading;
    private bool _isError;
    private string _errorMessage;
    private bool _isEmpty;
    
    public bool IsLoading
    {
        get => _isLoading;
        set { _isLoading = value; OnPropertyChanged(); }
    }
    
    public bool IsError
    {
        get => _isError;
        set { _isError = value; OnPropertyChanged(); }
    }
    
    public string ErrorMessage
    {
        get => _errorMessage;
        set { _errorMessage = value; OnPropertyChanged(); }
    }
    
    public bool IsEmpty
    {
        get => _isEmpty;
        set { _isEmpty = value; OnPropertyChanged(); }
    }
    
    public async Task LoadDataAsync()
    {
        try
        {
            IsLoading = true;
            IsError = false;
            IsEmpty = false;
            
            var data = await service.GetDataAsync();
            
            if (!data.Any())
            {
                IsEmpty = true;
            }
            
            IsLoading = false;
        }
        catch (Exception ex)
        {
            IsLoading = false;
            IsError = true;
            ErrorMessage = ex.Message;
        }
    }
    
    // ... INotifyPropertyChanged implementation
}
After (With StateManager)
public partial class NewViewModel : ObservableObject
{
    private readonly IStateManager _stateManager;
    
    [ObservableProperty]
    private List<Data> _items = new();
    
    public IStateManager StateManager => _stateManager;
    
    public NewViewModel(IStateManager stateManager)
    {
        _stateManager = stateManager;
    }
    
    [RelayCommand]
    private async Task LoadDataAsync()
    {
        await _stateManager.ExecuteWithStateAsync(async () =>
        {
            var data = await service.GetDataAsync();
            Items = data;
            
            if (!data.Any())
            {
                _stateManager.CurrentState = UIState.Empty("No data found");
            }
        }, "Loading data...");
    }
}

XAML Migration

Before
<Grid>
    <CollectionView ItemsSource="{Binding Items}" 
                   IsVisible="{Binding IsLoading, Converter={x:StaticResource InverseBooleanConverter}}" />
    
    <ActivityIndicator IsRunning="{Binding IsLoading}" 
                      IsVisible="{Binding IsLoading}" />
                      
    <Label Text="No data available" 
           IsVisible="{Binding IsEmpty}" />
           
    <Label Text="{Binding ErrorMessage}" 
           IsVisible="{Binding IsError}" />
</Grid>
After
<Grid>
    <CollectionView ItemsSource="{Binding Items}" 
                   IsVisible="{Binding StateManager.CurrentState.IsSuccess}" />
    
    <sm:StateOverlayView State="{Binding StateManager.CurrentState}" />
</Grid>

🎉 Conclusion

Shaunebu.MAUI.StateManager provides a robust, flexible, and consistent approach to UI state management in .NET MAUI applications. By centralizing state logic and providing a clean API, it eliminates boilerplate code and ensures a consistent user experience across your application.

Key Benefits

  • ✅ Reduced Boilerplate: Eliminate repetitive state management code

  • ✅ Consistent UX: Uniform state presentation across your app

  • ✅ Better Organization: Clean separation of state concerns

  • ✅ Improved Maintainability: Centralized state logic

  • ✅ Enhanced User Experience: Professional state presentations

  • ✅ Flexible Architecture: Adaptable to any application structure

Product Compatible and additional computed target framework versions.
.NET net9.0-android35.0 is compatible.  net9.0-ios18.0 is compatible.  net9.0-maccatalyst18.0 is compatible.  net9.0-windows10.0.19041 is compatible.  net10.0-android was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 164 10/9/2025