PhlegmaticOne.WPF.Navigation
2.0.6
dotnet add package PhlegmaticOne.WPF.Navigation --version 2.0.6
NuGet\Install-Package PhlegmaticOne.WPF.Navigation -Version 2.0.6
<PackageReference Include="PhlegmaticOne.WPF.Navigation" Version="2.0.6" />
paket add PhlegmaticOne.WPF.Navigation --version 2.0.6
#r "nuget: PhlegmaticOne.WPF.Navigation, 2.0.6"
// Install PhlegmaticOne.WPF.Navigation as a Cake Addin #addin nuget:?package=PhlegmaticOne.WPF.Navigation&version=2.0.6 // Install PhlegmaticOne.WPF.Navigation as a Cake Tool #tool nuget:?package=PhlegmaticOne.WPF.Navigation&version=2.0.6
PhlegmaticOne.WPF.Navigation
Nuget package
Installation
PM> NuGet\Install-Package PhlegmaticOne.WPF.Navigation -Version 2.0.6
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
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 callOnPropertyChanged
method explicitlyPhlegmaticOne.WPF.Core
- provides base type for all Models -EntityViewModelBase
, which implementsINotifyPropertyChanged
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
NavigationViewModel
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;
}
}
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);
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;
}
}
EntityContainingViewModelsNavigation
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.
NavigationFactory
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="<" 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>
Now it should work!
See working sample!
Product | Versions 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. |
-
net6.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 6.0.0)
- PhlegmaticOne.WPF.Core (>= 1.1.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Moved to WPF.Core v. 1.1.4