« Prism navigation with tabcontrol » : différence entre les versions
De Banane Atomic
Aller à la navigationAller à la recherche
(25 versions intermédiaires par le même utilisateur non affichées) | |||
Ligne 1 : | Ligne 1 : | ||
[[Category:WPF]] | [[Category:WPF]] | ||
= Navigation = | = Navigation = | ||
Injecting a view in the TabControl region will automatically create a new TabItem or select an existing TabItem. | |||
<filebox fn='MainWindow.xaml'> | <filebox fn='MainWindow.xaml'> | ||
<Window x:Class="NavigationWPF.Views.MainWindow" | <Window x:Class="NavigationWPF.Views.MainWindow" | ||
xmlns:prism="http://prismlibrary.com/" | xmlns:prism="http://prismlibrary.com/" | ||
prism:ViewModelLocator.AutoWireViewModel="True | prism:ViewModelLocator.AutoWireViewModel="True"> | ||
<Window.Resources> | <Window.Resources> | ||
Ligne 22 : | Ligne 22 : | ||
<StackPanel Orientation="Horizontal"> | <StackPanel Orientation="Horizontal"> | ||
<Button Command="{Binding NavigateCommand}" | <Button Command="{Binding NavigateCommand}" | ||
CommandParameter=" | CommandParameter="ItemsListView"> | ||
Items | |||
</Button> | </Button> | ||
</StackPanel> | </StackPanel> | ||
Ligne 42 : | Ligne 38 : | ||
private readonly IRegionManager _regionManager; | private readonly IRegionManager _regionManager; | ||
public DelegateCommand<string> NavigateCommand { get | public DelegateCommand<string> NavigateCommand { get; } | ||
public MainWindowViewModel(IRegionManager regionManager) | public MainWindowViewModel(IRegionManager regionManager) | ||
Ligne 50 : | Ligne 46 : | ||
} | } | ||
// | // ItemsListView is a command parameter passed as navigationPath | ||
void Navigate(string navigationPath) | void Navigate(string navigationPath) | ||
{ | { | ||
Ligne 64 : | Ligne 60 : | ||
{ | { | ||
// register the views to allow RequestNavigate with the view name | // register the views to allow RequestNavigate with the view name | ||
containerRegistry.RegisterForNavigation< | containerRegistry.RegisterForNavigation<ItemsListView>(); | ||
containerRegistry.RegisterForNavigation< | containerRegistry.RegisterForNavigation<ItemView>(); | ||
} | } | ||
</filebox> | </filebox> | ||
== | == Views and ViewModels == | ||
<filebox fn=' | <filebox fn='ItemsListView.xaml' collapsed> | ||
<UserControl x:Class="NavigationWPF.Views. | <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> | |||
</filebox> | |||
<filebox fn='ItemsListViewModel.cs' collapsed> | |||
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) { } | |||
} | |||
</filebox> | |||
<filebox fn='ItemView.xaml' collapsed> | |||
<UserControl x:Class="NavigationWPF.Views.ItemView" | |||
xmlns:mvvm="http://prismlibrary.com/" | xmlns:mvvm="http://prismlibrary.com/" | ||
mvvm:ViewModelLocator.AutoWireViewModel="True"> | mvvm:ViewModelLocator.AutoWireViewModel="True"> | ||
<Grid> | <Grid> | ||
<TextBlock Text=" | <TextBlock Text="{Binding Name}" | ||
HorizontalAlignment="Center" | HorizontalAlignment="Center" | ||
VerticalAlignment="Center" | VerticalAlignment="Center" | ||
Ligne 83 : | Ligne 152 : | ||
</filebox> | </filebox> | ||
<filebox fn=' | <filebox fn='ItemViewModel.cs' collapsed> | ||
public class | public sealed class ItemViewModel : BindableBase, INavigationAware | ||
{ | { | ||
private string | private string _name; | ||
private string _title; | |||
public string Name | |||
{ | |||
get => _name; | |||
set => SetProperty(ref _name, value); | |||
} | |||
public string Title | public string Title | ||
{ | { | ||
get => | get => _title; | ||
set => SetProperty(ref this. | 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) { } | |||
} | |||
</filebox> | |||
== RequestNavigateOnLoadedBehavior == | |||
Allow to inject a view into a region when the {{boxx|Window.Loaded}} event is raised. | |||
<filebox fn='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" /> | |||
</filebox> | |||
<filebox fn='RequestNavigateOnLoadedBehavior.cs'> | |||
public sealed class RequestNavigateOnLoadedBehavior : Behavior<Window> | |||
{ | |||
public string RegionName | |||
{ | |||
get => (string) GetValue(RegionNameProperty); | |||
set => SetValue(RegionNameProperty, value); | |||
} | } | ||
public | 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 | 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); | |||
} | |||
} | } | ||
</filebox> | </filebox> | ||
Ligne 119 : | Ligne 260 : | ||
<DockPanel> | <DockPanel> | ||
<Button DockPanel.Dock="Right" | <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}" | <ContentControl Content="{Binding}" | ||
VerticalAlignment="Center" /> | VerticalAlignment="Center" /> | ||
Ligne 134 : | Ligne 278 : | ||
<kode lang='xaml' collapsed> | <kode lang='xaml' collapsed> | ||
<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" | <ControlTemplate x:Key="CrossButton" | ||
TargetType="{x:Type Button}"> | TargetType="{x:Type Button}"> | ||
Ligne 177 : | Ligne 330 : | ||
</ControlTemplate> | </ControlTemplate> | ||
</kode> | </kode> | ||
<filebox fn='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) | |||
} | |||
} | |||
</filebox> | |||
* [https://wiki.bananeatomic.fr/wiki/WPF#Get_parent_of_a_specific_type VisualTreeHelperPlus.GetParent] | |||
== Preventing close == | |||
<filebox fn='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; | |||
} | |||
</filebox> | |||
<filebox fn='ViewAViewModel.cs'> | |||
public sealed class ViewAViewModel : BindableBase, INavigationAware, IConfirmNavigationRequest | |||
{ | |||
// ... | |||
public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback) | |||
{ | |||
continuationCallback(false); // forbid to close | |||
} | |||
} | |||
</filebox> | |||
== Knowing when a TabItem is closing == | |||
<filebox fn='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); | |||
} | |||
} | |||
</filebox> | |||
<filebox fn='ViewAViewModel.cs'> | |||
public sealed class ViewAViewModel : BindableBase, INavigationAware, IConfirmNavigationRequest | |||
{ | |||
public void OnNavigatedFrom(NavigationContext navigationContext) | |||
{ | |||
} | |||
} | |||
</filebox> | |||
= Child navigation = | |||
<filebox fn='ItemView.xaml'> | |||
<!-- add a TabControl with a region named ChildRegion in the ItemView --> | |||
<TabControl Grid.Row="1" mvvm:RegionManager.RegionName="ChildRegion" /> | |||
</filebox> | |||
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.<br> | |||
Solution to this is to use scoped regions: 1 RegionManager per ItemView. | |||
== ScopedRegionNavigationContentLoader == | |||
<filebox fn='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; | |||
} | |||
} | |||
</filebox> | |||
<filebox fn='App.xaml.cs'> | |||
protected override void RegisterTypes(IContainerRegistry containerRegistry) | |||
{ | |||
// register the ScopedRegionNavigationContentLoader to be the IRegionNavigationContentLoader used by this App | |||
containerRegistry.RegisterSingleton<IRegionNavigationContentLoader, ScopedRegionNavigationContentLoader>(); | |||
} | |||
</filebox> | |||
<filebox fn='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; | |||
} | |||
</filebox> | |||
== RegionManagerAware == | |||
Problem is now we doesn't want to access to the main RegionManager but to the scoped RegionManager (the one of the ItemView) to inject a view inside. | |||
<filebox fn='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); | |||
} | |||
} | |||
} | |||
</filebox> | |||
<filebox fn='IRegionManagerAware.cs'> | |||
public interface IRegionManagerAware | |||
{ | |||
IRegionManager RegionManager { get; set; } | |||
} | |||
</filebox> | |||
Replace the main RegionManager by the scoped RegionManager | |||
<filebox fn='ItemViewModel.cs'> | |||
public sealed class ItemViewModel : BindableBase, INavigationAware, ICreateRegionManagerScope, IRegionManagerAware | |||
{ | |||
//private readonly IRegionManager _regionManager; | |||
public IRegionManager RegionManager { get; set; } | |||
public ItemViewModel(IItemRepository itemRepository/*, IRegionManager regionManager*/) | |||
{ | |||
_itemRepository = itemRepository; | |||
//_regionManager = regionManager; | |||
} | |||
public void OnNavigatedTo(NavigationContext navigationContext) | |||
{ | |||
var navigationParameters = new NavigationParameters | |||
{ | |||
{ "action", "child" }, | |||
{ "itemId", Item.Id } | |||
}; | |||
//_regionManager.RequestNavigate("ChildRegion", "ItemsListView", navigationParameters); | |||
RegionManager.RequestNavigate("ChildRegion", "ItemsListView", navigationParameters); | |||
} | |||
} | |||
</filebox> |
Dernière version du 20 octobre 2021 à 23:12
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); } } |
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) { } } |
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) { } } |
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.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
Problem is now we doesn't want to access to the main RegionManager but to the scoped RegionManager (the one of the ItemView) to inject a view inside.
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; } } |
Replace the main RegionManager by the scoped RegionManager
ItemViewModel.cs |
public sealed class ItemViewModel : BindableBase, INavigationAware, ICreateRegionManagerScope, IRegionManagerAware { //private readonly IRegionManager _regionManager; public IRegionManager RegionManager { get; set; } public ItemViewModel(IItemRepository itemRepository/*, IRegionManager regionManager*/) { _itemRepository = itemRepository; //_regionManager = regionManager; } public void OnNavigatedTo(NavigationContext navigationContext) { var navigationParameters = new NavigationParameters { { "action", "child" }, { "itemId", Item.Id } }; //_regionManager.RequestNavigate("ChildRegion", "ItemsListView", navigationParameters); RegionManager.RequestNavigate("ChildRegion", "ItemsListView", navigationParameters); } } |