PhlegmaticOne.WPF.Navigation 2.0.3

There is a newer version of this package available.
See the version list below for details.
dotnet add package PhlegmaticOne.WPF.Navigation --version 2.0.3                
NuGet\Install-Package PhlegmaticOne.WPF.Navigation -Version 2.0.3                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PhlegmaticOne.WPF.Navigation" Version="2.0.3" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add PhlegmaticOne.WPF.Navigation --version 2.0.3                
#r "nuget: PhlegmaticOne.WPF.Navigation, 2.0.3"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install PhlegmaticOne.WPF.Navigation as a Cake Addin
#addin nuget:?package=PhlegmaticOne.WPF.Navigation&version=2.0.3

// Install PhlegmaticOne.WPF.Navigation as a Cake Tool
#tool nuget:?package=PhlegmaticOne.WPF.Navigation&version=2.0.3                

PhlegmaticOne.WPF.Navigation

Logo - Copy

Nuget package

PhlegmaticOne.WPF.Navigation

Installation

PM> NuGet\Install-Package PhlegmaticOne.WPF.Navigation -Version 2.0.3

Usage

Setup application structure

In this guide will be shown how to setup application where Models, ViewModels and Views are placed in different projects just to show all moments in navigation setup

Application initial structure

image

What is what

  • App - WPF application
  • Contracts - All services and other stuff needed for application to work
  • Data - Project with data access (for sample there will be no data access)
  • Models - Project with Models
  • ViewModels - Project with ViewModels
  • Views - Project with UserControls

All of this, of course, can be placed in a single WPF application project

Anyway, further here will be shown only ViewModels setup

Tip

Install in your Models project (if it exists):

  • PropertyChanged.Fody - it allows not to call OnPropertyChanged method explicitly
  • PhlegmaticOne.WPF.Core - provides base type for all Models - EntityViewModelBase, which implements INotifyPropertyChanged interface

ViewModels

Install this packages

  • PropertyChanged.Fody - it allows not to call OnPropertyChanged method explicitly
  • PhlegmaticOne.WPF.Navigation (this package) - provides WPF navigation

Typical ViewModel

public class AllSchedulesViewModel : ApplicationBaseViewModel
{
    private readonly IScheduleDataService _scheduleDataService;
    
    public AllSchedulesViewModel(IScheduleDataService scheduleDataService)
    {
        _scheduleDataService = scheduleDataService;
       ...
    }
    ...
}

Since all ViewModels will be registered in IServiceCollection it is allowed to inject any registered services in any ViewModel

EntityContainingViewModel

public class ScheduleViewModel : ApplicationBaseViewModel, IEntityContainingViewModel<ScheduleModel>
{
    public ScheduleModel Entity { get; set; }
}

Here you also can inject any services you need

Here is a simple NavigationViewModel

public class NavigationViewModel : ApplicationBaseViewModel, IDisposable
{
    private readonly INavigationService _navigationService;

    public NavigationViewModel(INavigationService navigationService)
    {
        _navigationService = navigationService;
        _navigationService.ViewModelChanged += NavigationService_ViewModelChanged;

        NavigateCommand = RelayCommandFactory.CreateRequiredParameterCommand<Type>(Navigate, _ => true);

        Navigate(typeof(HomeViewModel));
    }
    public ApplicationBaseViewModel CurrentViewModel { get; private set; } = null!;
    public IRelayCommand NavigateCommand { get; set; }

    public void Dispose()
    {
        _navigationService.ViewModelChanged -= NavigationService_ViewModelChanged;
    }

    private void Navigate(Type viewModelType)
    {
        _navigationService.NavigateTo(viewModelType);
    }

    private void NavigationService_ViewModelChanged(object? sender, ApplicationBaseViewModel e)
    {
        CurrentViewModel = _navigationService.CurrentViewModel;
    }
}

ChainedNavigationViewModel

If you are registered IChainedNavigationService instead of INavigationService you can write something like that:

