michaelscodingspot / WPF_MVVMC

A WPF framework for navigation between pages with MVC-like pattern
MIT License
64 stars 18 forks source link

Wpf.MVVMC

Build status

Nuget: Install-Package Wpf.MVVMC

Description

This project is a navigation framework for WPF, which implements the MVVMC pattern. MVVMC adds Controllers to MVVM, which are responsible for navigation and switching between views (screens or parts of screen).

In MVVMC, the View and ViewModel will request a navigation action from the controller. The controller will create the new View and ViewModel instances. This way, we achieve a separation of concerns, and the View & ViewModel are responsible only to themselves, and don't create or know about other Views.

To read more about MVVMC and the motivation for this framework, see the original blog posts: Part 1, Part 2.

Documentation

Quickstart

Let's build a small Wizard application with 3 steps in it. First, create a WPF application and add the Wpf.MVVMC Nuget package.

Step 1: Create a Region

Add a Region to the MainWindow, like this:

<Window 
    xmlns:mvvmc="clr-namespace:MVVMC;assembly=MVVMC"
    ...>
    <mvvmc:Region ControllerID="Wizard"></mvvmc:Region>
</Window>

A Region is an area on the screen with dynamic content, controlled by a Controller. The Region's controller is deterimned by the ControllerID property which is set to "Wizard". Wpf.MVVMC is a convention based framework, so naming matters. In this case, we'll have to create a controller class called "WizardController". The Controller will be responsible for navigating between the wizard steps.

Step 2: Create a Controller

public class WizardController : Controller
{
    public override void Initial()
    {
        FirstStep();
    }

    public void Next()
    {
        var currentVM = GetCurrentViewModel();
        if (currentVM is FirstStepViewModel)
        {
            SecondStep();
        }
        else if (currentVM is SecondStepViewModel)
        {
            ThirdStep();
        }
        else
        {
            MessageBox.Show("Finished!");
            App.Current.MainWindow.Close();
        }
    }

    private void FirstStep()
    {
        ExecuteNavigation();
    }

    private void SecondStep()
    {
        ExecuteNavigation();
    }

