Navigation mit WPF und Mvvm

Veröffentlicht von

Heute wollen wir uns eine einfache Navigation mit Mvvm und WPF anschauen. Diese erlaubt uns, rein mit ViewModellen und Data Binding, zwischen verschiedenen Seiten der UI zu navigieren.

Das Projekt gibt es am Ende des Projektes zum Download. Ziel ist die Navigation zwischen den einzelnen Seiten der UI ohne Code-Behind. Unsere UI ist einfach gestrickt:

Mit den Buttons auf der linken Seite können wir zwischen den Seiten der Anwendung wechseln. Die Anwendung besteht aus dem ViewModel für das Hauptfenster und 3 ViewModels für die einzelnen Seiten.

Die ViewModels für die Seiten sind von AViewModel abgeleitet, welches wiederum von ViewModelBase abgeleitet ist. Man sieht bereits, die Anwendung verwendet das MvvmLight Framework.

public class AViewModel : ViewModelBase
{
    private string _name = "";

    public string Name
    {
        get
        {
            return _name;
        }
        set
        {
            Set(() => Name, ref _name, value);
        }
    }
}

Die Eigenschaft “Name” verwenden wir für die Navigation über die Buttons und setzen wir im Konstruktor des jeweiligen ViewModels:

public View1ViewModel()
{
    Name = "View 1";
}

ViewModel des Hauptfensters

Schauen wir uns nun das ViewModel des Hauptfensters an:

public class MainViewModel : ViewModelBase
{
    private RelayCommand<AViewModel> _changePageCommand;
    private AViewModel _currentPageViewModel;

    private List<AViewModel> _viewModelList = new List<AViewModel>();

    public List<AViewModel> ViewModelList
    {
        get
        {
            return _viewModelList;
        }
    }

    public AViewModel CurrentPageViewModel
    {
        get
        {
            return _currentPageViewModel;
        }
        set
        {
            Set(() => CurrentPageViewModel, ref _currentPageViewModel, value);
        }
    }

    public RelayCommand<AViewModel> ChangePageCommand { get; private set; }   

    public MainViewModel()
    {
        ChangePageCommand = new RelayCommand<AViewModel>(p => ChangePageAction(p));

        _viewModelList.Add(ViewModelLocator.Instance.View1);
        _viewModelList.Add(ViewModelLocator.Instance.View2);
        _viewModelList.Add(ViewModelLocator.Instance.View3);

        _currentPageViewModel = _viewModelList[0];
    }

    private void ChangePageAction(AViewModel viewModel)
    {
        CurrentPageViewModel = ViewModelList.FirstOrDefault(vm => vm == viewModel);
    }
}

Wir haben hier einen Command zum Wechseln der Seite. Eine Eigenschaft, welche das aktuelle ViewModel enthält, sowie eine List der verfügbaren ViewModels. Im Konstruktor legen wir die Liste an, setzen das erste ViewModels als Default und erstellen den Command.

Der Command nimmt einfach den Parameter, welcher vom Typ AViewModel sein muss, und weist dieses aus der Liste der Eigenschaft “aktuelles ViewModel” zu.

XAML-Code des Hauptfensters

Soweit so gut, schauen wir uns nun die GUI an. Die verfügbaren Seiten, bzw. deren Buttons, stellen wir über ein ItemControl dar:

<ItemsControl DockPanel.Dock="Left"
              ItemsSource="{Binding ViewModelList}"
              Margin="10"
              MinWidth="150">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Button Content="{Binding Name}"
                    Command="{Binding DataContext.ChangePageCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
                    CommandParameter="{Binding }"
                    Margin="0,10,0,0"
                    HorizontalAlignment="Stretch"></Button>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

Neue Seiten können so einfach über die Liste des ViewModels hinzugefügt werden. Der Button wird an das Command des ViewModels gebunden. Als Parameter wird das aktuelle ViewModel übergeben.

Die Darstellung der Seiten erfolgt in einem ContentControl:

<ContentControl x:Name="Content" Content="{Binding CurrentPageViewModel}"></ContentControl>

Dieses wird an das aktuelle ViewModel für die jeweilige Seite gebunden. Damit das ControlControl weiß, was es darstellen soll, erstellen wir für jedes ViewModel ein DataTemplate.

