Navigation in WinUI 3: NavigationView, Frame und ViewModel-Loading (Teil 8)

Eine Desktop-Anwendung hat mehrere Ansichten: Übersichten, Detailseiten, Einstellungen. WinUI 3 bietet dafür zwei zentrale Bausteine: NavigationView als Menü-Shell und Frame als Container für den Seiteninhalt. In EchoPlay arbeiten beide zusammen, um eine saubere, erweiterbare Navigation zu ermöglichen — ohne Frameworks, ohne Magic.

NavigationView: Die Shell

NavigationView ist das Navigationsmenü am linken Rand der Anwendung. Es enthält Menüeinträge, einen optionalen Einstellungen-Eintrag, einen Zurück-Button und den eigentlichen Seiteninhalt. Das <Frame> innerhalb der NavigationView ist der eigentliche Anzeigebereich — die NavigationView selbst zeigt nur das Menü, der Frame zeigt die aktuelle Seite.

<NavigationView
    x:Name="NavView"
    PaneDisplayMode="Left"
    IsSettingsVisible="True"
    IsBackButtonVisible="Auto"
    SelectionChanged="OnSelectionChanged"
    BackRequested="OnBackRequested"
    Loaded="OnLoaded">

    <NavigationView.MenuItems>
        <NavigationViewItem
            x:Name="NavBibliothek"
            Content="Bibliothek"
            Tag="Bibliothek">
            <NavigationViewItem.Icon>
                <FontIcon Glyph="&#xE8F1;" />
            </NavigationViewItem.Icon>
        </NavigationViewItem>
    </NavigationView.MenuItems>

    <Frame x:Name="ContentFrame" />
</NavigationView>

Jeder Menüeintrag hat ein Tag-Attribut, das später im Code-Behind ausgelesen wird, um zu entscheiden, zu welcher Seite navigiert werden soll. Das ist ein einfaches Mapping-Schema ohne hartcodierte Typen im XAML — flexibel und leicht erweiterbar.

SelectionChanged: Navigation auslösen

Wenn ein Nutzer auf einen Menüeintrag klickt, feuert SelectionChanged. Im Code-Behind wird dann zum passenden Page-Typ navigiert.

private void OnSelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
{
    // Einstellungen-Eintrag ist ein Sonderfall in NavigationView
    if (args.IsSettingsSelected)
    {
        NavigateTo(typeof(SettingsPage));
        return;
    }

    if (args.SelectedItem is NavigationViewItem item)
    {
        Type? pageType = (item.Tag as string) switch
        {
            "Bibliothek" => typeof(SeriesListPage),
            _ => null
        };

        if (pageType is not null)
        {
            NavigateTo(pageType);
        }
    }
}

private void NavigateTo(Type pageType)
{
    // Nicht nochmals navigieren, wenn die Seite bereits aktiv ist
    if (ContentFrame.CurrentSourcePageType == pageType)
    {
        return;
    }

    ContentFrame.Navigate(pageType);
}

Der Einstellungen-Eintrag ist in NavigationView eingebaut und wird über das Flag IsSettingsSelected erkannt — er kommt nicht als normales NavigationViewItem zurück. Ohne diese Prüfung würde der Klick auf Einstellungen ins Leere laufen. Die Doppelnavigation wird ebenfalls verhindert: Wenn die Seite bereits aktiv ist, passiert nichts. Ohne diese Prüfung würde ein erneuter Klick auf Bibliothek eine neue Instanz der SeriesListPage erzeugen, das ViewModel neu laden und den Scroll-Zustand zurücksetzen.

Frame und die Navigationshistorie

Der Frame hat eine eingebaute Navigationshistorie. Jeder Navigate-Aufruf fügt die aktuelle Seite auf den Back-Stack. Mit GoBack() kommt man zur vorherigen Seite zurück.

private void OnBackRequested(NavigationView sender, NavigationViewBackRequestedEventArgs args)
{
    if (ContentFrame.CanGoBack)
    {
        ContentFrame.GoBack();
    }
}

Der Zurück-Button der NavigationView wird automatisch ein- und ausgeblendet, wenn IsBackEnabled gesetzt wird. Das passiert nach jeder Navigation über ein Event:

// Nach jeder Navigation den Zurück-Button aktualisieren
ContentFrame.Navigated += (_, _) => NavView.IsBackEnabled = ContentFrame.CanGoBack;

ContentFrame.CanGoBack ist true, wenn es Einträge im Back-Stack gibt. Wenn die erste Seite (Bibliothek) angezeigt wird, gibt es keinen Back-Stack — der Button ist ausgeblendet.

Pages laden ihr ViewModel selbst

WinUI 3 hat keine eingebaute ViewModel-Injection für Pages. Wenn ContentFrame.Navigate(typeof(SeriesListPage)) aufgerufen wird, erzeugt WinUI ein neues SeriesListPage-Objekt mit dem Standardkonstruktor. EchoPlay nutzt dafür App.Services, um das ViewModel aus dem DI-Container zu holen:

public sealed partial class SeriesListPage : Page
{
    private readonly SeriesListViewModel _viewModel;

    public SeriesListPage()
    {
        InitializeComponent();
        _viewModel = App.Services.GetRequiredService<SeriesListViewModel>();
        DataContext = _viewModel;
    }

    protected override async void OnNavigatedTo(NavigationEventArgs e)
    {
        base.OnNavigatedTo(e);
        await _viewModel.LoadAsync();
    }
}

OnNavigatedTo ist die richtige Stelle, um Daten zu laden — nicht der Konstruktor. Der Konstruktor wird aufgerufen, bevor die Seite überhaupt gerendert wird. OnNavigatedTo wird aufgerufen, nachdem die Navigation abgeschlossen ist und die UI bereit ist.

Beim Start die erste Seite setzen

OnLoaded wird aufgerufen, sobald die NavigationView vollständig gerendert ist. Hier wird der erste Menüeintrag programmatisch ausgewählt:

private void OnLoaded(object sender, RoutedEventArgs args)
{
    NavView.SelectedItem = NavBibliothek;
}

Das löst SelectionChanged aus, das wiederum zu SeriesListPage navigiert. Das Setzen von SelectedItem in OnLoaded ist die idiomatische WinUI-3-Methode für die Startnavigation.

Was nicht funktioniert

Navigation im Konstruktor ist ein häufiger Fehler: Das Frame ist beim ersten Konstruktor-Aufruf noch nicht bereit. Navigationsversuche dort werden ignoriert oder werfen Exceptions. Genauso problematisch ist Navigate ohne Frame-Konfiguration — ein ContentFrame.Navigate(typeof(SeriesListPage)) ohne ein <Frame> im XAML führt zu einer NullReferenceException.

WinUI 3 bevorzugt außerdem x:Bind gegenüber {Binding}. Bei x:Bind wird der DataContext als typisierter Accessor auf dem Code-Behind-Objekt erwartet, nicht über den generischen DataContext. Die elegantere Variante sieht so aus:

// XAML: x:DataType="vm:SeriesListViewModel"
// Code-Behind:
public SeriesListViewModel ViewModel { get; }
<TextBlock Text="{x:Bind ViewModel.SearchText, Mode=TwoWay}" />

Das erlaubt typsichere Bindungen mit Compile-Prüfung in XAML — Tippfehler in Property-Namen fallen beim Kompilieren auf, nicht erst zur Laufzeit.

Die gezeigten Code-Beispiele dienen zur Veranschaulichung. Nutzung auf eigene Verantwortung. Mehr dazu