2

I'm attempting to create a class & method which could be used on any window and page to change the current page displayed on the MainWindow window.

So far I got:

class MainWindowNavigation : MainWindow
{
    public MainWindow mainWindow;

   public void ChangePage(Page page)
    {
        mainWindow.Content = page;
    }
}

The main window itself:

public MainWindow()
    {
        InitializeComponent();
        MainWindowNavigation mainWindow = new MainWindowNavigation();
        mainWindow.ChangePage(new Pages.MainWindowPage());
    }

Unfortunately this ends up with System.StackOverflowException.

The main reason for creating this is that I want to be able to change the mainWindow.Content from a page which is currently displayed in mainWindow.Content.

I have already reviewed MVVM but I don't think it is worth using it for a small application like this as all I want it to do is Display a Welcome Page on open, then on the side there will be few buttons. Once pressed the mainWindow.Content correctly changes to a page where a user can enter login detail and then on the button press on the login page I want to change the mainWindow.Content to a different page on successful validation of the login details entered.

BionicCode
  • 1
  • 4
  • 28
  • 44
Pawel
  • 141
  • 1
  • 10

2 Answers2

11

Using MVVM is absolutely fine as it will simplify the implementation of your requirement. WPF is build to be used with the MVVM pattern, which means to make heavy use of data binding and data templates.

The task is quite simple. Create a UserControl (or DataTemplate) for each view e.g., WelcomePage and LoginPage with their corresponding view models WelcomePageViewModel and LoginPageViewModel.

A ContentControl will display the pages.
The main trick is that, when using an implicit DataTemplate (a template resource without an x:Key defined), the XAML parser will automatically lookup and apply the correct template, where the DataType matches the current content type of a ContentControl. This makes navigation very simple, as you just have to select the current page from a collection of page models and set this page via data binding to the Content property of the ContentControl or ContentPresenter:

Usage

MainWindow.xaml

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

  <Window.Resources>
    <DataTemplate DataType="{x:Type WelcomePageviewModel}">
      <WelcomPage />
    </DataTemplate>

    <DataTemplate DataType="{x:Type LoginPageviewModel}">
      <LoginPage />
    </DataTemplate>
  </Window.Resources>

  <StackPanel>
  
    <!-- Page navigation -->
    <StackPanel Orientation="Horizontal">
      <Button Content="Show Login Screen" 
              Command="{Binding SelectPageCommand}" 
              CommandParameter="{x:Static PageName.LoginPage}" />
      <Button Content="Show Welcome Screen" 
              Command="{Binding SelectPageCommand}" 
              CommandParameter="{x:Static PageName.WelcomePage}" />
    </StackPanel>
  
    <!-- 
      Host of SelectedPage. 
      Automatically displays the DataTemplate that matches the current data type 
    -->
    <ContentControl Content="{Binding SelectedPage}" />
  <StackPanel>
</Window>

Implementation

  1. Create the individual page controls (the controls that host the page content). This can be a Control, UserControl, Page or simply a plain DataTemplate:

WelcomePage.xaml

    <UserControl>
      <StackPanel>
        <TextBlock Text="{Binding PageTitle}" />
        <TextBlock Text="{Binding Message}" />
      </StackPanel>
    </UserControl>

LoginPage.xaml

    <UserControl>
      <StackPanel>
        <TextBlock Text="{Binding PageTitle}" />
        <TextBox Text="{Binding UserName}" />
      </StackPanel>
    </UserControl>
  1. Create the page models:

IPage.cs

    interface IPage : INotifyPropertyChanged
    {
      string PageTitel { get; set; }
    }

WelcomePageViewModel.cs

    class WelcomePageViewModel : IPage
    {
      private string pageTitle;   
      public string PageTitle
      {
        get => this.pageTitle;
        set 
        { 
          this.pageTitle = value; 
          OnPropertyChanged();
        }
      }

      private string message;   
      public string Message
      {
        get => this.message;
        set 
        { 
          this.message = value; 
          OnPropertyChanged();
        }
      }

      public WelcomePageViewModel()
      {
        this.PageTitle = "Welcome";
      }

      public event PropertyChangedEventHandler PropertyChanged;
      protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
      {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
      }
    }