<Window.Resources>
    <DataTemplate DataType="{x:Type vm:View1ViewModel}">
        <local:View1></local:View1>
    </DataTemplate>
    <DataTemplate DataType="{x:Type vm:View2ViewModel}">
        <local:View2></local:View2>
    </DataTemplate>
    <DataTemplate DataType="{x:Type vm:View3ViewModel}">
        <local:View3></local:View3>
    </DataTemplate>
</Window.Resources>

Für jede Seite wird ein UserControl erstellt, welches wir hier im DataTemplate verwenden. Fertig! Mit jedem Button-Klick wird nun zwischen den Seiten der Anwendung gewechselt.

Update: Wechsel des ViewModels innerhalb eines Views

Es gab die Frage, in den Kommentaren, wie man den View aus einem View heraus wechseln kann. Hier gibt es verschiedene Möglichkeiten. Im Grunde müssen wir dem MainViewModel, welcher die Navigation für uns erledigt nur das ViewModel mitteilen, zu welchem gewechselt werden soll. Hier kommen verschiedene Möglichkeiten in Fragen: Events, direkter Verweis und Aufruf oder auch die Messenger-Funktionen von MVVM light.

Letztere habe ich verwendet, um die Funktion zu implementieren. Das Projekt habe ich ebenfalls aktualisiert. In unserem Beispiel wollen wir mit einem Klick auf einen Button von View3 zu Views2 navigieren.

<Button Grid.Row="1"
Command="{Binding ChangeViewCommand}"
Content="Change to view 2"
Margin="50"></Button>

Der Button ist an einen Command gebunden. Dieser versendet eine Nachricht mit dem Messenger von MVVMlight:

ChangeViewCommand = new RelayCommand(() =>
{
    Messenger.Default.Send(new ChangeViewMessage(ViewModelLocator.Instance.View2));
});

Der Messenger verschickt eine “ChangeViewMessage”, welche als Parameter das Ziel-View-Model übernimmt. In unserem Fall übergeben wir das ViewModel von “View2”. Die Nachricht an sich ist eine einfache Klasse:

public class ChangeViewMessage
{
    public AViewModel TargetViewModel { get; private set; }
    public ChangeViewMessage(AViewModel viewModel)
    {
        TargetViewModel = viewModel;
    }
}

Nun registrieren wir uns noch im MainWindowViewModel auf diese Nachricht.

//register message for switching the view
Messenger.Default.Register<ChangeViewMessage>(this, (msg) =>
{
    CurrentPageViewModel = msg.TargetViewModel;
});

Hier können wir dann aus der Nachricht das Ziel-View-Model auslesen und setzen. Damit wird über den Button entsprechend zum anderen View gesprungen.

Update: Anzeige, welcher View gerade aktiv ist

In den Kommentaren kam die Frage, ob man auch irgendwie kenntlich machen kann, welcher View gerade aktiv ist. Im Beispiel wollen wir, dass die Schrift des gerade aktiven Views und Buttons fett wird.

Wie realisieren wir dies? Zuerst erweitern wir unser Template für den Button:

<Button.FontWeight>
<MultiBinding Converter="{StaticResource FontWeightConverter}">
    <Binding Path="DataContext.CurrentPageViewModel"
             RelativeSource="{RelativeSource AncestorType={x:Type Window}}"></Binding>
    <Binding Path="."></Binding>
</MultiBinding>
</Button.FontWeight>

Hier verwenden wir einen MultiBinding und binden das CurrentViewModel und das ViewModel des Buttons an diesen. Jeder Button hat ja sein ViewModel zugeordnet (z.B. der View1 Button hat das View1ViewlModel zugeordnet. Jetzt implementieren wir den Converter:

public class NavigationViewModelFontWeightConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values[0] == values[1])
            return FontWeights.Bold;

        return FontWeights.Regular;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Dieser prüft, ob das aktuelle ViewModel dem des Buttons entspricht. Die beiden Bindings werden hier als Object-Array übergeben.

Ist das der Fall, dann wird die Schrift auf “Bold” gesetzt:

Ist dies nicht der Fall und die aktuelle ViewModel im MainWindowViewModel entspricht nicht dem des Buttons, dann wird die Schriftart auf normal gesetzt:

Download des Beispiels

Download

