StateView.Maui
1.0.2
dotnet add package StateView.Maui --version 1.0.2
NuGet\Install-Package StateView.Maui -Version 1.0.2
<PackageReference Include="StateView.Maui" Version="1.0.2" />
<PackageVersion Include="StateView.Maui" Version="1.0.2" />
<PackageReference Include="StateView.Maui" />
paket add StateView.Maui --version 1.0.2
#r "nuget: StateView.Maui, 1.0.2"
#:package StateView.Maui@1.0.2
#addin nuget:?package=StateView.Maui&version=1.0.2
#tool nuget:?package=StateView.Maui&version=1.0.2
StateView.Maui
Description
StateView.Maui is a .NET MAUI control for showing loading, saving, success, error, empty, and custom UI states around normal page content.
The main control, StateView, keeps your regular content in place and swaps in templated state content when CurrentState changes. State UI is defined with DataTemplate instances, so state views are created fresh when rendered.
Features
- Built-in states:
None,Loading,Saving,Success,Error,Empty, andCustom. - Template-based state UI through
StateTemplate. - Custom state matching with
CurrentCustomStateKeyandCustomStateKey. - Repeated skeleton rows with
RepeatCountandRepeatTemplate. - Lightweight pulsing
SkeletonViewfor loading placeholders. - Optional fade transitions through
AnimateStateChangesandAnimationDuration. StateToBooleanConverterfor binding state values to boolean UI properties.
Installation
dotnet add package StateView.Maui --version 1.0.2
Use the XAML namespace in pages that render StateView.Maui controls:
xmlns:state="clr-namespace:StateView.Maui;assembly=StateView.Maui"
Usage
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:state="clr-namespace:StateView.Maui;assembly=StateView.Maui">
<state:StateView CurrentState="{Binding CurrentState}">
<state:StateView.StateTemplates>
<state:StateTemplate StateKey="Loading">
<state:StateTemplate.Template>
<DataTemplate>
<VerticalStackLayout Padding="24" Spacing="12" HorizontalOptions="Center" VerticalOptions="Center">
<ActivityIndicator IsRunning="True" />
<Label Text="Loading..." HorizontalTextAlignment="Center" />
</VerticalStackLayout>
</DataTemplate>
</state:StateTemplate.Template>
</state:StateTemplate>
<state:StateTemplate StateKey="Empty">
<state:StateTemplate.Template>
<DataTemplate>
<Label
Padding="24"
Text="No items found."
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center" />
</DataTemplate>
</state:StateTemplate.Template>
</state:StateTemplate>
</state:StateView.StateTemplates>
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid Padding="12">
<Label Text="{Binding Title}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</state:StateView>
</ContentPage>
State.None shows the normal content. Any other state looks for the first matching StateTemplate. If no template matches, the normal content remains visible.
ViewModel
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using StateView.Maui;
public sealed class ItemsViewModel : INotifyPropertyChanged
{
private State _currentState = State.Loading;
public event PropertyChangedEventHandler? PropertyChanged;
public ObservableCollection<Item> Items { get; } = [];
public State CurrentState
{
get => _currentState;
set => SetProperty(ref _currentState, value);
}
public async Task LoadAsync()
{
CurrentState = State.Loading;
await Task.Delay(500);
Items.Clear();
CurrentState = Items.Count == 0 ? State.Empty : State.None;
}
private void SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
{
return;
}
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public sealed record Item(string Title);
Custom States
Use State.Custom when one state slot needs multiple named templates. Set CurrentCustomStateKey on StateView, then match it with CustomStateKey on a StateTemplate.
<state:StateView
CurrentState="{Binding CurrentState}"
CurrentCustomStateKey="{Binding CurrentCustomStateKey}">
<state:StateView.StateTemplates>
<state:StateTemplate StateKey="Custom" CustomStateKey="Offline">
<state:StateTemplate.Template>
<DataTemplate>
<Border Padding="20">
<Label Text="Offline mode is active." />
</Border>
</DataTemplate>
</state:StateTemplate.Template>
</state:StateTemplate>
</state:StateView.StateTemplates>
<CollectionView ItemsSource="{Binding Items}" />
</state:StateView>
CurrentCustomStateKey = "Offline";
CurrentState = State.Custom;
Skeleton And Repeat Templates
SkeletonView is a pulsing BoxView placeholder. For repeated loading rows, set RepeatCount and provide a RepeatTemplate.
<state:StateTemplate StateKey="Loading" RepeatCount="5">
<state:StateTemplate.RepeatTemplate>
<DataTemplate>
<Grid Padding="16" RowDefinitions="20,14" RowSpacing="10">
<state:SkeletonView />
<state:SkeletonView Grid.Row="1" WidthRequest="190" HorizontalOptions="Start" />
</Grid>
</DataTemplate>
</state:StateTemplate.RepeatTemplate>
</state:StateTemplate>
When RepeatCount is greater than 1, repeated content is created from RepeatTemplate. If RepeatTemplate is missing, no repeated placeholder content is created.
Animated Transitions
State changes fade by default. Use AnimateStateChanges to enable or disable transitions and AnimationDuration to control the fade duration in milliseconds.
<state:StateView
AnimateStateChanges="True"
AnimationDuration="250"
CurrentState="{Binding CurrentState}">
<CollectionView ItemsSource="{Binding Items}" />
</state:StateView>
Rapid state changes are coordinated so obsolete transitions are cancelled before the latest state is applied.
Why
State-heavy screens often need loading, empty, error, success, and custom overlays without scattering conditional UI across the page. StateView.Maui keeps that logic in one control while leaving each state fully templated and app-specific.
Sample App
The sample app is in SAMPLE/StateView.Maui.Sample.
It demonstrates Loading, Empty, Error, Success, Custom/Offline, skeleton placeholders, animated transitions, and rapid state changes.
Build
dotnet restore StateView.Maui.sln
dotnet build StateView.Maui/StateView.Maui.csproj -f net10.0-android
dotnet build StateView.Maui/StateView.Maui.csproj -f net10.0-ios
dotnet build StateView.Maui/StateView.Maui.csproj -f net10.0-maccatalyst
Requirements
- .NET MAUI with .NET 10.
- Supported target frameworks:
net10.0-android,net10.0-ios, andnet10.0-maccatalyst. - Android API level 23 or later.
- iOS 15.0 or later.
- Mac Catalyst 15.0 or later.
Known Limitations
- The attached property compatibility layer is not included in v1.
- This is not a drop-in replacement for Xamarin.Forms.StateSquid.
- Windows is not included.
- Advanced theme and preset skeleton APIs are not included.
Local development with ProjectReference
This library is intended to be consumed primarily as a NuGet package.
For local development, debugging, or testing changes before publishing a new package, you can also clone the repository and reference the project directly with a ProjectReference from a consuming .NET MAUI app.
This mode is optional and should be treated as a development-only workflow. Normal consumers should use the NuGet package.
Enable ProjectReference mode locally
To enable local ProjectReference mode, create a file named:
Directory.Build.local.props
in the same directory as:
Directory.Build.props
Do not commit this file. It is meant to contain local machine/developer settings only.
Recommended local configuration:
<Project>
<PropertyGroup>
<UseAsProjectReference>true</UseAsProjectReference>
<OverrideAndroidSpecificVersion>36.0</OverrideAndroidSpecificVersion>
</PropertyGroup>
</Project>
What this does
By default, the project uses package-oriented, generic .NET MAUI platform TFMs, for example:
net10.0-ios
net10.0-android
When UseAsProjectReference is enabled, the project can adjust its target frameworks to match the platform-specific target required by a consuming app.
For example, with:
<OverrideAndroidSpecificVersion>36.0</OverrideAndroidSpecificVersion>
the Android target becomes:
net10.0-android36.0
This is useful when a consuming app targets a specific Android platform version and the library is referenced directly as a project instead of as a NuGet package.
If UseAsProjectReference=true is set and OverrideAndroidSpecificVersion is not provided, the project is configured to fall back to Android 36.0 for ProjectReference mode. Setting the value explicitly is still recommended because it makes the consuming setup easier to read.
Optional iOS override
If a consuming app requires a specific iOS platform version, use:
<OverrideIosSpecificVersion>26.0</OverrideIosSpecificVersion>
In that case, the iOS target becomes:
net10.0-ios26.0
If OverrideIosSpecificVersion is not set, the iOS target remains generic:
net10.0-ios
Optional MacCatalyst override
Projects that include MacCatalyst also support:
<OverrideMacCatalystSpecificVersion>26.0</OverrideMacCatalystSpecificVersion>
When set together with UseAsProjectReference=true, this changes the MacCatalyst target to:
net10.0-maccatalyst26.0
Important notes
Directory.Build.local.propsis for local development only.- Do not commit
Directory.Build.local.props. - Normal NuGet builds and CI builds should run without this local file.
- When the local file is not present,
UseAsProjectReferencedefaults tofalse. - When
UseAsProjectReferenceisfalse, the project uses its normal package-oriented target frameworks. - If the project supports MacCatalyst,
OverrideMacCatalystSpecificVersioncan be used in the same way as the Android and iOS overrides. - If you switch between package mode and project-reference mode, clean
binandobjfolders before rebuilding. - Restore and build should be performed in the same mode. If restore runs with local overrides enabled, build should use the same overrides.
- If Rider keeps building against an old Android/iOS target after switching modes, reload all projects. If the problem persists, use File > Invalidate Caches... and reopen the solution.
Packing and testing the NuGet package locally
When creating or testing the NuGet package, make sure the local ProjectReference overrides are disabled. Otherwise the package can be produced with development-specific target frameworks.
Before packing, temporarily rename the local props file if it exists:
mv Directory.Build.local.props Directory.Build.local.props.disabled
Then clean generated folders from the repository root:
find . -type d \( -name bin -o -name obj \) -prune -exec rm -rf {} +
Pack the library by calling dotnet pack directly on the library .csproj, not on the solution root:
dotnet pack StateView.Maui/StateView.Maui.csproj \
-c Release \
-o ./local-nuget
Packing the concrete library project avoids unintentionally building sample apps, tests, or other projects in the solution.
After packing, you can re-enable your local development settings:
mv Directory.Build.local.props.disabled Directory.Build.local.props
To inspect the generated package contents:
unzip -l ./local-nuget/StateView.Maui.1.0.2.nupkg | grep "lib/"
With .NET MAUI/.NET 10, it is normal for the generated .nupkg to contain platform-normalized asset folders such as:
lib/net10.0-android36.0/
lib/net10.0-ios26.0/
even when the project file declares generic TFMs such as net10.0-android or net10.0-ios. Those platform versions are resolved by the installed .NET SDK/workloads during build/pack.
To test the package without publishing it, add ./local-nuget as a local NuGet source in a consuming app and use the normal PackageReference workflow. This is the best way to verify the package as a real consumer would use it.
License
StateView.Maui is released under the MIT License. See the LICENSE file for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0-android36.0 is compatible. net10.0-ios26.0 is compatible. net10.0-maccatalyst26.0 is compatible. |
-
net10.0-android36.0
- Microsoft.Maui.Controls (>= 10.0.0)
-
net10.0-ios26.0
- Microsoft.Maui.Controls (>= 10.0.0)
-
net10.0-maccatalyst26.0
- Microsoft.Maui.Controls (>= 10.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.