    private void ThirdStep()
    {
        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.

Step 3: Add Views

The View can be any WPF control, like a simple UserControl. It should be in the same namespace as the Controller and the ViewModel. Let's add 3 User Controls to the project called FirstStepView, SecondStepView and ThirdStepView. Each will have a caption and a Next button. For example, FirstStepView.xaml will be:

<UserControl x:Class="MvvmcQuickstart1.FirstStepView"
         xmlns:mvvmc="clr-namespace:MVVMC;assembly=MVVMC"
             ...>
    <StackPanel>
        <TextBlock>First step</TextBlock>
    <Button Command="{mvvmc:NavigateCommand Action=Next, ControllerID=Wizard}">Next</Button>
    </StackPanel>
</UserControl>

Using mvvmc:NavigateCommand allows to navigate directly from the View. You can choose to leave it as is and the program is done! Creating a View-Model is optional. To initiate the navigation from the View-Model, you'll need to change the View to this:

<UserControl x:Class="MvvmcQuickstart1.FirstStepView"
             ...>
    <StackPanel>
        <TextBlock>First step</TextBlock>
    <Button Command="{Binding NextCommand}">Next</Button>
    </StackPanel>
</UserControl>

Step 4: Add ViewModels

The ViewModels need to be called same as the Views with the ViewModel postfix, and in the same namespace. So we'll add FirstStepViewModel, SecondStepViewModel and ThirdStepViewModel classes. Each ViewModel needs to inherit from MVVMCViewModel base class. For example, FirstStepViewModel class will be:

using System.Windows.Input;
using MVVMC;
...

public class FirstStepViewModel : MVVMCViewModel
{
    public ICommand _nextCommand { get; set; }

    public ICommand NextCommand
    {
        get
        {
            if (_nextCommand == null)
            {
                _nextCommand = new DelegateCommand(() =>
                {
                    GetController().Navigate("Next", parameter: null);
                });
            }
            return _nextCommand;
        }
    }
}

(DelegateCommand used here is part of the MVVMC package.)

That's it. We have a finished 3-step wizard.

The result:

With just a little bit of styling, the resulting program looks like this:

Quickstart result

Regions:

A Region is a Control which simply contains a content presenter with dynamic content. On navigation, the content changes to the target View. Each region area is controlled by a single controller, which is specified by the ControllerID property.

xmlns:mvvmc="clr-namespace:MVVMC;assembly=MVVMC"
...
<mvvmc:Region ControllerID="XXX" />

The Controller, in turn, controls a single Region, so there's 1 to 1 relation between Region and Controller.

Connecting Region and Controller happens with reflection. The framework looks for a class named [ControllerId]Controller that inherits from MVVMC.Controller.

In applications where you want the navigation to occur on the entire screen, the Window contents should be only the Region.

<Window 
    xmlns:mvvmc="clr-namespace:MVVMC;assembly=MVVMC"
    ...>
    <mvvmc:Region ControllerID="Wizard"></mvvmc:Region>
</Window>

The number of Regions is not limited. So in MainWindow.xaml, you might have a Region for the top bar, a Region for the Main-Content and a Region for the footer.

Regions can be nested. You can have a Region which navigates to some Page, which in turn can include additional Regions.

Sometimes, you'll want several different Controllers to control the same screen area. For example, the application has several full-screen flows which include multiple screens each. In that case, you'll have one region for the "MainController" with a Page for each Flow. Each of those pages will include another Region responsible for their respected flows.

A Page means a pair of a View and a ViewModel, where the ViewModel is optional. So the page "Employees" means there's a WPF UI element "EmployeesView" and optionally a class "EmployeesViewModel". A Page doesn't have to be full-screen sized. The size will be according to the Region's space on screen.

Naming convention:

Wpf.MVVMC is convention based. The naming rules are:

  1. Each Controller is in it's own namespace.
  2. Views and ViewModels are controlled by a single Controller and should be in the same namespace as the controller.
  3. A pair of a View and a ViewModel are called a Page, and should be named XXXView and XXXViewModel, with 'XXX' being the page's name.

It's recommended to create a separate folder for each Controller. This folder will contain the Controller class with the Views and ViewModels relevant to that Controller. This way, they will have a common and unique namespace.

Controllers:

A controller contains the actual navigation logic. Each controller is connected to a single Region and the navigation executes by replacing the Region's content.

Each Controller should dervive from the base class MVVMC.Controller.

Each method in the controller can be considered an Action. When an Action method calls ExecuteNavigation(), the controller will create a View and ViewModel instance of the name of the same Action. For example:

public class MyController : Controller
{
    public void Employees()
    {
        ExecuteNavigation()
    }

In this Controller we have the action "Employees". When called, an intance of "EmployeesView" and "EmployeesViewModel" will be created and the relevant Region's content will be replaced. If "EmployeesView" is not found in the same namespace as the Controller, exception will be thrown.

Here's another example:

public class MטController : Controller
{
    public void Initial()
    {
        ExecuteNavigation();//Will create InitialView and InitialViewModel
    }

    public void HireEmployee(object employee)
    {
        if (CanHire(employee))
    {
        Navigate("HireStart", employee);
    }
        else
    {
        HireError();
    }
    }

    public void HireStart()
    {
        ExecuteNavigation()
    }

    public void HireError()
    {
        ExecuteNavigation()
    }

Views:

A Views can be any WPF Control, like a User Control or a Custom Control. Each view can be placed in a single region and navigated by one Controller. The View must be named [Page]View and in the same namespace as the Controller it is connected to.

When navigating to a Page:

  1. The View's instance is created
  2. If a ViewModel class exists, the ViewModel instance is created.
  3. The View's DataContext is set to the ViewModel instance, allowing Binding between them.

You can use NavigateCommand in a View's Xaml to initiate navigation, like this:

xmlns:mvvmc="clr-namespace:MVVMC;assembly=MVVMC"
...
<Button Command="{mvvmc:NavigateCommand ControllerID='MainOperation', Action='AllEmployees'}">View Employees</Button>

Command Parameter can be included and passed to the navigation request.

The View can use a special binding called ViewBagBinding to bind directly to the ViewModel's ViewBag:

xmlns:mvvmc="clr-namespace:MVVMC;assembly=MVVMC"
...
<TextBlock Text="{mvvmc:ViewBagBinding Path=EmployeeName}"/>

See more info on the ViewBag in the ViewModel section.

ViewModels:

A ViewModel is a regular class that must derive from MVVMC.MVVMCViewModel or MVVMC.MVVMCViewModel<TController>. Creating a ViewModel for a Page is optional. The ViewModel's name must be [Page]ViewModel and in the same namespace as the Controller it is connected to.

When deriving from MVVMC.MVVMCViewModel: You can use GetController() to get an IController instance. With IController you can:

When deriving from MVVMC.MVVMCViewModel<TController> This is the recommended way to create ViewModels. You'll have to specify the controller type as TController. You will be able to use TController GetExactController() to get an instance of the Controller the ViewModel is connected to.

For example, the following code will navigate to the "Info" Action in AllEmployeesController and pass the "SelectedEmployee" as parameter.

public class SelectEmployeeViewModel : MVVMCViewModel<AllEmployeesController>
{

    public ICommand _selectEmployeeCommand;
    public ICommand SelectEmployeeCommand
    {
        get
        {
            if (_selectEmployeeCommand == null)
                _selectEmployeeCommand = new DelegateCommand(() =>
                {
                    GetExactController().Info(SelectedEmployee);
                },
                ()=>
                {
                    return SelectedEmployee != null;
                });
            return _selectEmployeeCommand;
        }
    }

If the ViewModel derived from MVVMC.MVVMCViewModel, we'd have to use GetController().Navigate("Info", SelectedEmployee).

The MVVMCViewModel has the NavigationParameter and ViewBag properties, which are populated by the Controller during naviagtion.

After navigation, the Controller will call the virtual Initialize() method, which you can override. That is the place to make use of the NavigationParamater to populate the ViewModel properties for example.

Navigation service:

The Navigation-Service is exposed by the INavigationService interface and can be used everywhere with the static NavigationServiceProvider.GetNavigationServiceInstance(). It's a singleton for now, and might be changed to some kind of injection pattern in the future.

INavigationService allows:

Which basically means we can navigate to everything from anywhere.

Go Back and Forward:

Historical navigation is available for each controller. You can tell a controller to "Go Back" to a previous page or "Go Forward" again after going back.

Each controller exposes the GoBack and GoForward methods. These methods will execute navigation immediately, without invoking the Controller's Action method. For example, in a wizard application we might have a FirstStep, SecondStep, and ThirdStep pages. If we are in the ThirdStep, invoking GoBack will create and navigate to SecondStepView and SecondStepViewModel without actually invoking the SecondStep() method in the Controller.

Each controller has a HistoryMode property, which can be set to: DiscardParameterInstance (default) , SaveViewModel (v2.3.0+), or SaveParameterInstance. According to this mode, each navigation saves the view model or the navigation parameter and view bag. This allows to restore the state of the page when going back or forward in history.

You can set this mode in your controller's constructor like this:

public class MyController : Controller
{
    public MyController()
    {
        HistoryMode = HistoryMode.DiscardParameterInstance;
    }
}

Or in XAML when defining Region like this: (only from version 2.3.0+)

<mvvmc:Region ControllerID="MyController" HistoryMode="SaveViewModel" />

When in DiscardParameterInstance mode, the GoBack method will expect a parameter and a ViewBag as parameters, since these were discarded after the navigation. Here is the method's signature:

public virtual void GoBack(object parameter, Dictionary<string, object> viewBag)

Note: Both the GoBack and GoForward methods can be overridden in your Controller.

When in history modes SaveParameterInstance or SaveViewModel, you can use the GoBack and GoForward methods that don't accept any parameters. SaveParameterInstance will navigate with the same parameter and ViewBag. Whereas SaveViewModel will reuse the same ViewModel instance as in the historical navigation.

Note that in both of those modes, parameter or view model instances are saved in memory. This can potentially lead to bigger memory footprints or memory leaks (although should be fine in most cases).

Additional methods and properties available in a Controller object are:

NavigationService exposes the events: CanGoBackChangedEvent and CanGoForwardChangedEvent

Going Back and Forward from XAML

In XAML, you can use mvvmc:GoBackCommand and mvvmc:GoForwardCommand like this:

xmlns:mvvmc="clr-namespace:MVVMC;assembly=MVVMC"
...
<Button Command="{mvvmc:GoBackCommand ControllerID='MainOperation',
    HistoricalNavigationMode=UseSavedViewModel}">Back</Button>

<Button Command="{mvvmc:GoForwardCommand ControllerID='MainOperation',
    HistoricalNavigationMode=UseSavedParameter}">Forward</Button>

<Button Command="{mvvmc:GoBackCommand ControllerID='MainOperation',
    HistoricalNavigationMode=UseCommandParameter}" 
    CommandParameter="{Binding MyNavigationParameter}">Back</Button>

HistoricalNavigationMode can be UseSavedViewModel, UseCommandParameter, or UseSavedParameter.

When using UseSavedParameter, the controller's HistoryMode should be set to SaveParameterInstance or an exception will be thrown. When using UseSavedViewModel, the controller's HistoryMode should be set to SaveViewModel.

When using UseCommandParameter (the default), you can set or bind to CommandParameter, which will act as the navigation parameter. You can also set the command's ViewBag property. The button will be enabled or disabled automatically according to CanGoBack and CanGoForward.