18 Kommentare

    1. Das sollte schon gehen, das klingt eher danach, als ob Dir das nuget Paket für MVVM light fehlt und er das nicht automatisch herunterlädt.

  1. Hallo, ich habe ein anderes Problem,
    wie kann ich von zum Beispiel View2 zu View3 wechseln?

    Es wäre schön, wenn mir dabei jemand behilflich sein könnte.

    Vielen Dank.
    Manu

    1. Du weist einfach das entsprechende ViewModel mit der Methode zu:

      ChangePageAction(AViewModel viewModel);

      Also kurz gesagt:

      ChangePageAction(viewModel3);

      1. Hallo andy,

        das geht doch aber nur aus dem MainViewModel, richtig?

        Ich habe folgendes Problem:
        Ich möchte aus dem View2ViewModel heraus View3 im ContentControl der MainWindow.xaml laden.

        Ich hoffe, dass es dafür eine Lösung gibt.

        Vielen Dank für deine Hilfe.

        Manu

          1. Hallo Andy,

            das Beispiel hat gut weiter geholfen. Vielen Dank dafür.
            Noch eine Frage. Gibt es eine Möglichkeit den Buttons die Buttons der aktiven Seite als aktiv zu kennzeichen? Mit einer anderen Farbe z.B.

            Vielen Dank Manu

          2. Hallo Manu,
            ich habe das Beispiel erweitert. Nicht die Farbe, sondern die Schrift, aber mit Farbe funktioniert es ähnlich. 🙂
            Gruß
            Andy

  2. Hallo Andy,

    ich hoffe, ich nerve nicht. Deine Beispiele helfen mir sehr gut weiter.
    Ich möchte in der MainWindow.xaml die Menüpunkte optisch gern ansprechend gestalten.
    Sie sollen als Grafik dargestellt werden und eine MouseOver-Grafik bekommen.
    Dazu möchte ich sie nicht mehr generieren lassen, sondern selbst einfügen.
    Was muss ich im CommandParameter angeben, dass mir das richtige ViewModel geladen wird?

    Viele Grüße
    Manu

    1. Sorry das ich mich erst jetzt melde, ich habe meist viel zu tun. Ich würde das vermutlich nicht mit manuellen Buttons lösen, sondern mit einem Untertemplate. Aber Du kannst auch einfach die Buttons von Hand erstellen. In vielen Fällen kannst Du auch einfach das On_Click Event umsetzen und das ViewModel aufrufen vom Code-Behind. Das ist meist einfacher als viel Xaml-Gezauber.

  3. Hallo Andy,

    ich bin es wieder mal.
    Ich möchte gern dynamisch bestimmte Buttons aus der Navigation aus- und einblenden können.
    Dafür habe ich boolsche Variablen eingebaut, mit denen man die Einstellungen in einer Setup-Datei speichern kann.
    Das funktioniert auch, wenn die Anwendung neu gestartet wird. Die entsprechenden Menüpunkte werden dann im MainViewModel nicht der _viewModelList hinzugefügt.
    Wie kann ich das aber dynamisch machen? Ich möchte gern die Buttons beim Ändern der Einstellungen aus- und wieder einblenden können.

    Viele Grüße
    Manu

  4. Hallo Andy,

    kann man dein Projekt so weiter entwickeln, dass daraus ein Multi-Level-Menu (Button) wird und das “CurrentPageViewModel” als TapControl genutzt werden kann?
    ich zerbreche mir darüber schon länger den Kopf.

    Danke und Gruß
    Mario

      1. ich denke mir das so, dass das eine Art Dropdown-Menu wird, wo mehrere “Untermenus” möglich sind, als auch direkt ViewModels angesprochen werden.
        ähnlich der Treeview-Ansicht bei der Ordnerstruktur im Dateimanager.

        hilft dir das?

        Gruß
        Mario

        1. Ja ok, ja ich denke das geht problemlos. Ist ja im Prinzip nichts anderes. Jedem Element im TreeView würde ich das entsprechende ViewModel zuordnen, welches dann dynamisch geladen werden kann.

          Gruß
          Andy

          1. Hallo Andy,

            kannst du mir das zeigen? ich bin mir bei der Umsetzung nicht im klaren wie.

            Gruß
            Mario

Kommentar hinterlassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert