Click here to Skip to main content
15,884,176 members
Articles / Desktop Programming / WPF

A Wizard Application in WPF with MVVMC Navigation

Rate me:
Please Sign up or sign in to vote.
3.86/5 (6 votes)
21 Dec 2018MIT6 min read 25.3K   951   7   10
Shows how to create two Wizard-type applications using Model-View-ViewModel-Controller navigation framework

Introduction

I know what you're thinking - What the world needs in the end of 2018 is another WPF implementation of a Wizard app. Well, the presented solution in this article will show a new approach for building multi-screen applications with some distinct advantages.

The implementation explained uses the MVVMC pattern. This is somewhat similar to MVC and adds Controllers to your app. We'll see how to use the Wpf.MVVMC library to build two wizard applications. The first one is a simple 4-step wizard. The second one has some advanced capabilities that will be a lot of work with simple MVVM or other navigation frameworks. You will see that MVVMC is fast to develop, simple, and flexible to changes.

Full disclosure: I'm the creator of Wpf.MVVMC.

Basic Wizard

Basic Wizard Video

Advanced Wizard

Advanced Wizard Video

Basic Wizard Tutorial

Start by creating a regular WPF project and add the Wpf.MVVMC NuGet package to your project. There's no need to do any sort of initialization or bootstrapping for this.

We will go over all the files, but to give you a general idea of how this works, the solution explorer will look like this:

Image 3

As you can see, there's a 'View' and a 'ViewModel' for each step of the Wizard, except for the first one. There's also a 'WizardController' file, which we will get to soon. First, let's start with the MainWindow.xaml:

MainWindow.xaml

XML
<Window x:Class="WizardAppMvvmc.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:mvvmc="clr-namespace:MVVMC;assembly=MVVMC"
        Title="Wizard with MVVMC" Height="450" Width="800"
        Background="#FFDDDDDD">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="180"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="50"/>
        </Grid.RowDefinitions>
        <Border Background="LightSkyBlue"></Border>
		
        <mvvmc:Region Grid.Column="1" ControllerID="Wizard"></mvvmc:Region>
		
        <Border Grid.Row="1" Grid.ColumnSpan="2" Background="LightGray">
            <StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
                <Button Command="{mvvmc:GoBackCommand ControllerID=Wizard}">Back</Button>
                <Button Command="{mvvmc:NavigateCommand Action=Next, ControllerID=Wizard}">
                 Next</Button>
            </StackPanel>
        </Border>
    </Grid>
</Window>

Explanation:

  • mvvmc:Region is an area in the screen which is controlled by a Controller. It's a regular ContentControl under the hood, with Content changing according to your navigation. The ControllerID=Wizard property determines which Controller it relates to. It's convention-based, so you will have to have a class called WizardController which derives from MVVMC.Controller (or an exception will be thrown).
  • The first Button's Command is mvvmc:GoBackCommand. With Wpf.MVVMC, history is saved on each navigation, and you can automatically go back to the previous step (or forward).
  • The second button's Command is mvvmc:NavigateCommand with the property Action=Next. This means that on Button Click, the method Next will be invoked in WizardController. In that method, we can (but don't have to) execute a navigation.

WizardController.cs

C#
public class Model
{
    public string Position { get; set; }
    public int YearsOfExperience { get; set; }
    public string Notes { get; set; }
}

public class WizardController : Controller
{
    private Model _model;

    public override void Initial()
    {
        FirstStep();
    }

    private void FirstStep()
    {
        _model  = new Model();
        ExecuteNavigation();
    }

    private void SecondStep()
    {
        ExecuteNavigation();
    }

    private void ThirdStep()
    {
        ExecuteNavigation();
    }

    private void FourthStep()
    {
        ExecuteNavigation(null, new Dictionary<string, object>()
        {
            { "Position",_model.Position},
            { "YearsOfExperience",_model.YearsOfExperience.ToString()},
            { "Notes",_model.Notes},

        });
    }

    public void Next()
    {
        if (this.GetCurrentPageName() + "View" == nameof(FirstStepView))
        {
            SecondStep();
        }
        else if (this.GetCurrentViewModel() is SecondStepViewModel secondStepViewModel)
        {
            _model.Position = GetPosition(secondStepViewModel);
            ThirdStep();
        }
        else if (this.GetCurrentViewModel() is ThirdStepViewModel thirdStepViewModel)
        {
            _model.YearsOfExperience = thirdStepViewModel.YearsOfExperience;
            _model.Notes = thirdStepViewModel.Notes;
            FourthStep();
            
        }
        else // From fourth step
        {
            ClearHistory();
            FirstStep();
        }
    }

