Navigation
Injecting a view in the TabControl region will automatically create a new TabItem or select an existing TabItem.
MainWindow.xaml
|
<Window x:Class="NavigationWPF.Views.MainWindow"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True">
<Window.Resources>
<Style TargetType="TabItem">
<Setter Property="Header"
Value="{Binding DataContext.Title}" />
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<Button Command="{Binding NavigateCommand}"
CommandParameter="ItemsListView">
Items
</Button>
</StackPanel>
<TabControl Grid.Row="1"
prism:RegionManager.RegionName="TabRegion" />
</Grid>
</Window>
|
MainWindowViewModel.cs
|
public class MainWindowViewModel : BindableBase
{
private readonly IRegionManager _regionManager;
public DelegateCommand<string> NavigateCommand { get; }
public MainWindowViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
NavigateCommand = new DelegateCommand<string>(Navigate);
}
// ItemsListView is a command parameter passed as navigationPath
void Navigate(string navigationPath)
{
// open/reuse a tab and inject a view that has the same name as the navigationPath
_regionManager.RequestNavigate("TabRegion", navigationPath);
}
}
|
Navigation configuration
App.xaml.cs
|
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// register the views to allow RequestNavigate with the view name
containerRegistry.RegisterForNavigation<ItemsListView>();
containerRegistry.RegisterForNavigation<ItemView>();
}
|
Views and ViewModels
ItemsListView.xaml
|
<UserControl x:Class="NavigationWPF.Views.ItemsListView"
xmlns:mvvm="http://prismlibrary.com/"
mvvm:ViewModelLocator.AutoWireViewModel="True">
<ListBox ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}">
<TextBlock.InputBindings>
<MouseBinding MouseAction="LeftDoubleClick"
Command="{Binding DataContext.OpenItemCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ListBox}}"
CommandParameter="{Binding}" />
</TextBlock.InputBindings>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</UserControl>
|
ItemsListViewModel.cs
|
public sealed class ItemsListViewModel : BindableBase, IConfirmNavigationRequest
{
private readonly IItemRepository _itemRepository;
private readonly IRegionManager _regionManager;
private string _title;
public AdvancedObservableCollection<Item> Items { get; }
public string Title
{
get => this._title;
set => SetProperty(ref this._title, value);
}
public ICommand OpenItemCommand { get; }
public ItemsListViewModel(IItemRepository itemRepository, IRegionManager regionManager)
{
this._itemRepository = itemRepository;
this._regionManager = regionManager;
Items = new AdvancedObservableCollection<Item>();
Title = "Items";
OpenItemCommand = new DelegateCommand<Item>(OpenItem);
var items = this._itemRepository.GetItems();
Items.AddRange(items);
}
private void OpenItem(Item item)
{
var navigationParameters = new NavigationParameters
{
{ "item", item }
};
// pass the item while navigating to the ItemView
this._regionManager.RequestNavigate("TabRegion", "ItemView", navigationParameters);
}
public bool IsNavigationTarget(NavigationContext navigationContext) => true; // always reuse the TabItem
public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
// allow to navigate from this TabItem but not to close it (navigationContext.Uri == null)
continuationCallback(navigationContext.Uri != null);
}
public void OnNavigatedFrom(NavigationContext navigationContext) { }
public void OnNavigatedTo(NavigationContext navigationContext) { }
}
|
ItemView.xaml
|
<UserControl x:Class="NavigationWPF.Views.ItemView"
xmlns:mvvm="http://prismlibrary.com/"
mvvm:ViewModelLocator.AutoWireViewModel="True">
<Grid>
<TextBlock Text="{Binding Name}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="48" />
</Grid>
</UserControl>
|
ItemViewModel.cs
|
public sealed class ItemViewModel : BindableBase, INavigationAware
{
private string _name;
private string _title;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
public string Title
{
get => _title;
set => SetProperty(ref _title, value);
}
public Item Item { get; set; }
public void OnNavigatedTo(NavigationContext navigationContext)
{
var item = navigationContext.Parameters.GetValue<Item>("item");
this.Item = item;
this.Title = item.Name;
this.Name = item.Name;
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
var item = navigationContext.Parameters.GetValue<Item>("item");
// reuse the TabItem if it is for the same item; otherwise create a new TabItem
return item.Id == Item.Id;
}
public void OnNavigatedFrom(NavigationContext navigationContext) { }
}
|
RequestNavigateOnLoadedBehavior
Allow to inject a view into a region when the Window.Loaded event is raised.
MainWindow.xaml
|
<Window x:Class="NavigationWPF.Views.MainWindow"
xmlns:b="http://schemas.microsoft.com/xaml/behaviors">
<b:Interaction.Behaviors>
<behaviors:RequestNavigateOnLoadedBehavior RegionName="TabRegion"
ViewName="ItemsListView" />
</b:Interaction.Behaviors>
<Grid>
<TabControl prism:RegionManager.RegionName="TabRegion" />
|
RequestNavigateOnLoadedBehavior.cs
|
public sealed class RequestNavigateOnLoadedBehavior : Behavior<Window>
{
public string RegionName
{
get => (string) GetValue(RegionNameProperty);
set => SetValue(RegionNameProperty, value);
}
public static readonly DependencyProperty RegionNameProperty = DependencyProperty.Register(
nameof(RegionName),
typeof(string),
typeof(RequestNavigateOnLoadedBehavior),
new PropertyMetadata(null));
public string ViewName
{
get => (string) GetValue(ViewNameProperty);
set => SetValue(ViewNameProperty, value);
}
public static readonly DependencyProperty ViewNameProperty = DependencyProperty.Register(
nameof(ViewName),
typeof(string),
typeof(RequestNavigateOnLoadedBehavior),
new PropertyMetadata(null));
protected override void OnAttached()
{
this.AssociatedObject.Loaded += OnLoaded;
}
protected override void OnDetaching()
{
this.AssociatedObject.Loaded -= OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
var regionManager = RegionManager.GetRegionManager(sender as DependencyObject);
regionManager.RequestNavigate(RegionName, ViewName);
}
}
|
Closing Tab items
MainWindow.xaml
|
<Window.Resources>
<Style TargetType="TabItem">
<!-- redefine the header template with a button -->
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<DockPanel>
<Button DockPanel.Dock="Right"
Style="{StaticResource TabCloseButton}">
<b:Interaction.Triggers>
<b:EventTrigger EventName="Click">
<local:CloseTabAction />
</b:EventTrigger>
</b:Interaction.Triggers>
</Button>
<ContentControl Content="{Binding}"
VerticalAlignment="Center" />
</DockPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
|
|
<Style x:Key="TabCloseButton"
TargetType="Button"
BasedOn="Button">
<Setter Property="Height" Value="14" />
<Setter Property="Foreground" Value="DarkGray" />
<Setter Property="Margin" Value="4,0,-4,0" />
<Setter Property="Template" Value="{StaticResource CrossButton}" />
</Style>
<ControlTemplate x:Key="CrossButton"
TargetType="{x:Type Button}">
<Grid>
<Rectangle x:Name="BackgroundRectangle" />
<Path x:Name="ButtonPath"
Margin="3"
Stroke="{Binding Foreground, RelativeSource={RelativeSource TemplatedParent}}"
StrokeThickness="1.5"
StrokeStartLineCap="Square"
StrokeEndLineCap="Square"
Stretch="Uniform"
VerticalAlignment="Center"
HorizontalAlignment="Center">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigure StartPoint="0,0">
<LineSegment Point="25,25" />
</PathFigure>
<PathFigure StartPoint="0,25">
<LineSegment Point="25,0" />
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="BackgroundRectangle" Property="Fill" Value="LightGray" />
<Setter TargetName="ButtonPath" Property="Stroke" Value="White" />
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="IsPressed" Value="true">
<Setter TargetName="BackgroundRectangle" Property="Fill" Value="Gray" />
<Setter TargetName="ButtonPath" Property="Stroke" Value="White" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
|
CloseTabAction
|
public sealed class CloseTabAction : TriggerAction<Button>
{
protected override void Invoke(object parameter)
{
var args = parameter as RoutedEventArgs;
if (args == null)
return;
var tabItem = VisualTreeHelperPlus.GetParent<TabItem>(args.OriginalSource as DependencyObject);
if (tabItem == null)
return;
var tabControl = VisualTreeHelperPlus.GetParent<TabControl>(tabItem);
if (tabControl == null)
return;
var region = RegionManager.GetObservableRegion(tabControl).Value;
if (region == null)
return;
if (region.Views.Contains(tabItem.Content))
region.Remove(tabItem.Content); // remove the view from the region
// Prism will remove automatically the containing element (TabItem) from the hosting control (TabControl)
}
}
|
Preventing close
CloseTabAction.cs
|
protected override void Invoke(object parameter)
{
// ...
RemoveItemFromRegion(tabItem.Content, region);
}
private void RemoveItemFromRegion(object item, IRegion region)
{
var navigationContext = new NavigationContext(region.NavigationService, null);
if (CanRemove(item, navigationContext))
region.Remove(item);
}
private bool CanRemove(object item, NavigationContext navigationContext)
{
var canClose = true;
// check if the VM implements IConfirmNavigationRequest
if (item is FrameworkElement frameworkElement &&
frameworkElement.DataContext is IConfirmNavigationRequest confirmNavigationRequest)
{
confirmNavigationRequest.ConfirmNavigationRequest(navigationContext, x => canClose = x);
}
return canClose;
}
|
ViewAViewModel.cs
|
public sealed class ViewAViewModel : BindableBase, INavigationAware, IConfirmNavigationRequest
{
// ...
public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
continuationCallback(false); // forbid to close
}
}
|
Knowing when a TabItem is closing
CloseTabAction.cs
|
private void RemoveItemFromRegion(object item, IRegion region)
{
var navigationContext = new NavigationContext(region.NavigationService, null);
if (CanRemove(item, navigationContext))
{
OnNavigatedFrom(item, navigationContext);
region.Remove(item);
}
}
private void OnNavigatedFrom(object item, NavigationContext navigationContext)
{
if (item is FrameworkElement frameworkElement &&
frameworkElement.DataContext is INavigationAware navigationAwareDataContext)
{
navigationAwareDataContext.OnNavigatedFrom(navigationContext);
}
}
|
ViewAViewModel.cs
|
public sealed class ViewAViewModel : BindableBase, INavigationAware, IConfirmNavigationRequest
{
public void OnNavigatedFrom(NavigationContext navigationContext)
{
}
}
|
Child navigation
ItemView.xaml
|
<!-- add a TabControl with a region named ChildRegion in the ItemView -->
<TabControl Grid.Row="1" mvvm:RegionManager.RegionName="ChildRegion" />
|
Problem is the region names have to be unique. So if I implement 2 instances of ItemView I will have 2 regions with the name ChildRegion.
Solution to this is to use scoped regions: 1 RegionManager per ItemView.
ScopedRegionNavigationContentLoader
ScopedRegionNavigationContentLoader.cs
|
public sealed class ScopedRegionNavigationContentLoader : RegionNavigationContentLoader
{
public ScopedRegionNavigationContentLoader(IContainerExtension container) : base(container)
{ }
protected override void AddViewToRegion(IRegion region, object view)
{
// create a scoped region if the view or the vm has the property CreateRegionManagerScope == true
region.Add(view, null, CreateRegionManagerScope(view));
}
private bool CreateRegionManagerScope(object view)
{
if (view is ICreateRegionManagerScope viewHasScopedRegions)
{
return viewHasScopedRegions.CreateRegionManagerScope;
}
if (view is FrameworkElement frameworkElement &&
frameworkElement.DataContext is ICreateRegionManagerScope viewModelHasScopedRegions)
{
return viewModelHasScopedRegions.CreateRegionManagerScope;
}
return false;
}
}
|
App.xaml.cs
|
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// register the ScopedRegionNavigationContentLoader to be the IRegionNavigationContentLoader used by this App
containerRegistry.RegisterSingleton<IRegionNavigationContentLoader, ScopedRegionNavigationContentLoader>();
}
|
ItemViewModel.cs
|
public sealed class ItemViewModel : BindableBase, INavigationAware, ICreateRegionManagerScope
{
// ItemViewModel implements ICreateRegionManagerScope and set CreateRegionManagerScope to true
// so the regions inside the ItemView will be scoped regions
public bool CreateRegionManagerScope => true;
}
|
RegionManagerAware
RegionManagerAwareBehavior.cs
|
public class RegionManagerAwareBehavior : RegionBehavior
{
public const string BehaviorKey = nameof(RegionManagerAwareBehavior);
protected override void OnAttach()
{
Region.Views.CollectionChanged += OnCollectionChanged;
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var newItem in e.NewItems)
{
var regionManager = Region.RegionManager;
// if the view was created with a scoped region manager, the behavior uses that region manager instead.
if (newItem is FrameworkElement frameworkElement
&& frameworkElement.GetValue(RegionManager.RegionManagerProperty) is IRegionManager scopedRegionManager)
{
regionManager = scopedRegionManager;
}
InvokeOnRegionManagerAwareElement(newItem, x => x.RegionManager = regionManager);
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var oldItem in e.OldItems)
{
InvokeOnRegionManagerAwareElement(oldItem, x => x.RegionManager = null);
}
}
}
private static void InvokeOnRegionManagerAwareElement(object item, Action<IRegionManagerAware> invocation)
{
if (item is IRegionManagerAware regionManagerAwareItem)
{
invocation(regionManagerAwareItem);
}
if (item is FrameworkElement frameworkElement
&& frameworkElement.DataContext is IRegionManagerAware regionManagerAwareDataContext)
{
// if a view doesn't have a DataContext (VM) it will inherit from the DataContext of the parent view.
// the following check is done to avoid setting the RegionManager property in the VM of the parent view by mistake.
if (frameworkElement.Parent is FrameworkElement frameworkElementParent
&& frameworkElementParent.DataContext is IRegionManagerAware regionManagerAwareDataContextParent
&& regionManagerAwareDataContext == regionManagerAwareDataContextParent)
{
// if all of the previous conditions are true, it means that this view doesn't have a view model
// and is using the VM of its visual parent.
return;
}
invocation(regionManagerAwareDataContext);
}
}
}
|
IRegionManagerAware.cs
|
public interface IRegionManagerAware
{
IRegionManager RegionManager { get; set; }
}
|