WPF Bootcamp

Navigation and Application Composition

Single-screen demos teach syntax. Real WPF applications require composition: multiple views, shared services, navigation flow, shell layout, and a stable boundary between domain state and screen state.

This lesson is about organizing a WPF app so it can grow without becoming a tangle of direct control references and singleton shortcuts.

What you'll learn

  • How windows, pages, frames, user controls, and shells differ.
  • How navigation services and region-like composition patterns keep screens decoupled.
  • How shared services, dialogs, and global state should be introduced carefully.
  • How to scale from one view model and one screen to a whole application without collapsing boundaries.

Shell

The top-level host that owns app chrome, shared navigation, toolbars, and long-lived services.

Feature view

A focused screen or panel with its own view model, templates, and bounded responsibilities.

The mental model in one sentence

Application composition is about deciding which parts of the UI are long-lived hosts, which parts are replaceable features, and which infrastructure concerns should be accessed through explicit services rather than direct view-to-view coupling.

Shell owns continuity

Navigation chrome, global commands, shared menus, and durable services usually belong here.

Feature views own focus

A feature screen should solve one bounded problem with its own view model and state.

Know the major host types

Window

A top-level host with its own lifetime, chrome, and independent position on screen.

Page and Frame

Useful when you want navigation-like screen replacement inside a host rather than multiple separate windows.

UserControl

A reusable piece of composed UI, often embedded inside larger screens or shells.

Shell

The top-level composition root for navigation areas, persistent chrome, and shared services.

These are not interchangeable. A shell is an architectural role. A window is a host surface. A user control is usually a reusable piece inside something larger.

Why a shell matters

As applications grow, they need a place that owns the durable parts of the experience: top-level menus, navigation affordances, shared commands, and app-wide services.

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <local:AppToolbar />
    <ContentControl Grid.Row="1"
                    Content="{Binding CurrentScreen}" />
</Grid>

That shell pattern gives the app a stable outer frame while still allowing the main content region to change over time.

Composition rules that scale

  • Keep view models small and feature-oriented.
  • Let a shell coordinate navigation rather than having child views directly create other views.
  • Wrap infrastructure concerns such as dialogs, navigation, and persistence behind services.
  • Prefer explicit composition over hidden global dependencies.
public interface INavigationService
{
    void ShowCustomerList();
    void ShowCustomerEditor(Guid customerId);
}

That interface is not about ceremony. It is about giving feature view models a stable way to request navigation without constructing views or knowing shell internals.

Navigation should be a service, not a direct control reference

When one child view directly creates or manipulates another child view, the application becomes tightly coupled and hard to evolve. Navigation is cleaner when it is expressed as an intent through a service or composition boundary.

public interface INavigationService
{
    void ShowCustomerList();
    void ShowCustomerEditor(Guid customerId);
}

A view model can request "show the editor for this customer" without knowing whether that means replacing content in a shell, opening a dialog, navigating a frame, or swapping a region.

Shared services should stay explicit

Real applications need shared services for things like dialogs, persistence, logging, settings, and user-session state. The problem is not that these services exist. The problem is when they become invisible global dependencies.

Good pattern

Inject or explicitly provide the focused services a feature needs.

Bad pattern

Let any view or view model reach into a global singleton bag of app state for everything.

Explicit dependencies make it easier to reason about who owns state and which screens are allowed to coordinate which parts of the application.

Dialogs, shared state, and boundaries

Dialogs and transient flows are part of composition too. A screen should usually request a dialog through a service boundary rather than directly constructing modal UI in the middle of business logic.

Likewise, truly shared state should be introduced carefully. Some state belongs to the current feature screen. Some belongs to the session or application. Mixing those levels carelessly creates confusing lifetimes and hidden coupling.

Region-like composition patterns

Large WPF applications often define replaceable areas in a shell and let navigation or composition logic decide which view appears there.

<Grid>
    <local:SidebarRegion />
    <ContentControl Content="{Binding ActiveWorkspace}" />
</Grid>

The exact framework or implementation can vary, but the idea is stable: the shell owns slots, and features are composed into those slots rather than reaching across the app to instantiate each other directly.

Feature-oriented view models scale better

A feature view model should represent one bounded screen or workflow, not the whole application.

  • A customer list feature can own list state, filters, selection, and commands relevant to that screen.
  • A customer editor feature can own edit state, validation, and save/cancel actions for that workflow.
  • The shell can coordinate which one is active without absorbing all of their state into one giant view model.

This keeps screens isolated and makes navigation feel like composition of features instead of mutation of one monolithic object.

A worked composition example

public interface INavigationService
{
    void ShowCustomerList();
    void ShowCustomerEditor(Guid customerId);
}
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <local:AppToolbar />
    <ContentControl Grid.Row="1"
                    Content="{Binding CurrentScreen}" />
</Grid>
  1. The shell owns the top-level layout and app chrome.
  2. The shell exposes a current screen or navigation target.
  3. A feature view model requests navigation through a service.
  4. The shell or navigation service swaps the active feature into the main region.
  5. Each feature keeps its own bounded state instead of sharing one giant mutable view model.

This is how a WPF application scales from "one screen that works" to "multiple screens that remain understandable".

Common mistakes

  • Letting child views locate and instantiate other views directly.
  • Keeping too much shared mutable state in a singleton instead of passing focused context.
  • Using one giant shell view model for the whole application.
  • Confusing reusable user controls with full application-level composition boundaries.
  • Letting infrastructure concerns leak directly into feature views instead of wrapping them behind services.

InkkSlinger parity note: InkkSlinger adds hosting concerns because UI lives inside a MonoGame app, but the composition rules remain useful: isolate screens, keep navigation explicit, and treat the host integration as infrastructure rather than screen logic.

Next, finish the bootcamp with an end-to-end walkthrough that combines the concepts into one coherent screen.