Navigating between views in WPF / MVVM using dependency injection

A previous post described how to navigate between views in a WPF / MVVM project using the Mediator pattern as a means of passing data around and/or identifying the screen you wish to navigate to.

I believe dependency injection is a cleaner and more SOLID-principled way of accomplishing the process of injecting dependencies into the objects of classes that depend on them. In using this, we lessen the need to create objects inside classes and make unit testing easier.

The following example shows how to accomplish this as a Visual Studio 2022 WPF project.

Step 1. Create a new WPF project

Step 2. Install NuGet packages

Install the packages necessary to enable dependency injection. In this example we use the ones from Microsoft Extensions, installing them via the Package Manager Console. You will need to install these two:

https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection/

https://www.nuget.org/packages/Microsoft.Extensions.Hosting

Copy the NuGet packages for each and enter into the console in this manner:

NuGet\Install-Package Microsoft.Extensions.DependencyInjection -Version 6.0.1
NuGet\Install-Package Microsoft.Extensions.Hosting -Version 6.0.1

Example output from NuGet installation as shown:

Step 3. Add classes for event handling, RelayCommand etc

RelayCommand.cs

using System;
using System.Windows.Input;

namespace MvvmNavigateViews
{
    public class RelayCommand<T> : ICommand
    {
        private readonly Predicate<T> _canExecute;
        private readonly Action<T> _execute;

        public RelayCommand(Action<T> execute)
           : this(execute, null)
        {
            _execute = execute;
        }

        public RelayCommand(Action<T> execute, Predicate<T> canExecute)
        {
            if (execute == null)
            {
                throw new ArgumentNullException("execute");
            }
            _execute = execute;
            _canExecute = canExecute;
        }

        public bool CanExecute(object parameter)
        {
            return _canExecute == null || _canExecute((T)parameter);
        }

        public void Execute(object parameter)
        {
            _execute((T)parameter);
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
    }

    public class RelayCommand : ICommand
    {
        private readonly Predicate<object> _canExecute;
        private readonly Action<object> _execute;

        public RelayCommand(Action<object> execute)
           : this(execute, null)
        {
            _execute = execute;
        }

        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            if (execute == null)
            {
                throw new ArgumentNullException("execute");
            }
            _execute = execute;
            _canExecute = canExecute;
        }

        public bool CanExecute(object parameter)
        {
            return _canExecute == null || _canExecute(parameter);
        }

        public void Execute(object parameter)
        {
            _execute(parameter);
        }

        // Ensures WPF commanding infrastructure asks all RelayCommand objects whether their
        // associated views should be enabled whenever a command is invoked 
        public event EventHandler CanExecuteChanged
        {
            add
            {
                CommandManager.RequerySuggested += value;
                CanExecuteChangedInternal += value;
            }
            remove
            {
                CommandManager.RequerySuggested -= value;
                CanExecuteChangedInternal -= value;
            }
        }

        private event EventHandler CanExecuteChangedInternal;

        public void RaiseCanExecuteChanged()
        {
            CanExecuteChangedInternal.Raise(this);
        }
    }
}

EventRaiser.cs

using System;

namespace MvvmNavigateViews
{
    public static class EventRaiser
    {
        public static void Raise(this EventHandler handler, object sender)
        {
            handler?.Invoke(sender, EventArgs.Empty);
        }

        public static void Raise<T>(this EventHandler<EventArgs<T>> handler, object sender, T value)
        {
            handler?.Invoke(sender, new EventArgs<T>(value));
        }

        public static void Raise<T>(this EventHandler<T> handler, object sender, T value) where T : EventArgs
        {
            handler?.Invoke(sender, value);
        }

        public static void Raise<T>(this EventHandler<EventArgs<T>> handler, object sender, EventArgs<T> value)
        {
            handler?.Invoke(sender, value);
        }
    }
}

EventArgs.cs

using System;

namespace MvvmNavigateViews
{
    public class EventArgs<T> : EventArgs
    {
        public EventArgs(T value)
        {
            Value = value;
        }
        public T Value { get; private set; }
    }
}

Step 4. Create a common ViewModel interface

IPageViewModel.cs

All user control based ViewModels inherit from an IPageViewModel interface so they can have some common properties:

using System;

namespace MvvmNavigateViews
{
    public interface IPageViewModel
    {
        event EventHandler<EventArgs<string>>? ViewChanged;
        string PageId { get; set; }
        string Title { get; set; }
    }
}

Step 5. Create an interface for storing and sharing common data between view models

IDataModel.cs

using System.Collections.Generic;

namespace MvvmNavigateViews
{
    public interface IDataModel
    {
        string Data { get; set; }
        string? Reverse();
    }
}

DataModel.cs


using System;
namespace MvvmNavigateViews;

public class DataModel : IDataModel
{
    public string Data { get; set; }
    public string? Reverse()
    {
        char[] charArray = Data.ToCharArray();
        Array.Reverse(charArray);
        return new string(charArray);
    }

    public DataModel()
    {
        Data = "";
    }

    public DataModel(string data)
    {
        Data = data;
    }
}

Step 6. Update the starting point of the application

App.xaml.cs is the WPF application’s starting point.

In this code we create the default builder, something that is common to dependency injection methods, so that it is wired up for dependency injection. We also modify the startup method to await the startup of our container and the exit method to await the stopping of the AppHost. Given that in this example the MainWindow is deemed to be a singleton, given that there is only one of them, plus we add a transient for the IPageViews data.

Don’t worry about the other missing classes yet.

App.xaml.cs

using System.Collections.Generic;
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace MvvmNavigateViews;


public partial class App : Application
{
    public App()
    {
        AppHost = Host.CreateDefaultBuilder().ConfigureServices((hostContext, services) =>
        {
            services.AddSingleton<MainWindow>();
            services.AddTransient<IDataModel, DataModel>();
        }).Build();
    }
    protected override async void OnStartup(StartupEventArgs e)
    {
        await AppHost!.StartAsync();
        var startupForm = AppHost.Services.GetRequiredService<MainWindow>();
        startupForm!.DataContext = new MainWindowViewModel(new DataModel { Data = "Placeholder" });
        startupForm!.Show();
        base.OnStartup(e);
    }

    protected override async void OnExit(ExitEventArgs e)
    {
        await AppHost!.StopAsync();
        base.OnExit(e);
    }

    
    public static IHost? AppHost { get; private set; }
}

Step 7. Create the ViewModel classes

The classes created are a base ViewModel class implementing INotifyPropertyChanged, the MainWindow ViewModel and ViewModels for the user controls.

BaseViewModel.cs

using System;
using System.ComponentModel;
using System.Diagnostics;

namespace MvvmNavigateViews
{
    public abstract class BaseViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;

        protected void OnPropertyChanged(string propertyName)
        {
            VerifyPropertyName(propertyName);
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        [Conditional("DEBUG")]
        private void VerifyPropertyName(string propertyName)
        {
            if (TypeDescriptor.GetProperties(this)[propertyName] == null)
                throw new ArgumentNullException(GetType().Name + " does not contain property: " + propertyName);
        }
    }
}

MainWindowViewModel.cs

using System.Collections.Generic;

namespace MvvmNavigateViews
{
    public class MainWindowViewModel : BaseViewModel
    {
        private IPageViewModel? _pageViewModel;
        private readonly Dictionary<string, IPageViewModel>? _pageViewModels = new();

        public IPageViewModel? CurrentPageViewModel
        {
            get
            {
                return _pageViewModel;
            }
            set
            {
                _pageViewModel = value;
                OnPropertyChanged(nameof(CurrentPageViewModel));
            }
        }


        public MainWindowViewModel(IDataModel pageViews)
        {
            _pageViewModels["1"] = new UserControl1ViewModel("1");
            _pageViewModels["1"].ViewChanged += (o, s) =>
            {
                CurrentPageViewModel = _pageViewModels[s.Value];
                pageViews.Data = "Data: " + s.Value.ToString();
            };

            _pageViewModels["2"] = new UserControl2ViewModel("2");
            _pageViewModels["2"].ViewChanged += (o, s) =>
            {
                CurrentPageViewModel = _pageViewModels[s.Value];
                pageViews.Data = "Data: " + s.Value.ToString();
            };

            _pageViewModels["3"] = new UserControl3ViewModel("3");
            _pageViewModels["3"].ViewChanged += (o, s) =>
            {
                CurrentPageViewModel = _pageViewModels[s.Value];
                pageViews.Data = "Data: " + s.Value.ToString();
            };

            CurrentPageViewModel = _pageViewModels["1"];
        }
    }
}

UserControl1ViewModel.cs

using System;
using System.Windows.Input;

namespace MvvmNavigateViews
{
    public class UserControl1ViewModel : IPageViewModel
    {
        private ICommand? _goTo2;
        private ICommand? _goTo3;

        public event EventHandler<EventArgs<string>>? ViewChanged;
        public string PageId { get; set; }
        public string Title { get; set; }

        public UserControl1ViewModel(string pageIndex = "1")
        {
            PageId = pageIndex;
            Title = "View 1";
        }
        public ICommand GoTo2
        {
            get
            {
                return _goTo2 ??= new RelayCommand(x =>
                {
                    ViewChanged?.Raise(this, "2");
    

            });
            }
        }

        public ICommand GoTo3
        {
            get
            {
                return _goTo3 ??= new RelayCommand(x =>
                {
                    ViewChanged?.Raise(this, "3");
                });
            }
        }
    }
}

UserControl2ViewModel.cs

using System;
using System.Windows.Input;

namespace MvvmNavigateViews
{
    public class UserControl2ViewModel : BaseViewModel, IPageViewModel
    {
        private ICommand? _goTo1;
        private ICommand? _goTo3;
        public event EventHandler<EventArgs<string>>? ViewChanged;
        public string PageId { get; set; }
        public string Title { get; set; } = "View 2";

        public UserControl2ViewModel(string pageIndex = "2")
        {
            PageId = pageIndex;
        }

        public ICommand GoTo1
        {
            get
            {
                return _goTo1 ??= new RelayCommand(x =>
                {
                    ViewChanged?.Raise(this, "1");
                });
            }
        }

