Home > .NET Core, C#, MVVM, WPF > An MVVM-aware NavigationService for WPF running on .NET Core

An MVVM-aware NavigationService for WPF running on .NET Core

13/01/2020

In the last article we talked about how to use the MVVM pattern in WPF applications running on .NET Core. Today, we’ll add support for navigation through Window from View Models. For this purpose, we start from the code of the last article. Our goal is to extend it adding a method to open new Window from View Models and, at the same time, pass parameters to View Models.

So, let’s start with the NavigationService:

public interface IActivable
{
    Task ActivateAsync(object parameter);
}

public class NavigationService
{
    private Dictionary<string, Type> windows { get; }
        = new Dictionary<string, Type>();

    private readonly IServiceProvider serviceProvider;

    public void Configure(string key, Type windowType) 
        => windows.Add(key, windowType);

    public NavigationService(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
    }

    public async Task ShowAsync(string windowKey,
        object parameter = null)
    {
        var window = await GetAndActivateWindowAsync(windowKey, parameter);
        window.Show();
    }

    public async Task<bool?> ShowDialogAsync(string windowKey,
        object parameter = null)
    {
        var window = await GetAndActivateWindowAsync(windowKey, parameter);
        return window.ShowDialog();
    }

    private async Task<Window> GetAndActivateWindowAsync(string windowKey,
        object parameter = null)
    {
        var window = serviceProvider.GetRequiredService(windows[windowKey])
            as Window;

        if (window.DataContext is IActivable activable)
        {
            await activable.ActivateAsync(parameter);
        }

        return window;
    }
}

First of all, at lines 1-4 we define an IActivable interface to mark View Models that support “activation”, i.e. the ability to receive parameters when the corresponding Window is loaded. Then, the NavigationService class encapsulates all the logic we need:

  • it defines a Configure method (lines 13-14) to map a string identifier to a window type. We follow this approach because we want to use the NavigationService from View Models that don’t have to know the Views;
  • it provides methods to open a window (modeless or modal, lines 24-25 and 31-32 respectively), getting it from the ServiceProvider using the windowKey parameter (lines 37-38);
  • if the Window.DataContext is a View Model that implements the IActivable interface, it calls its ActivateAsync method (lines 41-44), passing to it the specified parameter, if any.

Now we need to properly register the NavigationService and all the Windows and corresponding View Models of our application in the ConfigureServices method of App.xaml.cs file we defined in the previous post:

public static class Windows
{
    public const string MainWindow = nameof(MainWindow);
    public const string DetailWindow = nameof(DetailWindow);
}

private void ConfigureServices(IConfiguration configuration, 
    IServiceCollection services)
{
    // ...

    services.AddScoped<NavigationService>(serviceProvider =>
    {
        var navigationService = new NavigationService(serviceProvider);
        navigationService.Configure(Windows.MainWindow, typeof(MainWindow));
        navigationService.Configure(Windows.DetailWindow, typeof(DetailWindow));

        return navigationService;
    });

    services.AddSingleton<MainViewModel>();
    services.AddSingleton<DetailViewModel>();

    services.AddTransient<MainWindow>();
    services.AddTransient<DetailWindow>();
}

At lines 1-5 we create a Windows class that holds strings corresponding to the Windows of the application, in this sample a MainWindow and a DetailWindow (we’ll talk about them later in this article). Then, we create the NavigationService, configure all the Windows and add it to the services collection (lines 12-19). Finally, at lines 21-25 we add Views Models and Windows themselves.

In the OnStartup method, we can use the new service to show the MainWindow:

protected override async void OnStartup(StartupEventArgs e)
{
    await host.StartAsync();

    var navigationService = 
        ServiceProvider.GetRequiredService<NavigationService>();

    await navigationService.ShowAsync(Navigation.Windows.MainWindow);

    base.OnStartup(e);
}

Now let’s see how to use it. Our MainWindow already contains a TextBox and a Button from the last sample. These objects are already bound to the MainViewModel, so we need to modify it to obtain something like this:

public class MainViewModel : ViewModelBase
{
    private string input;
    public string Input
    {
        get => input;
        set => Set(ref input, value);
    }

    private readonly NavigationService navigationService;
    private readonly ISampleService sampleService;
    private readonly AppSettings settings;

    public RelayCommand ExecuteCommand { get; }

    public MainViewModel(NavigationService navigationService, 
        ISampleService sampleService, IOptions<AppSettings> options)
    {
        this.navigationService = navigationService;
        this.sampleService = sampleService;
        settings = options.Value;

        ExecuteCommand = new RelayCommand(
            async () => await ExecuteAsync());
    }

    private Task ExecuteAsync()
    {
        Debug.WriteLine($"Current value: {input}");
        return navigationService.ShowDialogAsync(Windows.DetailWindow, input);
    }
}

First of all, in the constructor we pass a reference to the NavigationService (line 16). As usual, the .NET Core ServiceProvider is responsible to instantiate it and pass it to the class. Then, in the ExecuteAsync method we show the DetailWindow as dialog using the service, passing the specified input as parameter (line 30).

The DetailWindow can be something like the following:

<Window
    x:Class="WpfNetCoreMvvm.Views.DetailWindow"
    ...
    DataContext
        ="{Binding Source={StaticResource Locator}, Path=DetailViewModel}"
    >

    <Grid>
        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
            <TextBlock Text="Passed Parameter:" />
            <TextBlock Text="{Binding Parameter}" />
        </StackPanel>
    </Grid>
</Window>

Note in particular the DataContext property at line 4-5 and the binding at line 11. Now we need to define the DetailViewModel itself:

public class DetailViewModel : ViewModelBase, IActivable
{
    private string parameter;
    public string Parameter
    {
        get => parameter;
        set => Set(ref parameter, value);
    }

    public Task ActivateAsync(object parameter)
    {
        Parameter = parameter?.ToString();
        return Task.CompletedTask;
    }
}

When we navigate to DetailWindow from MainViewModel, we pass a parameter, so the DetailViewModel must be able to handle it. For this reason, it implements the IActivable interface (lines 10-13): in this way, the input value from MainViewModel is automatically passed to the ActivateAsync method.

Now we can finally try running the application:

Navigation in the WPF Application with MVVM running on top of .NET Core

Navigation in the WPF Application with MVVM running on top of .NET Core

You can download the sample application using the link below:

An MVVM-aware NavigationService for WPF running on .NET Core

Categories: .NET Core, C#, MVVM, WPF
  1. Chris
    26/02/2020 at 10:12

    Thanks for your very usefull tutorials for WPF using .NET Core! They’re very helpful to me.

    Just a small question in relation to navigation. You only showed (the more complex, I know) switch between windows itself but not using one single window switching between different ContentControls. What would be the best solution for that?

  2. Meteor
    05/03/2020 at 08:48

    Ya, I am also interested and which to know is there any simple boilercode that is for Navigating frame.

    • 05/03/2020 at 09:34

      Can you explain better what do you need?

  3. meteor
    06/03/2020 at 08:24

    Hmm, i mean for totally newbie for WPF. How to apply Window to Frame?

    this.frame1.NavigationService.Navigate(new Uri(“DetailWindow.xaml”, UriKind.Relative),
    ViewModel);

    • 06/03/2020 at 09:51

      Where did you use this kind of navigation?

  1. 14/01/2020 at 11:15
  2. 14/01/2020 at 13:30
  3. 15/01/2020 at 07:00
Comments are closed.