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
<PackageReference Include="Shaunebu.MAUI.StateManager" Version="1.0.0" />
<PackageVersion Include="Shaunebu.MAUI.StateManager" Version="1.0.0" />
<PackageReference Include="Shaunebu.MAUI.StateManager" />
paket add Shaunebu.MAUI.StateManager --version 1.0.0
#r "nuget: Shaunebu.MAUI.StateManager, 1.0.0"
#:package Shaunebu.MAUI.StateManager@1.0.0
#addin nuget:?package=Shaunebu.MAUI.StateManager&version=1.0.0
#tool nuget:?package=Shaunebu.MAUI.StateManager&version=1.0.0
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.
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 displaySuccessContent: Content to show when state is SuccessTemplates: 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 displayAutomatically 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 | Versions 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. |
-
net9.0-android35.0
- CommunityToolkit.Mvvm (>= 8.4.0)
- Microsoft.Maui.Controls (>= 9.0.82)
-
net9.0-ios18.0
- CommunityToolkit.Mvvm (>= 8.4.0)
- Microsoft.Maui.Controls (>= 9.0.82)
-
net9.0-maccatalyst18.0
- CommunityToolkit.Mvvm (>= 8.4.0)
- Microsoft.Maui.Controls (>= 9.0.82)
-
net9.0-windows10.0.19041
- CommunityToolkit.Mvvm (>= 8.4.0)
- Microsoft.Maui.Controls (>= 9.0.82)
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 |