LoginPageViewModel.cs

    class LoginPageViewModel : IPage
    {
      private string pageTitle;   
      public string PageTitle
      {
        get => this.pageTitle;
        set 
        { 
          this.pageTitle = value; 
          OnPropertyChanged();
        }
      }

      private string userName;   
      public string UserName
      {
        get => this.userName;
        set 
        { 
          this.userName = value; 
          OnPropertyChanged();
        }
      }

      public LoginPageViewModel()
      {
        this.PageTitle = "Login";
      }

      public event PropertyChangedEventHandler PropertyChanged;
      protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
      {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
      }
    }
  1. Create an enumeration of page identifiers (to eliminate magic strings in XAML and C#):

PageName.cs

    public enum PageName
    {
      Undefined = 0, WelcomePage, LoginPage
    }
  1. Create the MainViewModel which will manage the pages and their navigation:

MainViewModel.cs
An implementation of RelayCommand can be found at
Microsoft Docs: Patterns - WPF Apps With The Model-View-ViewModel Design Pattern - Relaying Command Logic

    class MainViewModel
    {
      public ICommand SelectPageCommand => new RelayCommand(SelectPage);

      private Dictionary<PageName, IPage> Pages { get; }

      private IPage selectedPage;   
      public IPage SelectedPage
      {
        get => this.selectedPage;
        set 
        { 
          this.selectedPage = value; 
          OnPropertyChanged();
        }
      }

      public MainViewModel()
      {
        this.Pages = new Dictionary<PageName, IPage>
        {
          { PageName.WelcomePage, new WelcomePageViewModel() },
          { PageName.LoginPage, new LoginPageViewModel() }
        };

        this.SelectedPage = this.Pages.First().Value;
      }

      public void SelectPage(object param)
      {
        if (param is PageName pageName 
          && this.Pages.TryGetValue(pageName, out IPage selectedPage))
        {
          this.SelectedPage = selectedPage;
        }
      }

      public event PropertyChangedEventHandler PropertyChanged;
      protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) 
        => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
BionicCode
  • 1
  • 4
  • 28
  • 44
  • 4
    I am coming back to this over and over, every time I have to do something with WPF. It is a really nice starting point tutorial. – herrwolken Jan 22 '21 at 13:00
  • Are the welcome window will be destroy upon change from welcome to login? – Luiey Apr 09 '22 at 23:54
  • @Luiey It depends. The state of the page is stored in the page models. In this example the pages are not "destroyed" as the models are stored in the Dictionary. This way you can navigate forward and backward between the views without losing data. If you want to destroy a view, you must remove every reference to the model e.g., by removing it from the Dictionary or by replacing the instance with a fresh one. – BionicCode Apr 10 '22 at 00:07
  • @BionicCode I see. Great explanations btw. I'm just curious about applying MVVM to full screen (self-service)unattended payment application(like ordering food, pay bills) whether it should use MVVM or just remain using Page Navigation as previous. Though decision to made but I love how the MVVM works actually especially when you show effort to write full MVVM pattern. Been done MVVM before but mostly single page. – Luiey Apr 10 '22 at 00:43
  • @Luiey There is no reason against MVVM. It helps to separate data from UI and gives your application a nice structure that perfectly extends into the WPF framework. Any structure is better than no structure. If you write unit tests for your application, you can't get away from MVVM or any similar pattern. – BionicCode Apr 10 '22 at 08:52
1

You probably want to define MainWindowNavigation as a static class with a method that simply changes the Content of the current MainWindow:

static class MainWindowNavigation
{
    public static void ChangePage(Page page)
    {
        var mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();
        if (mainWindow != null)
            mainWindow.Content = page;
    }
}

You can then call the method from any class without having a reference to the MainWindow:

MainWindowNavigation.ChangePage(new Pages.MainWindowPage());
mm8
  • 163,881
  • 10
  • 57
  • 88