    private string GetPosition(SecondStepViewModel secondStepViewModel)
    {
        if (secondStepViewModel.IsQAEngineer)
            return "QA Engineer";
        else if (secondStepViewModel.IsSoftwareEngineer)
            return "Software Engineer";
        else
            return "Team Leader";
    }
}

As mentioned before, Wpf.MVVMC is convention based. So when we write in XAML <mvvmc:Region ControllerID="Wizard"/>, we have to have a class called WizardController deriving from MVVMC.Controller. Beyond that, the pairing between the Region and the Controller is automatic.

Explanation:

  • Model is a small class to save the data between the steps.
  • In each Controller, we have to override Initial() that determines the initial Content of the Region. You can do nothing in that method, in which case the Region will just remain empty. We are calling the FirstStep() method - see next.
  • Each step method in the Controller FirstStep, SecondStep, ThirdStep, and FourthStep call the protected method ExecuteNavigation. ExecuteNavigation() depends on the calling method name. When called from "FirstStep()", for example, it will navigate to "FirstStep" page. Which means it will create FirstStepView and FirstStepViewModel instances, and connect them for binding. You don't have to have a ViewModel.
  • In FourthStep(), we are passing parameters to ExecuteNavigation. This is the ViewBag, which you might know from MVC. It acts in a similar way - It allows easy binding from XAML as you will see later on.
  • The Next method is the one automatically called from XAML with Command="{mvvmc:NavigateCommand Action=Next, ControllerID=Wizard}". It checks which step we are on right now, and calls the next one. As you can see, this is in code and really flexible. You can choose to skip steps, add steps, go back some steps or do nothing.
  • Your Views, ViewModels, and Controller should be in the same namespace. This way, you can have steps with the same name in the same project.

This concludes the hardest part of the code. All the Views are a simple UserControl. All the ViewModels should derive from MVVMC.MVVMCViewModel. A pair of a View and a ViewModel is called a Page in MVVMC terminology. Let's see the code of one of the pages.

Page Example: SecondStep

The View is a regular UserControl. SecondStepView.xaml:

XML
<UserControl x:Class="WizardAppMvvmc.Wizard.SecondStepView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
    <StackPanel Margin="50">
      <TextBlock FontSize="25" >Second Step - Start recruitment</TextBlock>
      <TextBlock FontSize="18" >Who do you want to recruit to your team?</TextBlock>
      <RadioButton IsChecked="{Binding IsQAEngineer}">QA Engineer</RadioButton>
      <RadioButton IsChecked="{Binding IsSoftwareEngineer}">Software Engineer</RadioButton>
      <RadioButton IsChecked="{Binding IsTeamLeader}">Team Leader</RadioButton>
    </StackPanel>
</UserControl>

The ViewModel should derive from MVVMC.MVVMCViewModel:

C#
public class SecondStepViewModel : MVVMCViewModel
{
    private bool _isQAEngineer;
    public bool IsQAEngineer
    {
        get { return _isQAEngineer; }
        set
        {
            _isQAEngineer = value;
            OnPropertyChanged();
        }
    }

    private bool _isSoftwareEngineer;
    public bool IsSoftwareEngineer
    {
        get { return _isSoftwareEngineer; }
        set
        {
            _isSoftwareEngineer = value;
            OnPropertyChanged();
        }
    }

    private bool _isTeamLeader;
    public bool IsTeamLeader
    {
        get { return _isTeamLeader; }
        set
        {
            _isTeamLeader = value;
            OnPropertyChanged();
        }
    }
}

As you can see, the View and ViewModel are exactly the same as your regular Views and ViewModels. The only exception here is with FourthStep where we use the ViewBag. Here is the code:

Using the ViewBag - FourthStep

FourthStepView.xaml:

