Skip to content

JaymeFernandes/Umbra.Router.Core

Β 
Β 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

39 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ“¦ Umbra.Router.Core

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 πŸ˜„)

✨ Overview

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.


⚑ Core Idea

The routing pipeline follows this strict order:

URL β†’ Resolver β†’ Guard β†’ ViewModel β†’ View β†’ UI

Why this order?

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.


🧠 Key Concepts

1. Route Registration

Routes are registered via RoutesBuilder.

Simple registration

x.Register<HomePage, HomeViewModel>("home");

Angular-style nested routes

x.UseAngularStyleRoutes(new RoutesAngularStyle
{
    new RouteAngularStyle
    {
        Path = "sub",
        Children =
        {
            new RouteAngularStyle
            {
                Path = "first",
                Component = typeof(FirstSubPage),
                ViewModel = typeof(FirstSubViewModel)
            }
        }
    }
});

2. Title System (Important)

Each route can define a title template:

SetTitle("Params {0}")

Then navigation replaces {0} dynamically:

_history.NavigateAsync(
    url: "example/params?page=2",
    title: "2"
);

Result:

Params 2

3. Navigation Flow

When you call:

_history.NavigateAsync("example/params?page=2");

This is what happens internally:

πŸ” Pipeline

  1. URL Parsing
  2. Route Resolver (IoC-based)
  3. Guard execution (CanMatch)
  4. Guard execution (CanDeactivate)
  5. ViewModel resolution
  6. View creation
  7. Binding (ConfigureTView)
  8. UI update event

πŸ›‘οΈ Guards System

Guards control navigation BEFORE ViewModel is created.

Base class

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);
}

Example usage

x.Register<HomePage, HomeViewModel>("home")
 .CanMatchGuard<AuthGuard>()
 .CanDeactivateGuard<UnsavedChangesGuard>();

Guard lifecycle

  • CanMatch β†’ executed BEFORE ViewModel is created
  • CanDeactivate β†’ executed BEFORE leaving current page

If guard returns:

  • Allow β†’ navigation continues
  • Deny β†’ navigation stops
  • Redirect β†’ navigates to another route

πŸ“¦ ViewModel Context (IMPORTANT)

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");
}

❓ Where does Context come from?

It is injected automatically by the router.

Each ViewModel that implements IRoutePage receives:

NavigationContext

It contains:

  • Query β†’ URL query string (?page=1)
  • Body β†’ navigation payload object
  • Path β†’ route path
  • Title β†’ computed title
  • RouteSnapshot β†’ full resolved route state

So nothing is "magic" β€” it's injected during ViewModel resolution.


🧩 View Binding (Framework Adapter)

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;
    }
}

Why this exists?

Because Umbra Router is framework-agnostic.

You decide:

  • Avalonia β†’ DataContext
  • WPF β†’ DataContext
  • MAUI β†’ BindingContext
  • WinUI β†’ DataContext

The router does NOT enforce UI behavior.


🧭 Router Registration Example

services.AddUmbraRouter<Control, PageViewModelBase>(x =>
{
    x.Register<HomePage, HomeViewModel>("home");
    x.Register<ParamsPage, ParamsModelView>("example/params");

    x.Register<Error404Page, Error404ViewModel>("**");
});

πŸ’‘ IoC First Design

Everything is resolved through DI:

  • ViewModels
  • Views
  • Guards
  • Router services

No new outside the container.


πŸš€ Why this router exists

  • Avoid heavy navigation frameworks
  • Full control over pipeline
  • Fast guard-first filtering
  • No unnecessary ViewModel instantiation
  • Framework-independent design

πŸ§ͺ Mental Model

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.


🧼 Design Philosophy

  • Minimal allocations
  • Early rejection (guards first)
  • IoC everywhere
  • No UI coupling
  • Explicit lifecycle hooks

πŸ“Œ Final Note

If something looks like β€œmagic” in this system, it's usually just:

dependency injection + structured context passing + strict pipeline order

About

Cross platform view (model) router library for AvaloniaUI

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages