Home > Software engineering >  How do I go to another view from a view in MVVM WPF?
How do I go to another view from a view in MVVM WPF?

Time:01-29

Here I have a WPF application that is made with the MVVM structure. I am fairly new to C# WPF and am not familiar with this concept. I am attempting to switch to another view through a function in one view via the press of a button.

Here is what the application looks like, enter image description here

Once the Login button is pressed a function is triggered that will validate the inputs and if valid switch to another view. Which would look like such,

enter image description here

File Structure

enter image description here

How can i switch the views ? Below are some code for reference.

MainWindow.xaml

<Window x:Class="QuizAppV2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:QuizAppV2"
        xmlns:viewModel="clr-namespace:QuizAppV2.MVVM.ViewModel"
        mc:Ignorable="d"
        Height="600" Width="920"
        WindowStartupLocation="CenterScreen"
        WindowStyle="None"
        ResizeMode="NoResize"
        Background="Transparent"
        AllowsTransparency="True">
    <Window.DataContext>
        <viewModel:MainViewModel/>
    </Window.DataContext>
    <Border Background="#272537"
            CornerRadius="20">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="75"/>
                <RowDefinition/>
                <RowDefinition Height="25"/>
            </Grid.RowDefinitions>

            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>

                <TextBlock Text="Online Quiz"
                            Grid.Column="1"
                            FontSize="20"
                            Foreground="White"
                            HorizontalAlignment="Center"
                            VerticalAlignment="Center"/>
                <StackPanel Grid.Column="2"
                            Margin="30,20"
                            Orientation="Horizontal"
                            HorizontalAlignment="Right"
                            VerticalAlignment="Top">

                    <Button Content="–"
                            Background="#00CA4E"
                            Style="{StaticResource UserControls}"
                            Click="Minimise"/>
                    <Button Content="▢"
                            Background="#FFBD44"
                            Style="{StaticResource UserControls}"
                            Click="Restore"/>
                    <Button Content="X"
                            Background="#FF605C"
                            Style="{StaticResource UserControls}"
                            Click="Exit"/>
                </StackPanel>
            </Grid>
            <ContentControl Grid.Column="1"
                            Grid.Row="1"
                            Margin="20,10,20,50"
                            Content="{Binding CurrentView}"/>
        </Grid>
    </Border>
</Window>

MainViewModel.cs

using QuizAppV2.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace QuizAppV2.MVVM.ViewModel
{
    class MainViewModel : ObservableObject
    {

        public RelayCommand LoginViewCommand { get; set; }
        public RelayCommand SubjectSelectionViewCommand { get; set; }
        public RelayCommand QuizViewCommand { get; set; }
        public RelayCommand ResultViewCommand { get; set; }

        public LoginViewModel LoginVM { get; set; }
        public SubjectSelectionViewModel SubjectSelectVM { get; set; }
        public QuizViewModel QuizVM { get; set; }
        public ResultViewModel ResultVM { get; set; }


        private object _currentView;

        public object CurrentView
        {
            get { return _currentView; }
            set
            {
                _currentView = value;
                onPropertyChanged();
            }
        }

        public MainViewModel()
        {
            LoginVM = new LoginViewModel();
            SubjectSelectVM = new SubjectSelectionViewModel();
            QuizVM = new QuizViewModel();
            ResultVM = new ResultViewModel();
            CurrentView = SubjectSelectVM;

            LoginViewCommand = new RelayCommand(o =>
            {
                CurrentView = LoginVM;
            });
            SubjectSelectionViewCommand = new RelayCommand(o =>
            {
                CurrentView = SubjectSelectVM;
            });
        }
    }
}

LoginView.xaml

using QuizAppV2.MVVM.ViewModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace QuizAppV2.MVVM.View
{
    /// <summary>
    /// Interaction logic for LoginView.xaml
    /// </summary>
    public partial class LoginView : UserControl
    {
        public LoginView()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            if (UsrId.Text == "" || UsrName.Text == "")
            {
                UsrIDErrMsg.Visibility = Visibility.Visible;
                UsrNameErrMsg.Visibility = Visibility.Visible;
            }
            else
            {
                UsrIDErrMsg.Visibility = Visibility.Hidden;
                UsrNameErrMsg.Visibility = Visibility.Hidden;
                MainWindow.currentUser = new Student(UsrId.Text, UsrName.Text);
                
            }
        }
    }
}

Thank you

CodePudding user response:

Navigation is a tricky topic there are few ways to do this but since you are new to WPF I tried to outline a simple technique, along the lines of the examples you've provided requirement is have to go from page to page, a simple idea would be to swap out the contents. What I mean by that is when the user clicks "Login" we authenticate the user and swap the LoginPage with some other page, in your case a quiz page, when the user selection any option we swap out the view with the next view and so on.

I've coded up a simple solution with Shell mechanism. Essentially we create a empty shell in MainWindow (i.e it has no UI) and we load pages into this empty shell using a NavigationService/Helper. I've gone with a singleton class here just for simplicity, there are 3 main Methods in this,

RegisterShell : This has to be the Window where the swapping will happen, this ideally needs to be set once.

Load View : Method which Swaps out old view with the new one, I have gone with user control for this as most of the sub views can be user control in WPF.

LoadViewWithCustomData : Similar to above but has more flexibilty since it allows you to supply extra data.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace Navigation
{
    class NavigationService
{
    /// <summary>
    /// Singleton so we keep on shell which views can use this to navigate to different pages.
    /// </summary>
    public static NavigationService Instance = new NavigationService();
    private MainWindow myShell;


    private NavigationService()
    {
    }

    /// <summary>
    /// Register the main shell so this service know where to swap the data out and in of
    /// </summary>
    /// <param name="theShell"></param>
    public void RegisterShell(MainWindow theShell)
    {
        this.myShell = theShell;
    }

    /// <summary>
    /// Swaps out any view to the shell.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public void LoadView<T>() where T : UserControl, new()
    {
        myShell.TheShell = new T();
    }

    /// <summary>
    /// Swaps out any view to the shell with custom data, here the user responsible to create UserControl with all the reqired data for the view.
    /// We can automate this via reflection if required.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="theNewControl"></param>
    public void LoadViewWithCustomData<T>(UserControl theNewControl) where T : UserControl, new()
    {
        myShell.TheShell = theNewControl;
    }
}

Now here's how my LoginPage looks, the important line here is NavigationService.Instance.LoadView<_4OptionQuizPage>() this essentially sends the user to _4OptionQuizPage.

    public partial class LoginPage : UserControl
        {
            public ICommand LoginClicked { get; }
            public LoginPage()
            {
                InitializeComponent();
                this.DataContext = this;
                LoginClicked = new SimpleCommand(OnLoginClicked);
            }
    
            private void OnLoginClicked()
            {
                // TODO : Authenticate user here.
    
                // Send the user to Quiz Page
                NavigationService.Instance.LoadView<_4OptionQuizPage>();
            }
        }

And in the _4OptionQuizPage we can have something like this, this is where the bulk of business logic may reside, I have 4 buttons here, 2 of them show message box but Button 1 sends you back to LoginPage and Button 2 reloads the same page with different data (i.e sending the user to next question)

public partial class _4OptionQuizPage : UserControl, INotifyPropertyChanged
    {
        public ICommand Option1Clicked { get; }
        public ICommand Option2Clicked { get; }
        public ICommand Option3Clicked { get; }
        public ICommand Option4Clicked { get; }
        private string myQuestion;
        public event PropertyChangedEventHandler PropertyChanged;
        public string Question
        {
            get { return myQuestion; }
            set 
            {
                myQuestion = value;
                NotifyPropertyChanged();
            }
        }
        public _4OptionQuizPage() : this($"Question Loaded At {DateTime.Now}, this can be anything.")
        {
        }
        public _4OptionQuizPage(string theCustomData)
        {
            InitializeComponent();
            Question = theCustomData; 
            this.DataContext = this;
            this.Option1Clicked = new SimpleCommand(OnOption1Clicked);
            this.Option2Clicked = new SimpleCommand(OnOption2Clicked);
            this.Option3Clicked = new SimpleCommand(OnOption3Clicked);
            this.Option4Clicked = new SimpleCommand(OnOption4Clicked);
        }
        private void OnOption4Clicked()
        {
            MessageBox.Show("Option 4 selected, Store the results");
        }
        private void OnOption3Clicked()
        {
            MessageBox.Show("Option 3 selected, Store the results");
        }
        private void OnOption1Clicked()
        {
            NavigationService.Instance.LoadView<LoginPage>();
        }
        private void OnOption2Clicked()
        {
            NavigationService.Instance.LoadViewWithCustomData<LoginPage>(new _4OptionQuizPage("A custom question to emulate custom data"));
        }
        private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

Finally your MainWindow would be registering the shell and sending the user to LoginPage, and it's XAML file should not have anything in it

    public partial class MainWindow : Window, INotifyPropertyChanged
{
    private object myShell;

    public object TheShell
    {
        get { return myShell; }

        set 
        { 
            myShell = value;
            this.NotifyPropertyChanged();
        }
    }

    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = this;
        NavigationService.Instance.RegisterShell(this);
        NavigationService.Instance.LoadView<LoginPage>();
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

}

MainWindow.xaml should be empty, essentially a shell for everything else.

<Window x:Class="Navigation.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Navigation"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800" Content="{Binding TheShell}">
</Window>

CodePudding user response:

You should move the navigation logic to a dedicated class and provide a reference to a shared instance to every class that should be able to navigate to a different view model class.

Alternatively, you can use routed commands that you handle on the MainWindow, which then delegates the command to the MainViewModel. In this scenario each button would have to pass the destination as CommandParameter. This solution allows the particular view models to not directly participate in the navigation.

Solution 1

NavigationService.cs

class NavigationService
{
  public RelayCommand QuizViewCommand { get; }
  public RelayCommand ResultViewCommand { get; }

  private QuizViewModel QuizVM { get; }
  private ResultViewModel ResultVM { get; }

  private object _currentView;
  public object CurrentView
  {
    get => _currentView;
    set
    {
      _currentView = value;
      OnPropertyChanged();
    }
  }

  public NavigationService()
  {
    QuizVM = new QuizViewModel(this);
    ResultVM = new ResultViewModel(this);

    CurrentView = QuizVM;

    QuizViewCommand = new RelayCommand(o => CurrentView = QuizVM);
    ResultViewCommand = new RelayCommand(o => CurrentView = ResultVM);
  }
}

MainViewModel.cs

class MainViewModel
{
  public NavigationService NavigationService { get; }

  public MainViewModel()
  {
    NavigationService = new NavigationService();
  }
}
```**LoginViewModel.cs**  
Every view model that participates in navigation must get a shared instance of the `NavigationService` (via constructor or property).
```c#
class QuizViewModel
{
  public NavigationService NavigationService { get; }

  public LoginViewModel(NavigationService navigationService)
  {
    NavigationService = navigationService;
  }
}

LoginView.xaml

<QuizView>
  <Button Content="Result View"
          Command="{Binding NavigationService.ResultViewCommand}" />
</QuizView>

MainWindow.xaml

<Window>
  <Window.DataContext>
    <MainViewModel />
  </Window.DataContext>

  <ContentControl Content="{Binding NavigationService.CurrentView}" />
</Window>

Solution 2 (recommended)

MainViewModel
To support validation (only navigate when the navigation source is valid)
let the view models implement INotifyDataErrorInfo.
The MainViewModel is the only view model class that knows how to navigate and the related details. All other view models can serve as destination but are completely agnostic of the navigation infrastructure.
Navigation is completely triggered by the view (using routed commands). This enables extensibility while keeping the implementation of the view model classes simple.

class MainViewModel : ObservableObject
{
  public object CurrentView { get; set; }
  private Dictionary<Type, INotifyPropertyChanged> ViewModelMap { get; }

  public MainViewModel() => this.ViewModelMap = new Dictionary<Type, INotifyPropertyChanged>
    {
      { typeof(QuizVm),  new QuizVm() },
      { typeof(ResultVm),  new ResultVm() },
    };

  // Check if destination type is valid.
  // In case the navigation source implements INotifyDataErrorInfo
  // check if the source is in a valid state
  public bool CanNavigate(Type navigationSourceType, Type navigationDestinationType)
    => CanNavigateAwayFrom(navigationSourceType) 
      && CanNavigateTo(navigationDestinationType);

  private bool CanNavigateAwayFrom(Type navigationSourceType) 
    => this.ViewModelMap.TryGetValue(navigationSourceType, out INotifyPropertyChanged viewModel)
      && viewModel is INotifyDataErrorInfo notifyDataErrorInfo
        ? !notifyDataErrorInfo.HasErrors
        : true;

  private bool CanNavigateTo(Type navigationDestinationType)
    => this.ViewModelMap.ContainsKey(navigationDestinationType);

  public void NavigateTo(Type destinationType)
  {
    if (this.ViewModelMap.TryGetValue(destinationType, out INotifyPropertyChanged viewModel))
    {
      this.CurrentView = viewModel;
    }
  }
}

MainWindow.xaml.cs

partial class MainWindow : Window
{
  public static RoutedCommand NavigateCommand { get; } = new RoutedUICommand(
    "Navigate to view command", 
    nameof(NavigateCommand), 
    typeof(MainWindow));

  private MainViewModel { get; }

  public MainWindow()
  {   
    InitializeComponent();

    this.MainViewModel = new MainViewModel();
    this.DataContext = this.MainViewModel;

    var navigateCommandBinding = new CommandBinding(MainWindow.NavigateCommand, ExecuteNavigateCommand, CanExecuteNavigateCommand);
    this.CommandBindings.Add(navigateCommandBinding);
  }

  private void CanExecuteNavigateCommand(object sender, CanExecuteRoutedEventArgs e)
  {
    Type navigationSourceType = (e.Source as FrameworkElement).DataContext.GetType();
    var navigationDestinationType = (Type)e.Parameter;
    e.CanExecute = this.MainViewModel.CanNavigate(navigationSourceType, navigationDestinationType);
  }

  private void ExecuteNavigateCommand(object sender, ExecutedRoutedEventArgs e)
  {
    var destinationViewModelType = (Type)e.Parameter;
    this.MainViewModel.NavigateTo(destinationViewModelType);
  }
}

MainWindow.xaml

<Window>
  <Window.Resources>
    <DataTemplate DataType="{x:Type local:QuizVM}">
      <QuizView />
    </DataTemplate>
  </Window.Resources>

  <ContentControl Content="{Binding CurrentView}" />
</Window>

QuizView.xaml
If the view needs to navigate, it must use the routed command (defined and handled in the MainWindow).
This way navigation will not pollute the view models and stays within the view.

<QuizView>
  <Button Content="Next"
          Command="{x:Static local:MainWindow.NextPageCommand}"
          CommandParameter="{x:Type local:ResultVM}"/>
</QuizView>

QuizVM.cs
Because this class does not directly participate in the navigation,
it doesn't have to implement any related commands or depend on any navigation service.
Navigation is completely controlled by the MainViewModel.

class QuizVM : INotifyPropertyChnaged, INotifyDataErrorInfo
{
}

CodePudding user response:

I suggest using "Datatemplate". Put in the main window resources the following:

<DataTemplate DataType="{x:Type viewmodel:QuizViewModel}">
            <local:QuizView/>
 </DataTemplate>
<DataTemplate DataType="{x:Type viewmodel:LoginViewModel}">
            <local:LoginView/>
 </DataTemplate>

and so on with the others...

  • Related