PC.Framework 5.0.2

dotnet add package PC.Framework --version 5.0.2
                    
NuGet\Install-Package PC.Framework -Version 5.0.2
                    
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="PC.Framework" Version="5.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PC.Framework" Version="5.0.2" />
                    
Directory.Packages.props
<PackageReference Include="PC.Framework" />
                    
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 PC.Framework --version 5.0.2
                    
#r "nuget: PC.Framework, 5.0.2"
                    
#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 PC.Framework@5.0.2
                    
#: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=PC.Framework&version=5.0.2
                    
Install as a Cake Addin
#tool nuget:?package=PC.Framework&version=5.0.2
                    
Install as a Cake Tool

PC.Framework

An opinionated toolkit for building .NET MAUI apps with ReactiveUI, MVVM, MediatR (CQRS), and FluentValidation — wired together so you don't have to.

PC.Framework gives you base ViewModel/Page classes with automatic ViewModel wiring, a Shell-based navigation service, a dialogs abstraction, FluentValidation-backed edit forms with dirty-tracking, a global exception handler, and pluggable HTTP auth strategies. The goal is to remove the boilerplate every MAUI + ReactiveUI project ends up writing by hand.

Packages in this repo

Project What it is
src/PC.Framework The main MAUI library: base views/viewmodels, navigation, dialogs, validation, error handling, DI registration (UsePcFramework)

Requirements

  • .NET 10 SDK (src/global.json pins 10.0.201, rolling forward to the latest minor)
  • MAUI workload (dotnet workload install maui)
  • Visual Studio 2022 or JetBrains Rider with MAUI support

Installation

dotnet add package PC.Framework

Step-by-step guide

1. Register the framework in MauiProgram.cs

UsePcFramework is the single entry point. It registers navigation, dialogs, auth, storage, the global exception handler, and MediatR (with logging + validation pipeline behaviors) into the MAUI DI container.

using PC.Framework;
using ReactiveUI.Builder;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();

        builder
            .UseMauiApp<App>()
            .UseMauiCommunityToolkit()
            .UsePcFramework(new FrameworkConfiguration
            {
                FontRegistration = new Dictionary<string, string>
                {
                    { "OpenSansRegular", "OpenSans-Regular.ttf" },
                    { "OpenSansSemibold", "OpenSans-Semibold.ttf" }
                },
                RegisterServicesFromAssembly = typeof(App), // assembly MediatR scans for handlers
                ReactiveBuilder = rxBuilder => rxBuilder.WithMaui().BuildApp(),
                TokenStorage = TokenStorageMode.SecureStorage, // or .Preferences (default)
                ExceptionHandlerConfig = new GlobalExceptionHandlerConfig(
                    AlertType: ErrorAlertType.Localize,
                    IgnoreTokenCancellations: true,
                    LogError: true)
            });

        builder.Services.AddViewsAndViewModels(); // your own registrations, see step 4

        return builder.Build();
    }
}

UsePcFramework registers, among others: INavigationService, IDialogsService, IGlobalExceptionHandler, IAuthStateService, ISecureStorageService, IPreferencesService, IAuthStrategyResolver, BaseServices, and IMediator (MediatR) with LoggingBehavior<,> and ValidationBehavior<,> in its pipeline.

Replacing a framework service

Every framework service is registered with TryAdd, so registering your own before UsePcFramework wins — the framework will not overwrite it:

builder.Services.AddSingleton<IDialogsService, MyDialogsService>();  // must come first
builder.UsePcFramework(new FrameworkConfiguration { ... });

The shipped implementations (NavigationService, DialogsService, PreferencesService, SecureStorageService, AuthStrategyResolver, AuthStateService, GlobalExceptionHandler) are all unsealed with virtual members, so you can subclass and override a single method instead of reimplementing the interface:

public sealed class LoggingNavigationService(ILogger<LoggingNavigationService> log) : NavigationService
{
    public override Task GoAsync(string route, INavigationParameters? p = null)
    {
        log.LogInformation("Navigating to {Route}", route);
        return base.GoAsync(route, p);
    }
}
Turning parts off, and adding your own
builder.UsePcFramework(new FrameworkConfiguration
{
    UseMediatR = false,           // app doesn't use CQRS, or wires MediatR itself
    UseCommunityToolkit = false,  // app calls UseMauiCommunityToolkit() itself
    ConfigureMediatR = c => c.AddBehavior(typeof(IPipelineBehavior<,>), typeof(MyBehavior<,>)),
    ConfigureServices = services => services.AddSingleton<IMyService, MyService>(),
});

