A lightweight, framework-agnostic routing system for .NET UI applications.
Works with:
- Avalonia
- WPF
- WinUI / Uno
- MAUI
- WinForms (sim, atΓ© lΓ‘ se vocΓͺ quiser sofrer um pouco π)
Umbra Router was designed to provide a clean navigation pipeline with IoC-first resolution, guards, parameters, and view-model binding, without locking you into a specific UI framework.
The routing pipeline follows this strict order:
URL β Resolver β Guard β ViewModel β View β UI
Performance and predictability.
- Resolver is cheap (types only)
- Guards run BEFORE ViewModel creation
- ViewModel is only created if navigation is allowed
- View is only instantiated at the end
This avoids unnecessary allocations and initialization work.
Routes are registered via RoutesBuilder.
x.Register<HomePage, HomeViewModel>("home");x.UseAngularStyleRoutes(new RoutesAngularStyle
{
new RouteAngularStyle
{
Path = "sub",
Children =
{
new RouteAngularStyle
{
Path = "first",
Component = typeof(FirstSubPage),
ViewModel = typeof(FirstSubViewModel)
}
}
}
});Each route can define a title template:
SetTitle("Params {0}")Then navigation replaces {0} dynamically:
_history.NavigateAsync(
url: "example/params?page=2",
title: "2"
);Params 2
When you call:
_history.NavigateAsync("example/params?page=2");This is what happens internally:
- URL Parsing
- Route Resolver (IoC-based)
- Guard execution (CanMatch)
- Guard execution (CanDeactivate)
- ViewModel resolution
- View creation
- Binding (ConfigureTView)
- UI update event
Guards control navigation BEFORE ViewModel is created.
public abstract class NavigationGuardBase : IGuard
{
public async Task<GuardResult> ExecuteGuardAsync(NavigationContext context)
{
var result = await GuardAsync(context);
if (result.Decision == GuardDecision.Allow)
await OnGuardAllow(context);
else
await OnGuardDeny(context);
return result;
}
protected abstract Task<GuardResult> GuardAsync(NavigationContext context);
}x.Register<HomePage, HomeViewModel>("home")
.CanMatchGuard<AuthGuard>()
.CanDeactivateGuard<UnsavedChangesGuard>();CanMatchβ executed BEFORE ViewModel is createdCanDeactivateβ executed BEFORE leaving current page
If guard returns:
Allowβ navigation continuesDenyβ navigation stopsRedirectβ navigates to another route
This is where many people get confused.
Inside a ViewModel:
public override async Task OnNavigatedToAsync(CancellationToken ctx)
{
Username = Context.Query["name"];
Page = Context.Query["page"];
if (Context.Body.TryGetValue(out ParamsBody body))
Date = body.Date.ToString("hh:mm:ss");
}It is injected automatically by the router.
Each ViewModel that implements IRoutePage receives:
It contains:
Queryβ URL query string (?page=1)Bodyβ navigation payload objectPathβ route pathTitleβ computed titleRouteSnapshotβ full resolved route state
So nothing is "magic" β it's injected during ViewModel resolution.
Umbra Router does NOT assume how your UI binds.
Instead, you define it:
public class RouterHistory<TViewModel> : RouterHistoryBase<TViewModel, Control>
{
protected override void ConfigureTView(ref Control? view, TViewModel viewModel)
{
view.DataContext = viewModel;
}
}Because Umbra Router is framework-agnostic.
You decide:
- Avalonia β
DataContext - WPF β
DataContext - MAUI β
BindingContext - WinUI β
DataContext
The router does NOT enforce UI behavior.
services.AddUmbraRouter<Control, PageViewModelBase>(x =>
{
x.Register<HomePage, HomeViewModel>("home");
x.Register<ParamsPage, ParamsModelView>("example/params");
x.Register<Error404Page, Error404ViewModel>("**");
});Everything is resolved through DI:
- ViewModels
- Views
- Guards
- Router services
No new outside the container.
- Avoid heavy navigation frameworks
- Full control over pipeline
- Fast guard-first filtering
- No unnecessary ViewModel instantiation
- Framework-independent design
Think of it like a conveyor belt:
URL
β
Route Resolver (cheap lookup)
β
Guards (can I even continue?)
β
ViewModel creation (DI)
β
View creation (UI layer)
β
Binding injection
β
UI update event
If anything fails early β everything after is skipped.
- Minimal allocations
- Early rejection (guards first)
- IoC everywhere
- No UI coupling
- Explicit lifecycle hooks
If something looks like βmagicβ in this system, it's usually just:
dependency injection + structured context passing + strict pipeline order