public class NavigationViewModel : ApplicationBaseViewModel, IDisposable
{
    private readonly IChainNavigationService _chainNavigationService;
    public NavigationViewModel(IChainNavigationService chainNavigationService)
    {
        _chainNavigationService = chainNavigationService;
        _chainNavigationService.DirectionCanMoveChanged += ChainNavigationServiceOnDirectionCanMoveChanged;
        _chainNavigationService.ViewModelChanged += ChainNavigationServiceOnViewModelChanged;

        MoveCommand = RelayCommandFactory
            .CreateRequiredParameterCommand<NavigationMoveDirection>(Move, _ => true);

        NavigateCommand = RelayCommandFactory
            .CreateRequiredParameterCommand<Type>(Navigate, _ => true);

        ResetCommand = RelayCommandFactory.CreateCommand(Reset, _ => true);

        NavigateDefault();
    }
    public bool CanMoveBack { get; private set; }
    public bool CanMoveForward { get; private set; }
    public ApplicationBaseViewModel CurrentViewModel { get; private set; } = null!;
    public IRelayCommand NavigateCommand { get; }
    public IRelayCommand MoveCommand { get; }
    public IRelayCommand ResetCommand { get; }

    private void Move(NavigationMoveDirection navigationMoveDirection)
    {
        _chainNavigationService.Move(navigationMoveDirection);
    }

    private void Navigate(Type parameter)
    {
        _chainNavigationService.NavigateTo(parameter);
    }
    private void Reset(object? _)
    {
        _chainNavigationService.Reset();
        NavigateDefault();
    }
    private void NavigateDefault()
    {
        Navigate(typeof(HomeViewModel));
    }
    private void ChainNavigationServiceOnViewModelChanged(object? sender, ApplicationBaseViewModel e)
    {
        CurrentViewModel = e;
    }

    private void ChainNavigationServiceOnDirectionCanMoveChanged(object? sender, NavigationMoveDirectionChangedArgs e)
    {
        switch (e.NavigationMoveDirection)
        {
            case NavigationMoveDirection.Forward:
            {
                CanMoveForward = e.CanMove;
                break;
            }
            case NavigationMoveDirection.Back:
            {
                CanMoveBack = e.CanMove;
                break;
            }
        }
    }

    public void Dispose()
    {
        _chainNavigationService.DirectionCanMoveChanged -= ChainNavigationServiceOnDirectionCanMoveChanged;
        _chainNavigationService.ViewModelChanged -= ChainNavigationServiceOnViewModelChanged;
    }
}

EntityContainingViewModelsNavigation

In order to use this navigation you need to implement NavigationFactory<TFrom, TTo>, they are used by EntityContainingViewModelsNavigationService during navigation process. Let's see example.

This example is not very useful, but is show the concept.

Suppose we have ViewModel with list of ScheduleModels and we want to navigate to specified one:

public class AllSchedulesViewModel : ApplicationBaseViewModel
{
    private readonly IEntityContainingViewModelsNavigationService _entityContainingViewModelsNavigationService;
    ...
    public AllSchedulesViewModel(...,  
      IEntityContainingViewModelsNavigationService entityContainingViewModelsNavigationService)
    {
      _entityContainingViewModelsNavigationService = entityContainingViewModelsNavigationService;
      Schedules = new();
      ...
      NavigateToScheduleCommand = RelayCommandFactory
        .CreateRequiredParameterAsyncCommand<ScheduleModel>(NavigateToSchedule, _ => true);
    }
    public ObservableCollection<ScheduleModel> Schedules { get; }
    public IRelayCommand NavigateToScheduleCommand { get; }
    private async Task NavigateToSchedule(ScheduleModel scheduleModel)
    {
      await _entityContainingViewModelsNavigationService
        .From<ScheduleModel, ScheduleModel>()
        .NavigateAsync<ScheduleViewModel>(scheduleModel);
    }
    ...
}

Navigation process starts here:

