PM> NuGet\Install-Package PhlegmaticOne.WPF.Navigation -Version 2.0.6
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
- 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
Install in your Models project (if it exists):
PropertyChanged.Fody
- it allows not to callOnPropertyChanged
method explicitlyPhlegmaticOne.WPF.Core
- provides base type for all Models -EntityViewModelBase
, which implementsINotifyPropertyChanged
interface
- PropertyChanged.Fody - it allows not to call
OnPropertyChanged
method explicitly - PhlegmaticOne.WPF.Navigation (this package) - provides WPF navigation
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
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);
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;
}
}
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);
NavigateCommand = RelayCommandFactory
.CreateRequiredParameterCommand<Type>(Navigate);
ResetCommand = RelayCommandFactory.CreateEmptyCommand(Reset);
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()
{
_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;
}
}
In order to use this navigation you need to implement NavigationFactoryBase<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);
}
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);
}
Now supported 3 policies to bind ViewModels to Views:
- Hand binding
- Attributes binding
- Auto binding
Let's see how they works.
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>();
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
{...}
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");
All your views must be placed in single namespace
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);
});
}
serviceCollection.AddNavigation(typeof(HomeViewModel).Assembly, typeof(HomeView).Assembly, b =>
{
b.UseChainNavigation();
b.BindViewModelsToViews(Current, bindingPolicy);
});
serviceCollection.AddNavigation(typeof(HomeViewModel).Assembly, typeof(HomeView).Assembly, b =>
{
b.UseDefaultNavigation();
//or b.UseChainNavigation();
b.AddEntityContainingNavigation(typeof(ScheduleModelToScheduleViewModelNavigationFactory).Assembly);
b.BindViewModelsToViews(Current, bindingPolicy);
});
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="<" Width="60" Command="{Binding MoveCommand}"
CommandParameter="{x:Static nav:NavigationMoveDirection.Back}"
IsEnabled="{Binding CanMoveBack}"/>
<Button Content=">" 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>