PC.Framework
4.5.14
dotnet add package PC.Framework --version 4.5.14
NuGet\Install-Package PC.Framework -Version 4.5.14
<PackageReference Include="PC.Framework" Version="4.5.14" />
<PackageVersion Include="PC.Framework" Version="4.5.14" />
<PackageReference Include="PC.Framework" />
paket add PC.Framework --version 4.5.14
#r "nuget: PC.Framework, 4.5.14"
#:package PC.Framework@4.5.14
#addin nuget:?package=PC.Framework&version=4.5.14
#tool nuget:?package=PC.Framework&version=4.5.14
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(),
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, 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; }
}
}
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 (registered as {StaticResource ErrorLookup}):
<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>();
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 |
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.14 | 95 | 7/29/2026 |
| 4.5.13 | 149 | 7/15/2026 |
| 4.5.12 | 94 | 7/13/2026 |
| 4.5.11 | 84 | 7/13/2026 |
| 4.5.10 | 114 | 7/13/2026 |
| 4.5.9 | 91 | 7/11/2026 |
| 4.5.8 | 104 | 7/10/2026 |
| 4.5.7 | 105 | 7/10/2026 |
| 4.5.6 | 101 | 7/10/2026 |
| 4.5.5 | 95 | 7/10/2026 |
| 4.5.4 | 98 | 7/10/2026 |
| 4.5.3 | 110 | 7/6/2026 |
| 4.5.2 | 104 | 7/1/2026 |
| 4.5.1 | 110 | 6/29/2026 |
| 4.5.0 | 110 | 6/23/2026 |
| 4.4.0 | 667 | 7/31/2025 |
| 4.3.9 | 260 | 6/16/2025 |
| 4.3.8 | 255 | 6/16/2025 |
| 4.3.7 | 412 | 5/5/2025 |
| 4.3.6 | 273 | 3/18/2025 |