private async Task NavigateToSchedule(ScheduleModel scheduleModel)
{
  await _entityContainingViewModelsNavigationService
    .From<ScheduleModel, ScheduleModel>()
    .NavigateAsync<ScheduleViewModel>(scheduleModel);
}

It means that we want to navigate from ScheduleModel (first generic type in method From) to ApplciationViewModel that implements interface IEntityContainingViewModel<T> (here T is ScheduleModel) (generic type in NavigateAsync method) that has single EntityViewModel of type ScheduleModel (second generic type in method From). During the navigation NavigationFactoryBase<ScheduleModel, ScheduleModel> will be found and used to create ScheduleModel from ScheduleModel object; it means that we need to implement it.

public class ScheduleModelToScheduleViewModelNavigationFactory : NavigationFactoryBase<ScheduleModel, ScheduleModel>
{
    private readonly IScheduleDataService _scheduleDataService;

    public ScheduleModelToScheduleViewModelNavigationFactory(IScheduleDataService scheduleDataService)
    {
        _scheduleDataService = scheduleDataService;
    }
    public override Task<ScheduleModel> CreateViewModelAsync(ScheduleModel entityViewModel)
    {
        var result = _scheduleDataService.GetSchedule(entityViewModel.Id);
        if (result.IsOk)
        {
            return Task.FromResult(result.Result.First());
        }
        else
        {
            return Task.FromResult(entityViewModel);
        }
    }
}

Since all NavigationFactories will be registered in IServiceCollection it is allowed to inject any registered services in any NavigationFactory.

In practical case, instead of two equal types, there will be types, for example: SchedulePreviewModel and ScheduleFullModel. And navigation will look like:

private async Task NavigateToSchedule(SchedulePreviewModel schedulePreviewModel)
{
  await _entityContainingViewModelsNavigationService
    .From<SchedulePreviewModel, ScheduleFullModel>()
    .NavigateAsync<ScheduleViewModel>(scheduleModel);
}

Registering NavigationService

Binding ViewModels to Views

Now supported 3 policies to bind ViewModels to Views:

  • Hand binding
  • Attributes binding
  • Auto binding

Let's see how they works.

1. Hand Binding

Bind ViewModels to Views in WPF App project explicitly

var bindingPolicy = new HandViewModelsToViewsBindingInfoProvider()
                .Bind<HomeViewModel, HomeView>()
                .Bind<AllSchedulesViewModel, AllSchedulesView>()
                .Bind<CreatingScheduleViewModel, CreatingScheduleView>()
                .Bind<ScheduleViewModel, ScheduleView>();

2. Attributes bindings

Use HasView attribute to mark ViewModel as ViewModel that has View. In that case, Views for ViewModels will be found by ViewModel name without 2 last words, for example: ViewModel name - AllSchedulesViewModel, name in finding process - AllSchedules; your View in that case must have name 'AllSchedules...', otherwise View won't be found.

[HasView]
public class AllSchedulesViewModel : ApplicationBaseViewModel
{...}

To specify View name more explicitly use HasView attribute with View name. In that case View will be found directly by specified name

[HasView("AllSchedulesView")]
public class AllSchedulesViewModel : ApplicationBaseViewModel
{...}

3. Auto bindings

You don't need to use attributes or hand bindings. Instead of this you only need to specify last word in your View type names and last word in Views namespace. For example, let's see how it works in current guide application: all views (HomeView, SchedulesView, ...) ends with 'View' that means last word in View names is 'View'; all views placed in project with namespace - PhlegmaticOne.WPF.Navigation.Sample.Views - that means last word in namespace is 'Views'. Relation between ViewModels and Views also will be found depending on ViewModel start words in their names.

All you need is to specify that info in binding provider.

var bindingPolicy = new AutoScanViewModelsToViewsBindingInfoProvider("View", "Views");

Warn

All your views must be placed in single namespace

ServiceCollection registration

Default navigation registration