ConfigureMediatR runs after the framework's own MediatR setup, so the built-in LoggingBehavior and ValidationBehavior stay in place and yours are appended.

Resolving anything else: BaseServices.Services

BaseServices carries the app's IServiceProvider. Resolve extras from it rather than adding constructor parameters to your ViewModel bases:

var theme = Services.GetRequiredService<IThemeService>();  // throws with a clear message if unregistered
var maybe = Services.GetService<IAnalytics>();             // null when unregistered
Custom View→ViewModel naming

The default convention is FooPage/FooView → FooViewModel, searching the View's own assembly. Apps that name things differently, or split Views and ViewModels across projects, can extend it instead of giving up auto-wiring:

builder.UsePcFramework(new FrameworkConfiguration
{
    // ViewModels live in a separate assembly
    ViewModelAssemblies = { typeof(LogInViewModel).Assembly },

    // e.g. FooScreen -> FooViewModel
    ViewModelConventions =
    {
        t => t.Name.EndsWith("Screen")
            ? t.Assembly.GetType($"{t.Namespace}.{t.Name[..^"Screen".Length]}ViewModel")
            : null
    },
});

2. Create a ViewModel

Derive from ViewModelBase and inject BaseServices — the framework's bundle of Navigation, Dialogs, Dispatcher (MediatR), ErrorHandler, and Connectivity.

using PC.Framework;
using PC.Framework.Bases.ViewModels;
using PC.Framework.Interfaces;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using Unit = System.Reactive.Unit;

public sealed partial class SampleViewModel : ViewModelBase
{
    [Reactive] private string _displayName = string.Empty;

    public ReactiveCommand<Unit, Unit> RefreshCommand { get; }

    public SampleViewModel(BaseServices services) : base(services)
    {
        RefreshCommand = ReactiveCommand.CreateFromTask(Load);
    }

    // Called automatically when the page is navigated to (forward navigation)
    protected override async Task OnNavigatedTo(INavigationParameters parameters, CancellationToken token)
    {
        await Load();
    }

    private async Task Load()
    {
        IsBusy = true;
        try { /* fetch data via Services.Dispatcher.Send(...) */ }
        finally { IsBusy = false; }
    }
}

BaseServices bundles Navigation, Dialogs, Dispatcher (MediatR), ErrorHandler, Connectivity, and Preferences (step 11).

ViewModelBase implements IActivatableViewModel + IQueryAttributable. Based on how the page was navigated to, one lifecycle hook fires automatically:

  • OnNavigatedTo — forward navigation (Navigation.GoAsync)
  • OnNavigatedBack — back navigation (Navigation.GoBackAsync)
  • OnNavigateToRefresh — self-navigation/refresh (Navigation.GoYourSelfAsync)

Override OnWhenActivated(CompositeDisposable disposables) for any additional WhenActivated subscriptions.

Lifecycle hooks
Hook When
InitializeAsync Once, before the first OnNavigatedTo. Setup that shouldn't repeat on return.
OnNavigatedTo Activated after a forward navigation.
OnNavigatedBack Activated after a back navigation.
OnNavigateToRefresh Activated after GoYourSelfAsync.
OnNavigatedFrom Deactivated — navigated away from. Not awaited; keep it short.
OnWhenActivated Every activation, for Rx subscriptions bound to the CompositeDisposable.

Exceptions thrown by these hooks are routed to IGlobalExceptionHandler.

3. Create the View

Use BaseReactivePage<TViewModel> for a normal page, or BaseReactiveEditablePage<TViewModel> when the ViewModel needs "discard unsaved changes?" back-button behavior (pairs with EditViewModelBase<T>, step 6).

<views:BaseReactivePage x:TypeArguments="vm:SampleViewModel"
                         xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                         xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                         xmlns:views="clr-namespace:PC.Framework.Bases.Views;assembly=PC.Framework"
                         xmlns:vm="clr-namespace:YourApp.ViewModels"
                         x:Class="YourApp.Views.SamplePage">
    <Label Text="{Binding DisplayName}" />
</views:BaseReactivePage>