        public ICommand GoTo3
        {
            get
            {
                return _goTo3 ??= new RelayCommand(x =>
                {
                    ViewChanged?.Raise(this, "3");
                });
            }
        }
    }
}

UserControl3ViewModel.cs

using System;
using System.Windows.Input;

namespace MvvmNavigateViews
{
    public class UserControl3ViewModel : BaseViewModel, IPageViewModel
    {
        private ICommand? _goTo1;
        private ICommand? _goTo2;

        public event EventHandler<EventArgs<string>>? ViewChanged;
        public string PageId { get; set; }
        public string Title { get; set; } = "View 3";

        public UserControl3ViewModel(string pageIndex = "3")
        {
            PageId = pageIndex;
        }

        public ICommand GoTo1
        {
            get
            {
                return _goTo1 ??= new RelayCommand(x =>
                {
                    ViewChanged?.Raise(this, "1");
                });
            }
        }

        public ICommand GoTo2
        {
            get
            {
                return _goTo2 ??= new RelayCommand(x =>
                {
                    ViewChanged?.Raise(this, "2");
                });
            }
        }
    }
}

Step 8: Create the Views

App.xaml

<Application x:Class="MvvmNavigateViews.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:MvvmNavigateViews">
    <Application.Resources>
        <DataTemplate DataType="{x:Type local:UserControl1ViewModel}">
            <local:UserControl1 />
        </DataTemplate>
        <DataTemplate DataType="{x:Type local:UserControl2ViewModel}">
            <local:UserControl2 />
        </DataTemplate>
        <DataTemplate DataType="{x:Type local:UserControl3ViewModel}">
            <local:UserControl3 />
        </DataTemplate>
    </Application.Resources>
</Application>

MainWindow.xaml

<Window x:Class="MvvmNavigateViews.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:MvvmNavigateViews" d:DataContext="{d:DesignInstance Type=local:MainWindowViewModel}"
        mc:Ignorable="d"
        Title="Page navigation" Height="450" Width="800">
    <Grid>
        <ContentControl Content="{Binding CurrentPageViewModel}" />
    </Grid>
</Window>

For adding the following User controls select New item > User Control (WPF)
UserControl1.xaml

<UserControl x:Class="MvvmNavigateViews.UserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:MvvmNavigateViews" d:DataContext="{d:DesignInstance Type=local:UserControl1ViewModel}"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Label 
                Name="View1"  
                VerticalAlignment="Top" HorizontalAlignment="Center"
                Content="View 1"  
                Height="40"  
                Canvas.Left="10" Canvas.Top="10"  
                FontSize="14" FontFamily="Georgia"  
                FontWeight="Bold"/>

        <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
            <Button                
                Content="Go to View 2"
                Command="{Binding GoTo2}"
                Width="90" Height="30" Margin="0,20" />
            <Button               
                Content="Go to View 3"
                Command="{Binding GoTo3}"
                Width="90" Height="30" />
        </StackPanel>
    </Grid>
</UserControl>

UserControl2.xaml

<UserControl x:Class="MvvmNavigateViews.UserControl2"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:MvvmNavigateViews" d:DataContext="{d:DesignInstance Type=local:UserControl2ViewModel}"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Label 
                Name="View2"  
                VerticalAlignment="Top" HorizontalAlignment="Center"
                Content="View 2"  
                Height="40"  
                Canvas.Left="10" Canvas.Top="10"  
                FontSize="14" FontFamily="Georgia"  
                FontWeight="Bold"/>

        <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
            <Button                
                Content="Go to View 1"
                Command="{Binding GoTo1}"
                Width="90" Height="30" Margin="0,20" />
            <Button               
                Content="Go to View 3"
                Command="{Binding GoTo3}"
                Width="90" Height="30" />
        </StackPanel>
    </Grid>
</UserControl>

UserControl3.xaml

<UserControl x:Class="MvvmNavigateViews.UserControl3"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:MvvmNavigateViews" d:DataContext="{d:DesignInstance Type=local:UserControl3ViewModel}"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Label 
                Name="View3"  
                VerticalAlignment="Top" HorizontalAlignment="Center"
                Content="View 3"  
                Height="40"  
                Canvas.Left="10" Canvas.Top="10"  
                FontSize="14" FontFamily="Georgia"  
                FontWeight="Bold"/>

        <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
            <Button                
                Content="Go to View 1"
                Command="{Binding GoTo1}"
                Width="90" Height="30" Margin="0,20" />
            <Button               
                Content="Go to View 2"
                Command="{Binding GoTo2}"
                Width="90" Height="30" />
        </StackPanel>
    </Grid>
</UserControl>

Now that you’ve bolted on all the bits and pieces needed to implement the dependency injection within a WPF / MVVM environment, it’s time to give it a spin.

On firing up the application see that the current page view model / view defaults to that of “View 1”.

Now click the button the button titled “Go to View 2” and observe how the application navigates us to “View 2” as intended.

And then click the button the button titled “Go to View 3” and observe how the application navigates us to “View 3”

And so on.