« 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">
        Title="MainWindow" Height="350" Width="525">


     <Window.Resources>
     <Window.Resources>
Ligne 22 : Ligne 22 :
         <StackPanel Orientation="Horizontal">
         <StackPanel Orientation="Horizontal">
             <Button Command="{Binding NavigateCommand}"
             <Button Command="{Binding NavigateCommand}"
                     CommandParameter="ViewA">
                     CommandParameter="ItemsListView">
                 Navigate A
                 Items
            </Button>
            <Button Command="{Binding NavigateCommand}"
                    CommandParameter="ViewB">
                Navigate B
             </Button>
             </Button>
         </StackPanel>
         </StackPanel>
Ligne 42 : Ligne 38 :
     private readonly IRegionManager _regionManager;
     private readonly IRegionManager _regionManager;


     public DelegateCommand<string> NavigateCommand { get; set; }
     public DelegateCommand<string> NavigateCommand { get; }


     public MainWindowViewModel(IRegionManager regionManager)
     public MainWindowViewModel(IRegionManager regionManager)
Ligne 50 : Ligne 46 :
     }
     }


     // ViewA and ViewB are command parameters passed as navigationPath
     // 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<ViewA>();
     containerRegistry.RegisterForNavigation<ItemsListView>();
     containerRegistry.RegisterForNavigation<ViewB>();
     containerRegistry.RegisterForNavigation<ItemView>();
}
}
</filebox>
</filebox>


== View to inject ==
== Views and ViewModels ==
<filebox fn='ViewA.xaml'>
<filebox fn='ItemsListView.xaml' collapsed>
<UserControl x:Class="NavigationWPF.Views.ViewA"
<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="View A"
         <TextBlock Text="{Binding Name}"
                   HorizontalAlignment="Center"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   VerticalAlignment="Center"
Ligne 83 : Ligne 152 :
</filebox>
</filebox>


<filebox fn='ViewAViewModel.cs'>
<filebox fn='ItemViewModel.cs' collapsed>
public class ViewAViewModel : BindableBase, INavigationAware
public sealed class ItemViewModel : BindableBase, INavigationAware
{
{
     private string title;
     private string _name;
    private string _title;
 
    public string Name
    {
        get => _name;
        set => SetProperty(ref _name, value);
    }


     public string Title
     public string Title
     {
     {
         get => this.title;
         get => _title;
         set => SetProperty(ref this.title, value);
         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 ViewAViewModel()
     public static readonly DependencyProperty RegionNameProperty = DependencyProperty.Register(
        nameof(RegionName),
        typeof(string),
        typeof(RequestNavigateOnLoadedBehavior),
        new PropertyMetadata(null));
 
    public string ViewName
     {
     {
         Title = "View A";
         get => (string) GetValue(ViewNameProperty);
        set => SetValue(ViewNameProperty, value);
     }
     }


     public bool IsNavigationTarget(NavigationContext navigationContext) => true;
     public static readonly DependencyProperty ViewNameProperty = DependencyProperty.Register(
        nameof(ViewName),
        typeof(string),
        typeof(RequestNavigateOnLoadedBehavior),
        new PropertyMetadata(null));


     public void OnNavigatedFrom(NavigationContext navigationContext)
     protected override void OnAttached()
     { }
     {
        this.AssociatedObject.Loaded += OnLoaded;
    }
 
    protected override void OnDetaching()
    {
        this.AssociatedObject.Loaded -= OnLoaded;
    }


     public void OnNavigatedTo(NavigationContext navigationContext)
     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"
                                 Foreground="DarkGray"
                                 Style="{StaticResource TabCloseButton}">
                                 Height="14"
                            <b:Interaction.Triggers>
                                 Margin="4,0,-4,0"
                                 <b:EventTrigger EventName="Click">
                                Template="{StaticResource CrossButton}" />
                                    <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

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>
Xaml.svg
<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

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);
    }
}