The page auto-wires its BindingContext from DI as soon as it's constructed/parented — no code-behind needed. Resolution uses a naming convention (ViewModelLocator):

  • FooPage → looks for FooViewModel (namespace *.Views → *.ViewModels, falling back to an assembly-wide scan by type name)
  • FooView → looks for FooViewModel, same rules

Set AutoWire="False" on the page if you want to assign BindingContext yourself. If a match can't be resolved or isn't registered in DI, the framework throws loudly in DEBUG and logs quietly in RELEASE.

4. Register the View + ViewModel and (if needed) a Shell route

public static class ServiceExtensions
{
    public static IServiceCollection AddViewsAndViewModels(this IServiceCollection services)
    {
        // Full pages (navigable via Shell)
        services.RegisterNavigation<SamplePage, SampleViewModel>();

        // Child/embedded views (not directly routable)
        services.RegisterView<SampleHeaderView, SampleHeaderViewModel>();

        return services;
    }
}

Pages referenced from ShellContent in AppShell.xaml are picked up automatically; anything reached with GoAsync("SomeRoute") needs an explicit route in AppShell.xaml.cs:

public partial class AppShell
{
    public AppShell()
    {
        InitializeComponent();
        Routing.RegisterRoute(nameof(SamplePage), typeof(SamplePage));
    }
}

5. Navigate with INavigationService

Available on every ViewModel via Services.Navigation:

await Services.Navigation.GoAsync<SampleViewModel>();               // typed — no route string
await Services.Navigation.GoAsync(nameof(SamplePage));              // by route
await Services.Navigation.GoBackAsync();                            // pop back
await Services.Navigation.GoToRootAsync();                          // pop to Shell root
await Services.Navigation.GoYourSelfAsync(nameof(SamplePage));      // refresh current route
await Services.Navigation.GoToTabAsync("home");                     // switch Shell tab
await Services.Navigation.GoModalAsync<FilterPage>();               // present modally
await Services.Navigation.PopModalAsync();                          // dismiss modal

Typed navigation uses the route recorded by RegisterNavigation<TPage, TViewModel>() (step 4), which also means you no longer write Routing.RegisterRoute in AppShell — routes are pushed into Shell automatically before the first navigation.

Every method returns Task<bool>: false means the call was absorbed because a navigation was already in flight. That gate is what stops a double-tap pushing the same page twice.

Passing data

Pass an INavigationParameters (a Dictionary<string, object>-like bag) as the second argument — it arrives in OnNavigatedTo/OnNavigatedBack/OnNavigateToRefresh. Read values with the typed accessors rather than casting object; they also cope with Shell handing you a query-string value as text:

await Services.Navigation.GoAsync<DetailViewModel>(new NavigationParameters
{
    { NavParams.Id, 42 },
    { "mode", EditMode.ReadOnly },
});

protected override Task OnNavigatedTo(INavigationParameters parameters, CancellationToken token)
{
    var id   = parameters.GetValue<int>(NavParams.Id);           // 0 when absent
    var mode = parameters.GetValue("mode", EditMode.Editable);   // enum, with a fallback
    if (parameters.TryGetValue<Guid>("ref", out var reference)) { /* ... */ }
    return Task.CompletedTask;
}
Returning a result
// caller
var picked = await Services.Navigation.GoForResultAsync<string>(nameof(PickerPage));

// on the picker, before going back
Services.Navigation.SetResult(selected);
await Services.Navigation.GoBackAsync();

Only back-navigations that go through GoBackAsync settle the result. A page dismissed by the OS back gesture settles as default on the next navigation rather than hanging — call SetResult explicitly rather than relying on that.

6. Dialogs, popups and busy UI

Services.Dialogs covers transient messages. Every method marshals to the main thread, so it is safe to call from background work — these APIs throw if called off the UI thread.

await Services.Dialogs.ShowAsync("Saved", "Your changes were saved.");
var ok      = await Services.Dialogs.ConfirmAsync("Delete?", "This cannot be undone.", "Delete", "Keep");
var name    = await Services.Dialogs.PromptAsync("Name", "What should we call it?",
                                                 placeholder: "e.g. Weekly report", maxLength: 60);
var choice  = await Services.Dialogs.ActionSheetAsync("Sort by", "Cancel", "Newest", "Oldest", "A–Z");

await Services.Dialogs.ShowToastAsync("Copied to clipboard");
await Services.Dialogs.ShowSnackbarAsync("Message archived", "Undo", () => Restore());

