Migrating from NaluShell to Scaffold
The Scaffold replaces MAUI Shell as the host; your navigation code does not change —
same INavigationService, same relative/absolute navigations, same page models, intents,
guards and lifecycle events. The migration is a re-declaration of the app structure plus
swapping a few Shell-specific chrome features for their Scaffold equivalents.
Concept map
| NaluShell / Shell | Scaffold |
|---|---|
NaluShell subclass |
Scaffold subclass |
TabBar + Tab + ShellContent |
ScaffoldTabBar + ScaffoldRoot |
ShellContent nalu:Navigation.PageType="..." |
ScaffoldRoot PageType="{x:Type ...}" |
| Shell flyout | Scaffold.FlyoutStart/FlyoutEnd + ScaffoldFlyoutMenuView |
Shell.TitleView |
nalu:Scaffold.TitleView (page-attached) |
Shell.NavBarIsVisible |
nalu:Scaffold.IsNavBarVisible |
| Shell tab bar visibility | nalu:Scaffold.TabBarVisibility (Auto/Visible/Hidden) |
NaluTabBar (UseNaluTabBar + SetTabBarView) |
Built-in default tab bar (or ScaffoldTabBar.TabBarView) |
Shell modal routes / PresentationMode |
nalu:Scaffold.PageMode (Modal/DismissableModal) |
Native nav bar styling (Shell.BackgroundColor, …) |
Scaffold.NavBarBackground/NavBarForeground/… attached properties (per-property page → area → scaffold merge) |
| Native page transitions | Scaffold.PageTransition specs + Scaffold.TransitionName shared elements |
Step by step
1. Packages & registration
builder
.UseMauiApp<App>()
.UseNaluNavigation<App>(nav => nav.AddPage<...>()) // unchanged
.UseNaluScaffold(); // NEW — registers the handler and services (IOverlayService, IScaffoldFlyoutController)
Remove UseNaluTabBar() if you used the custom Shell tab bar — the Scaffold's default bar
replaces it.
2. Re-declare the structure
Before (AppShell.xaml):
<nalu:NaluShell xmlns:nalu="https://nalu-development.github.com/nalu/navigation" ...>
<TabBar>
<Tab Title="Home" Icon="home.png">
<ShellContent nalu:Navigation.PageType="pages:HomePage" />
</Tab>
<Tab Title="Settings" Icon="gear.png">
<ShellContent nalu:Navigation.PageType="pages:SettingsPage" />
</Tab>
</TabBar>
</nalu:NaluShell>
After (AppScaffold.xaml — note the different xmlns):
<nalu:Scaffold xmlns:nalu="https://nalu-development.github.com/nalu/scaffold"
nalu:Scaffold.NavBarView="{nalu:ScaffoldNavBarView}" ...>
<nalu:ScaffoldTabBar>
<nalu:ScaffoldRoot Title="Home" Icon="home.png"
PageType="{x:Type pages:HomePage}" />
<nalu:ScaffoldRoot Title="Settings" Icon="gear.png"
PageType="{x:Type pages:SettingsPage}" />
</nalu:ScaffoldTabBar>
</nalu:Scaffold>
The code-behind loses the Shell plumbing — a Scaffold subclass needs no constructor
arguments (structure defines the initial selection; use InitialRootPageType to override):
public partial class AppScaffold : Scaffold
{
public AppScaffold() => InitializeComponent();
}
3. Host it
protected override Window CreateWindow(IActivationState? activationState)
=> new(_serviceProvider.GetRequiredService<AppScaffold>());
(Register AppScaffold as a singleton service.) The Android window-caching workaround
NaluShell needed is not required.
4. Move page-attached chrome properties
| Before | After |
|---|---|
Shell.TitleView |
nalu:Scaffold.TitleView — its BindingContext is the page model now |
Shell.NavBarIsVisible="False" |
nalu:Scaffold.IsNavBarVisible="False" |
| Tab bar hidden on pushed pages | opt-in per page: nalu:Scaffold.TabBarVisibility="Auto" (the default is Visible) |
The TitleView binding-context change is the most common migration fix: NaluShell's title
views bound against Shell internals; Scaffold title views bind your page model directly (and
{nalu:NavBarBinding} reaches the nav-bar ambient state when needed).
5. Navigation code — nothing to do
INavigationService, Navigation.Relative()..., intents, ILeavingGuard,
IEnteringAware/IAppearingAware etc. work unchanged. The Shell-specific relative-navigation
caveats (route registration, // prefixes) never applied to Nalu APIs, so there is nothing to
port.
INavigation-based automation (e.g. test drivers popping pages) keeps working: the Scaffold
installs a truthful INavigation bridge whose pops route through the engine.
6. Adopt what Shell couldn't do
Once migrated, the features that motivated the move are one attribute away — full-bleed headers with scroll-materializing bars (Nav Bar), shared-element push/pop (Transitions), popups/sheets without platform modals (Popups & Sheets), and automatic status-bar contrast (System Bars).
Behavior differences worth knowing
- Every navigation is engine-routed — including tab taps from chrome. If you relied on Shell's native tab switching bypassing guards, that no longer happens (by design).
- Safe areas: pages get the correct insets from the chrome automatically (bar footprints
contribute as insets unless
NavBarOverlapsContent); page content uses standardSafeAreaEdgesper edge. - Android back uses the predictive-back dispatcher (works on Android 16+ where Shell's legacy back channel is dead); root pages defer to the system back-to-home preview.
Page.OnBackButtonPressedis deliberately unsupported on hosted pages: it only fires for hardware back, so confirmation logic written there would be silently bypassed by on-screen pops. Move it toILeavingGuard— the single confirmation mechanism covering every leave path.- State preservation across tab switches matches NaluShell semantics (stacks kept alive); scroll offsets and entry text survive switches on both platforms.
- Shell-specific APIs (
Shell.Current, customShellItemtemplates, query-string routes) have no Scaffold counterpart — their use cases map to scaffold structure, intents, and the chrome APIs above.