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
Advanced Wizard
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:
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
<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
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
{
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
View
s, ViewModel
s, 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 ViewModel
s 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:
<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
:
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 View
s and ViewModel
s. The only exception here is with FourthStep
where we use the ViewBag
. Here is the code:
Using the ViewBag - FourthStep
FourthStepView.xaml:
<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:
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
/ViewModel
s 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
:
<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:
<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:
<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:
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();
},
() => 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.