PromptAsync returns null when cancelled. Toast and snackbar come from CommunityToolkit.Maui, so they need it initialized — the default unless you set UseCommunityToolkit = false.

Custom popups

Register a popup view with its ViewModel, then show it by view type:

services.RegisterPopup<FilterPopupView, FilterPopupViewModel>();

// returns default when dismissed by tapping outside
var filter = await Services.Popups.ShowAsync<FilterPopupView, FilterOptions>();

// from inside the popup's ViewModel
await Services.Popups.CloseAsync(selectedFilter);

The service is IPopupPresenter, deliberately not IPopupService — CommunityToolkit already publishes a type by that name, and matching it would be ambiguous in any file importing the toolkit.

Busy overlay

BusyOverlay dims its content and shows a spinner while IsBusy is set. It is a Grid, so its children share one cell and the overlay is kept on top of them:

xmlns:pc="clr-namespace:PC.Framework.Controls;assembly=PC.Framework"

<pc:BusyOverlay IsBusy="{Binding IsBusy}" BusyText="Signing in…">
    <ScrollView> ... </ScrollView>
</pc:BusyOverlay>

The scrim also swallows taps, so content underneath can't be interacted with while busy. OverlayColor and IndicatorColor are overridable.

7. Edit forms with validation + dirty-tracking (EditViewModelBase<T>)

For forms, derive from EditViewModelBase<T> and inject a FluentValidation validator for the ViewModel itself. It gives you Errors, IsValid, and IsDirty as reactive properties, kept in sync as the user types.

public sealed partial class LogInViewModel : EditViewModelBase<LogInViewModel>
{
    [Reactive] private string _email = string.Empty;
    [Reactive] private string _password = string.Empty;

    public ReactiveCommand<Unit, Unit> LoginCommand { get; }

    public LogInViewModel(BaseServices services, IValidator<LogInViewModel> validator) : base(services, validator)
    {
        var canLogin = this.WhenAnyValue(x => x.IsValid, x => x.IsBusy, (valid, busy) => valid && !busy);
        LoginCommand = ReactiveCommand.CreateFromTask(Login, canLogin);
    }

    protected override IReadOnlySet<string> ValidatedProperties { get; } =
        new HashSet<string> { nameof(Email), nameof(Password) };

    private async Task Login() { /* ... */ }
}

public class LogInViewModelValidator : AbstractValidator<LogInViewModel>
{
    public LogInViewModelValidator()
    {
        RuleFor(x => x.Email).NotEmpty().EmailAddress();
        RuleFor(x => x.Password).NotEmpty().MinimumLength(8);
    }
}

Register validators once, scanning the assembly(ies) they live in:

builder.Services.AddValidatorsFromAssemblies([typeof(App).Assembly]);

In XAML, bind a field's error through the built-in ErrorLookupConverter. The framework does not register it for you — add it to a ResourceDictionary your App.xaml merges:

<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                    xmlns:converters="clr-namespace:PC.Framework.Converters;assembly=PC.Framework">
    <converters:ErrorLookupConverter x:Key="ErrorLookup" />
</ResourceDictionary>

Then bind each field's message by property name:

<Entry Text="{Binding Email, Mode=TwoWay}" />
<Label Text="{Binding Errors, Converter={StaticResource ErrorLookup}, ConverterParameter=Email}" />

Use BaseReactiveEditablePage<TViewModel> for the page so a back-button press while IsDirty prompts the user via ConfirmLeaveIfDirtyAsync() before leaving.

8. Global exception handling

GlobalExceptionHandlerConfig (passed in step 1) controls behavior:

new GlobalExceptionHandlerConfig(
    AlertType: ErrorAlertType.Localize,   // None | NoLocalize | FullError | Localize
    IgnoreTokenCancellations: true,       // swallow TaskCanceledException
    LogError: true);

To also catch unhandled exceptions at the AppDomain/platform level (native iOS/Android crashes, unobserved task exceptions), call this once during app startup (e.g. in App.xaml.cs):

GlobalExceptionHandlerRegistration.RegisterAppDomainUnhandledExceptionHandlers(
    app.Services.GetRequiredService<IGlobalExceptionHandler>());

9. CQRS with MediatR (Services.Dispatcher)

UsePcFramework wires up MediatR with LoggingBehavior<,> and ValidationBehavior<,> already in the pipeline, scanning RegisterServicesFromAssembly (from step 1) for handlers. Define requests/handlers as usual and call them from a ViewModel via Services.Dispatcher:

var result = await Services.Dispatcher.Send(new LoginRequest(Email, Password), ct);

Handlers whose request implements FluentValidation's IValidator<TRequest> get validated automatically before execution by ValidationBehavior<,>.

10. HTTP + auth

IApiClient reports outcomes as ApiResult<T> instead of throwing, so call sites do not need a try/catch around every request:

services.AddPcHttpClient(new Uri("https://api.example.com"));
var result = await _api.GetAsync<Booking[]>("bookings");

if (result.IsSuccess)          Bookings = result.Data!;
else if (result.HasValidationErrors) ShowFieldErrors(result.ValidationErrors);
else                           await Services.Dialogs.ShowAsync("Error", result.ErrorMessage!);

Non-2xx responses, transport failures and malformed payloads all come back as failed results carrying StatusCode and Exception. Cancellation is the exception — an OperationCanceledException propagates, because a cancelled call has no outcome to report. ValidationProblemDetails responses are unpacked into ValidationErrors per field.

ApiResult<T> composes: Match, Map, GetValueOrDefault, and an implicit conversion from T. There is a non-generic ApiResult for calls that return nothing.

Resilience

The framework ships no retry policy and takes no dependency on Polly. AddPcHttpClient returns the IHttpClientBuilder, so add one if you want it:

// dotnet add package Microsoft.Extensions.Http.Resilience
services.AddPcHttpClient(new Uri("https://api.example.com"))
        .AddStandardResilienceHandler();
Auth

Register an IAuthStrategy; AuthHeaderHandler applies it to every request and retries once after a recovered 401.

services.AddAuthStrategy(sp => new BearerTokenAuthStrategy(
    sp.GetRequiredService<ISecureStorageService>(),
    refresh: async (current, ct) => await MyIdentityApi.RefreshAsync(current.RefreshToken, ct),
    authState: sp.GetRequiredService<IAuthStateService>()));

BearerTokenAuthStrategy refreshes proactively once AuthToken.NeedsRefresh is true (five minutes before expiry) and reactively on a 401, persisting the new token and signalling IAuthStateService.SessionExpired when the session is genuinely over.

Concurrent 401s collapse into a single refresh. When a token expires, every in-flight request fails at once; ITokenRefreshCoordinator (a singleton, so one refresh serves every named HttpClient) ensures only one refresh runs per scheme. Without it, N requests meant N refreshes — and with rotating refresh tokens, the later ones fail and sign the user out.

Strategies are tried in registration order, first CanHandle wins, so register the most specific first. ApiKeyAuthStrategy is also provided for keys that never expire.

11. Commands, screen state and collections

Commands

CreateCommand gives a command the wiring nearly every command needs, so it stops being written by hand each time:

SaveCommand = CreateCommand(
    async ct => await Save(ct),
    canExecute: this.WhenAnyValue(x => x.IsValid),
    requiresInternet: true);       // fails fast with NoConnectivityException when offline

RefreshCommand = CreateCommand(ct => Reload(ct), trackBusy: false);