XML
<UserControl x:Class="WizardAppMvvmc.Wizard.FourthStepView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:mvvmc="clr-namespace:MVVMC;assembly=MVVMC">
  <StackPanel >
      <TextBlock FontSize="25" >Finished, Recruitment is on the way</TextBlock>
      <UniformGrid Columns="2">
          <TextBlock>Position:</TextBlock>
        <TextBlock Text="{mvvmc:ViewBagBinding Path=Position}"></TextBlock>
          <TextBlock>Years of Experience:</TextBlock>
        <TextBlock Text="{mvvmc:ViewBagBinding Path=YearsOfExperience}"></TextBlock>
          <TextBlock>Notes:</TextBlock>
        <TextBlock Text="{mvvmc:ViewBagBinding Path=Notes}"></TextBlock>
      </UniformGrid>
  </StackPanel>
</UserControl>

FourthStepViewModel.cs:

C#
public class FourthStepViewModel : MVVMCViewModel
{
}

You can use mvvmc:ViewBagBinding to automatically bind to values that you passed in the ViewBag in ExecuteNavigation. When using the ViewBag, you have to have a ViewModel, even if it's an empty one.

Summary of the Basic Wizard

I think you can see the similarities between MVVMC and MVC. The Navigation "request" goes to the Controller. The Controller has an internal logic that decides on the "Page" to navigate to. This achieves separation of concerns between the View/ViewModels and the navigation logic. The convention based approach is also inspired by MVC and in my opinion, saves a lot of boiler-plate you would otherwise have to write.

All the Navigation "requests" were in the View with mvvmc:NavigateCommand. This is just one way to do this. The ViewModel can initiate navigation as well by getting the Controller object and calling its methods (there's an example near the end of this article).

Advanced Wizard Tutorial

The "Advanced" wizard (which is not really that advanced) showcases some more Wpf.MVVMC capabilities. Here are some of the things it does simply that will otherwise require a lot of work:

  • You can have Nested navigation. Like a sub-wizard within the main wizard.
  • Navigation buttons can be both within the "Dynamic" content and outside of it.
  • The Wizard doesn't have to have linear steps "1,2,3,..". Rather, it can have any step logic you choose according to choices the user made.

The source code of the wizard is available to download. I'll show you some of the more interesting parts of it. Let's start with the nested navigation.

Nested Regions

In the video, when pressing on "Software Engineer", you are referred to a sub-wizard where you can choose the technology (front-end) and framework (Angular, React). Here is the code of SoftwareEngineerView:

XML
<UserControl x:Class="AdvancedWizard.Wizard.SoftwareEngineerView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mvvmc="clr-namespace:MVVMC;assembly=MVVMC">
    <Grid>
        <Grid.RowDefinitions>
          <RowDefinition Height="100"></RowDefinition>
          <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>
        <Border Background="WhiteSmoke">
            <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="18">
             Software Engineer Recruitment</TextBlock>
        </Border>
        <mvvmc:Region ControllerID="SoftwareEngineer" Grid.Row="1"></mvvmc:Region>
    </Grid>
</UserControl>

SoftwareEngineerView is a step in the "main" wizard. In it, you can have more Regions which are controlled by another Controller. In our case, the SoftwareEngineerController.

You can request any navigation from any Controller. In our case, the SoftwareEngineerController does navigation on itself and on the "main" Controller as well. Here is the FrontEndView.xaml:

XML
<UserControl x:Class="AdvancedWizard.Wizard.SoftwareEngineer.FrontEndView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mvvmc="clr-namespace:MVVMC;assembly=MVVMC">
  <StackPanel>
    <TextBlock Margin="5">What framework do you like best?</TextBlock>
    <Button Margin="5" Command="{mvvmc:NavigateCommand ControllerID=SoftwareEngineer, 
                                 Action=Angular1}">Angular 1</Button>
    <Button Margin="5" Command="{mvvmc:NavigateCommand ControllerID=Wizard, 
                                 Action=Finish}">Angular 2+</Button>
    <Button Margin="5" Command="{mvvmc:NavigateCommand ControllerID=Wizard, 
                                 Action=Finish}">React</Button>
    <Button Margin="5" Command="{mvvmc:NavigateCommand ControllerID=Wizard, 
                                 Action=Finish}">Vue.js</Button>
  </StackPanel>
</UserControl>

And this is Angular1View.xaml:

XML
<UserControl x:Class="AdvancedWizard.Wizard.SoftwareEngineer.Angular1View"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mvvmc="clr-namespace:MVVMC;assembly=MVVMC">
    <StackPanel>
      <TextBlock Margin="5" FontSize="18">Angular 1, Really?</TextBlock>
      <Button Margin="5" Command="{mvvmc:NavigateCommand ControllerID=Wizard, 
                                   Action=Finish}">Yes</Button>
      <Button Margin="5" Command="{mvvmc:GoBackCommand ControllerID=SoftwareEngineer}">No</Button>
    </StackPanel>
</UserControl>

Just one more example I want to show is navigation from ViewModel. So here is StartViewModel.cs:

C#
using MVVMC;

namespace AdvancedWizard.Wizard
{
    public class StartViewModel : MVVMCViewModel<WizardController>
    {
        private bool _isQA;
        public bool IsQA
        {
            get { return _isQA; }
            set
            {
                _isQA = value; 
                OnPropertyChanged();
                NextCommand.RaiseCanExecuteChanged();
            }
        }

        private bool _isSoftwareEngineer;
        public bool IsSoftwareEngineer
        {
            get { return _isSoftwareEngineer; }
            set
            {
                _isSoftwareEngineer= value; 
                OnPropertyChanged();
                NextCommand.RaiseCanExecuteChanged();
            }
        }

        private bool _isTeamLeader;
        public bool IsTeamLeader
        {
            get { return _isTeamLeader; }
            set
            {
                _isTeamLeader = value;
                OnPropertyChanged();
                NextCommand.RaiseCanExecuteChanged();
            }
        }

        public ICommand _nextCommand;
        public ICommand NextCommand
        {
            get
            {
                if (_nextCommand == null)
                {
                    _nextCommand = new DelegateCommand(
                        () =>
                        {
                            var controller = GetExactController();
                            if (IsQA)
                                controller.QA();
                            else if (IsSoftwareEngineer)
                                controller.SoftwareEngineer();
                            else
                                controller.TeamLeader();
                        },
                        // can execute
                        () => IsQA || IsSoftwareEngineer || IsTeamLeader);
                }

                return _nextCommand;
            }
        }
    }
}

Explanation

The ViewModel derives from MVVMCViewModel<WizardController>. This allows to use GetExactController() method which returns the Controller instance. From there, navigation is as simple as calling a method. For example, controller.SoftwareEngineer().

Summary

I hope I convinced you about the strength of MVVMC. Whenever you have an application with multiple screens, this can be a great choice. It's certainly an opinionated library, but it's usually for the best as it gives you structure and more time to work on the app's functionality rather than the navigation.

The source code and documentation is available on GitHub and the NuGet package is here.

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Team Leader OzCode
Israel Israel
I'm a Senior Software Developer and a Blogger.
I dabble mostly in C# .NET, WPF, Vue.js, and Asp.NET.
You can find me at www.michaelscodingspot.com
I work as a team leader in OzCode, an extension for Visual Studio that makes C# debugging easier.

Comments and Discussions

 
QuestionThe textbox TextChanged event could not trigger next button Pin
Kerwen Zhang23-Jul-23 19:46
Kerwen Zhang23-Jul-23 19:46 
Questionadd pages/views/steps "dynamically" Pin
Member 1577474122-Sep-22 23:42
Member 1577474122-Sep-22 23:42 
QuestionHow about a Wizard Engine approach ? Pin
Laurent de Laprade 202124-Mar-21 22:15
Laurent de Laprade 202124-Mar-21 22:15 
Questionre: compiler error Pin
Member 1062769915-Jan-20 23:56
Member 1062769915-Jan-20 23:56 
QuestionHow to Initialize Controller with some ViewModel Pin
Member 1108071824-May-19 20:36
Member 1108071824-May-19 20:36 
GeneralMy vote of 5 Pin
Hyland Computer Systems24-Dec-18 20:59
Hyland Computer Systems24-Dec-18 20:59 
GeneralRe: My vote of 5 Pin
Michael Shpilt27-Dec-18 5:55
Michael Shpilt27-Dec-18 5:55 
QuestionLink to own blog Pin
Nelek21-Dec-18 4:46
protectorNelek21-Dec-18 4:46 
QuestionWhy the three Vs? Pin
netizenk21-Dec-18 4:04
professionalnetizenk21-Dec-18 4:04 
AnswerRe: Why the three Vs? Pin
Michael Shpilt21-Dec-18 4:22
Michael Shpilt21-Dec-18 4:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.