private void AddNavigation(IServiceCollection serviceCollection)
{
    IViewModelsToViewsBindingInfoProvider bindingPolicy;

    bindingPolicy = new HandViewModelsToViewsBindingInfoProvider()
        .Bind<HomeViewModel, HomeView>()
        .Bind<AllSchedulesViewModel, AllSchedulesView>()
        .Bind<CreatingScheduleViewModel, CreatingScheduleView>()
        .Bind<ScheduleViewModel, ScheduleView>();

    //bindingPolicy = new AutoScanViewModelsToViewsBindingInfoProvider("View", "Views");

    //bindingPolicy = new AttributesViewModelsToViewsBindingInfoProvider();

    serviceCollection.AddNavigation(typeof(HomeViewModel).Assembly, typeof(HomeView).Assembly, b =>
    {
        b.UseDefaultNavigation();
        //or b.UseDefaultNavigation(ServiceLifetime.Transient);
        b.BindViewModelsToViews(Current, bindingPolicy);
    });
}

Chained navigation registration

serviceCollection.AddNavigation(typeof(HomeViewModel).Assembly, typeof(HomeView).Assembly, b =>
{
    b.UseChainNavigation();
    b.BindViewModelsToViews(Current, bindingPolicy);
});

Registration With EntityContainingViewModelsNavigation

serviceCollection.AddNavigation(typeof(HomeViewModel).Assembly, typeof(HomeView).Assembly, b =>
{
    b.UseDefaultNavigation();
    //or b.UseChainNavigation();
    b.AddEntityContainingNavigation(typeof(ScheduleModelToScheduleViewModelNavigationFactory).Assembly);
    b.BindViewModelsToViews(Current, bindingPolicy);
});

Creating Navigation Control

Last thing you need to do is create NavigationBar control and place it in MainWindow. It can look like this:

<Grid Width="300">
    <Grid.RowDefinitions>
        <RowDefinition Height="60"/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <Grid Grid.Row="0">
        <StackPanel Orientation="Horizontal">
            <Button Content="&lt;" Width="60" Command="{Binding MoveCommand}"
                    CommandParameter="{x:Static nav:NavigationMoveDirection.Back}"
                    IsEnabled="{Binding CanMoveBack}"/>
            <Button Content="&gt;" Width="60" Command="{Binding MoveCommand}"
                    CommandParameter="{x:Static nav:NavigationMoveDirection.Forward}"
                    IsEnabled="{Binding CanMoveForward}"/>
            <Button Content="Reset" Width="60" Command="{Binding ResetCommand}"/>
        </StackPanel>
    </Grid>
    <Grid Grid.Row="1">
        <ScrollViewer>
            <StackPanel>
                <Button Content="Home" Command="{Binding NavigateCommand}"
                    CommandParameter="{x:Type viewmodels:HomeViewModel}"/>
                <Button Content="All schedules" Command="{Binding NavigateCommand}"
                    CommandParameter="{x:Type viewmodels:AllSchedulesViewModel}"/>
                <Button Content="Create schedule" Command="{Binding NavigateCommand}"
                    CommandParameter="{x:Type viewmodels:CreatingScheduleViewModel}"/>
            </StackPanel>
        </ScrollViewer>
    </Grid>
</Grid>

And finally place it in MainWindow and set binding to CurrentViewModel in NavigationViewModel

<Grid Background="#181818">
    <DockPanel>
        <controls:NavigationBar DockPanel.Dock="Left" DataContext="{Binding NavigationViewModel}"/>
        <Border>
            <ContentPresenter Content="{Binding NavigationViewModel.CurrentViewModel}" Grid.Row="1"/>
        </Border>
    </DockPanel>
</Grid>

Now it should work!

See working sample!

ezgif-3-a776a90eba

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
2.0.6 490 10/4/2022
2.0.5 397 10/4/2022
2.0.4 400 10/4/2022
2.0.3 397 10/3/2022
2.0.2 385 10/3/2022
2.0.1 408 10/3/2022
2.0.0 410 10/3/2022
1.0.1 416 9/28/2022
1.0.0 416 9/28/2022

Added types for navigation between ViewModels in WPF