It sets IsBusy for the duration (reference-counted, so a short command finishing doesn't clear the flag while a longer one is still running) and — importantly — subscribes ThrownExceptions to IGlobalExceptionHandler. A raw ReactiveCommand leaves that unobserved, so a command that throws fails silently.

For non-command code paths there is RunBusyAsync(...).

Screen state

Bind ViewModelBase.State (a ContentState: Loading / Content / Empty / Error) to a StateContainer instead of juggling IsBusy / IsEmpty / HasError flags:

<pc:StateContainer State="{Binding State}">
    <pc:StateContainer.LoadingView><ActivityIndicator IsRunning="True" /></pc:StateContainer.LoadingView>
    <pc:StateContainer.EmptyView><Label Text="Nothing here yet" /></pc:StateContainer.EmptyView>
    <pc:StateContainer.ErrorView><Label Text="Something went wrong" /></pc:StateContainer.ErrorView>
    <pc:StateContainer.ContentView><CollectionView ItemsSource="{Binding Items}" /></pc:StateContainer.ContentView>
</pc:StateContainer>

State and IsBusy are independent: use IsBusy with BusyOverlay for an overlay on existing content, and State when the whole screen is a spinner, an empty message or an error. It is ContentState, not ViewState, because MAUI already publishes Microsoft.Maui.Controls.ViewState.

Collections

ObservableRangeCollection<T> changes in bulk with a single notification — adding 50 items to a plain ObservableCollection makes MAUI re-measure the list 50 times:

Items.AddRange(page);        // one notification
Items.ReplaceRange(results); // one notification

ObservableDictionary<TKey, TValue> is a bindable dictionary. It now implements IDictionary<TKey, TValue> over a single store; previously it derived from ObservableCollection<KeyValuePair<,>> alongside a private dictionary, and the two could silently disagree.

12. Local storage with IPreferencesService

For non-sensitive app data you want to survive restarts — last selected tab, onboarding seen, a cached user profile. Available on every ViewModel as Services.Preferences, or inject IPreferencesService anywhere.

Services.Preferences.Set("last_tab", 2);
Services.Preferences.Set("onboarding_seen", true);
Services.Preferences.Set("last_sync", DateTimeOffset.UtcNow);
Services.Preferences.Set("profile", new UserProfile("Ada", "ada@example.com"));

var tab      = Services.Preferences.Get("last_tab", 0);
var seen     = Services.Preferences.Get("onboarding_seen", false);
var profile  = Services.Preferences.Get<UserProfile?>("profile");

Services.Preferences.Remove("profile");

string, bool, int, long, double, float and DateTime are stored natively by MAUI Preferences. Guid, DateTimeOffset and enums are stored as strings, and any other type is serialized to JSON — so records and POCOs round-trip without extra work.

  • Set(key, null) removes the key.
  • Get returns the supplied default when the key is missing or when the stored value can't be read back as T (e.g. it was written as a different type) — it never throws.
  • Clear() wipes all preferences for the app, including auth tokens when TokenStorage is Preferences. Prefer Remove unless you mean a full reset.

This is plain-text storage. Anything sensitive belongs in ISecureStorageService with TokenStorageMode.SecureStorage.


Reference

Type Namespace Purpose
ViewModelBase PC.Framework.Bases.ViewModels Base ReactiveUI ViewModel with navigation lifecycle hooks
EditViewModelBase<T> PC.Framework.Bases.ViewModels Adds FluentValidation Errors/IsValid + dirty-tracking IsDirty
BaseReactivePage<TViewModel> PC.Framework.Bases.Views Auto-wiring ReactiveContentPage<TViewModel>
BaseReactiveEditablePage<TViewModel> PC.Framework.Bases.Views BaseReactivePage + "discard changes?" back-button guard
BaseServices PC.Framework DI bundle: Navigation, Dialogs, Dispatcher, ErrorHandler, Connectivity, Preferences
IPreferencesService PC.Framework.Interfaces Typed key/value local storage over MAUI Preferences
TokenStorageMode PC.Framework Selects Preferences vs SecureStorage for auth tokens
INavigationService PC.Framework.Interfaces GoAsync / GoBackAsync / GoYourSelfAsync over Shell navigation
IDialogsService PC.Framework.Interfaces ConfirmAsync / ShowAsync / PromptAsync
IGlobalExceptionHandler PC.Framework.Interfaces Central exception processing + alerting
ApiResult<T> / ApiException PC.Framework.Core.Http Typed API call outcomes
IAuthStrategy / IAuthStrategyResolver PC.Framework.Core.Contracts Pluggable request authentication
ViewModelLocator PC.Framework Naming-convention ViewModel resolution used by BaseReactivePage

License

MIT © Pirate Chicken

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-android36.0 is compatible.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-ios26.0 is compatible.  net10.0-maccatalyst was computed.  net10.0-maccatalyst26.0 is compatible.  net10.0-macos was computed.  net10.0-tvos 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
5.0.2 131 10/9/2026
4.5.16 250 8/10/2026
4.5.15 120 8/9/2026
4.5.14 136 7/29/2026
4.5.13 185 7/15/2026
4.5.12 119 7/13/2026
4.5.11 119 7/13/2026
4.5.10 146 7/13/2026
4.5.9 121 7/11/2026
4.5.8 127 7/10/2026
4.5.7 134 7/10/2026
4.5.6 122 7/10/2026
4.5.5 120 7/10/2026
4.5.4 130 7/10/2026
4.5.3 138 7/6/2026
4.5.2 127 7/1/2026
4.5.1 131 6/29/2026
4.5.0 130 6/23/2026
4.4.0 738 7/31/2025
4.3.9 280 6/16/2025
Loading failed