PC.Framework
4.5.16
dotnet add package PC.Framework --version 4.5.16
NuGet\Install-Package PC.Framework -Version 4.5.16
<PackageReference Include="PC.Framework" Version="4.5.16" />
<PackageVersion Include="PC.Framework" Version="4.5.16" />
<PackageReference Include="PC.Framework" />
paket add PC.Framework --version 4.5.16
#r "nuget: PC.Framework, 4.5.16"
#:package PC.Framework@4.5.16
#addin nuget:?package=PC.Framework&version=4.5.16
#tool nuget:?package=PC.Framework&version=4.5.16
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.jsonpins10.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.
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.
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 forFooViewModel(namespace*.Views→*.ViewModels, falling back to an assembly-wide scan by type name)FooView→ looks forFooViewModel, 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(nameof(SamplePage)); // push forward
await Services.Navigation.GoBackAsync(); // pop back
await Services.Navigation.GoYourSelfAsync(nameof(SamplePage)); // refresh current route
Pass an INavigationParameters (a Dictionary<string, object>-like bag) as the second argument to pass data — it shows up in OnNavigatedTo/OnNavigatedBack/OnNavigateToRefresh.
6. Show dialogs with IDialogsService
await Services.Dialogs.ShowAsync("Saved", "Your changes were saved.", "OK");
var confirmed = await Services.Dialogs.ConfirmAsync("Discard?", "You have unsaved changes.", "Discard", "Keep editing");
var name = await Services.Dialogs.PromptAsync("Rename", "Enter a new name", "OK");
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 (optional)
PC.Framework.Core defines transport-agnostic building blocks so you're not tied to a specific HTTP client:
ApiResult<T>/ApiException— typed success/failure/validation-failure results for API callsIAuthStrategy— pluggable "how do I authenticate this request" strategy (apply headers, recover from 401), resolved per-request byIAuthStrategyResolverAuthHeaderHandler(PC.Framework) — aDelegatingHandleryou attach to anHttpClient/Refit client that applies the resolvedIAuthStrategyand retries once after a successful 401 recoveryIAuthStateService— raisesSessionExpiredso the rest of the app (e.g. a navigation-to-login redirect) can reactISecureStorageService— abstraction over MAUISecureStoragefor tokens/credentials
services.AddSingleton<IAuthStrategy, MyBearerTokenStrategy>();
services.AddRefitClient<IMyApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri(baseUrl))
.AddHttpMessageHandler<AuthHeaderHandler>();
Where the token itself is kept is controlled by TokenStorage (step 1):
TokenStorageMode |
Backing store | Notes |
|---|---|---|
Preferences (default) |
MAUI Preferences |
Plain text (SharedPreferences / NSUserDefaults). Fast, works everywhere. |
SecureStorage |
MAUI SecureStorage |
Keystore/Keychain backed. The right choice for tokens. |
The two stores don't share a namespace, so switching mode in an app that's already
shipped makes existing tokens unreadable and signs those users out once. The default
stays Preferences so upgrading the package doesn't silently log anyone out — pick
SecureStorage deliberately, ideally in a release where a re-login is acceptable.
11. 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.Getreturns the supplied default when the key is missing or when the stored value can't be read back asT(e.g. it was written as a different type) — it never throws.Clear()wipes all preferences for the app, including auth tokens whenTokenStorageisPreferences. PreferRemoveunless 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 | Versions 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. |
-
net10.0
- CommunityToolkit.Maui (>= 14.2.0)
- FluentValidation (>= 12.1.1)
- FluentValidation.DependencyInjectionExtensions (>= 12.1.1)
- MediatR (>= 14.1.0)
- Microsoft.Maui.Controls (>= 10.0.71)
- Microsoft.Maui.Controls.Compatibility (>= 10.0.71)
- PC.Framework.Core (>= 0.0.5)
- ReactiveUI.Maui (>= 23.2.28)
- ReactiveUI.Validation (>= 7.1.0)
- System.ComponentModel.Annotations (>= 5.0.0)
-
net10.0-android36.0
- CommunityToolkit.Maui (>= 14.2.0)
- FluentValidation (>= 12.1.1)
- FluentValidation.DependencyInjectionExtensions (>= 12.1.1)
- MediatR (>= 14.1.0)
- Microsoft.Maui.Controls (>= 10.0.71)
- Microsoft.Maui.Controls.Compatibility (>= 10.0.71)
- PC.Framework.Core (>= 0.0.5)
- ReactiveUI.Maui (>= 23.2.28)
- ReactiveUI.Validation (>= 7.1.0)
- System.ComponentModel.Annotations (>= 5.0.0)
-
net10.0-ios26.0
- CommunityToolkit.Maui (>= 14.2.0)
- FluentValidation (>= 12.1.1)
- FluentValidation.DependencyInjectionExtensions (>= 12.1.1)
- MediatR (>= 14.1.0)
- Microsoft.Maui.Controls (>= 10.0.71)
- Microsoft.Maui.Controls.Compatibility (>= 10.0.71)
- PC.Framework.Core (>= 0.0.5)
- ReactiveUI.Maui (>= 23.2.28)
- ReactiveUI.Validation (>= 7.1.0)
- System.ComponentModel.Annotations (>= 5.0.0)
-
net10.0-maccatalyst26.0
- CommunityToolkit.Maui (>= 14.2.0)
- FluentValidation (>= 12.1.1)
- FluentValidation.DependencyInjectionExtensions (>= 12.1.1)
- MediatR (>= 14.1.0)
- Microsoft.Maui.Controls (>= 10.0.71)
- Microsoft.Maui.Controls.Compatibility (>= 10.0.71)
- PC.Framework.Core (>= 0.0.5)
- ReactiveUI.Maui (>= 23.2.28)
- ReactiveUI.Validation (>= 7.1.0)
- System.ComponentModel.Annotations (>= 5.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.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.5.16 | 133 | 8/10/2026 |
| 4.5.15 | 88 | 8/9/2026 |
| 4.5.14 | 112 | 7/29/2026 |
| 4.5.13 | 163 | 7/15/2026 |
| 4.5.12 | 104 | 7/13/2026 |
| 4.5.11 | 93 | 7/13/2026 |
| 4.5.10 | 124 | 7/13/2026 |
| 4.5.9 | 100 | 7/11/2026 |
| 4.5.8 | 112 | 7/10/2026 |
| 4.5.7 | 111 | 7/10/2026 |
| 4.5.6 | 108 | 7/10/2026 |
| 4.5.5 | 102 | 7/10/2026 |
| 4.5.4 | 109 | 7/10/2026 |
| 4.5.3 | 116 | 7/6/2026 |
| 4.5.2 | 109 | 7/1/2026 |
| 4.5.1 | 117 | 6/29/2026 |
| 4.5.0 | 114 | 6/23/2026 |
| 4.4.0 | 698 | 7/31/2025 |
| 4.3.9 | 264 | 6/16/2025 |
| 4.3.8 | 260 | 6/16/2025 |