Search code examples
c#wpfmvvmprismprism-6

Making Tab Items and Prism navigation play nice with each other


The application that I am currently writing uses a Tab Control as its primary menu structure. Tabs allow caching of views; the tab retains the loaded view when another tab is selected.

Each tab contains a Region placeholder:

<TabControl>
    <TabItem Name="Home">
        <ContentControl prism:RegionManager.RegionName="HomeRegion"/>
    </TabItem>
    <TabItem Name="Project" Header="Program">
        <ContentControl prism:RegionManager.RegionName="ProjectRegion"/>
    </TabItem>
    <TabItem Name="TestPlan" Header="Test Plan">
        <ContentControl prism:RegionManager.RegionName="TestPlanRegion"/>
    </TabItem>
</TabControl>

PRISM provides for navigation to Tab Items via the INavigationAware interface, automagically creating the View in a new tab if it doesn't already exist, or refreshing an existing View in an existing tab.

public class PersonDetailViewModel : INavigationAware
{
    // Data-Bound property
    public Person SelectedPerson { get; set; }

    // INavigationAware method
    public void OnNavigatedTo(NavigationContext navigationContext)
    {
        var person = navigationContext.Parameters["person"] as Person;
        if (person != null)
            SelectedPerson = person;
    }
}

The navigation is accomplished thusly:

var parameters = new NavigationParameters();
parameters.Add("person", person);

if (person != null)
    _regionManager.RequestNavigate("PersonDetailsRegion", "PersonDetail", parameters);

Here's the problem: clicking a tab does not accomplish the usual navigation. It merely displays the existing view, which is already loaded on startup. So there's no opportunity for refreshing the tab when the tab is selected.

The behavior I want: I'd like the tab to load its content on first use, whether that's via a RequestNavigate call or clicking the tab, and then re-use its content on subsequent uses, including the passing of parameters like Person.

Unfortunately, at the moment, I think I'm left with two less than ideal choices:

  1. Simulate a RequestNavigate when a user clicks on the tab, presumably by hooking the SelectionChanged event on the Tab Control. For this to work, I need to lazy load the View into the tab, since applying a RequestNavigate on a tab click will make the tab load twice on first use, defeating any caching benefit I might have, or

  2. Use conventional menus that RequestNavigate to a single Region instead of tabs, which has the virtue of simplicity, but which defeats the caching benefits altogether.

Any thoughts on how I might make this work?


Solution

  • 2.Use conventional menus that RequestNavigate to a single Region instead of tabs, which has the virtue of simplicity, but which defeats the caching benefits altogether

    Not when I tried it... the region reuses a view, normally, when you navigate to it repeatedly (if you tell it to by returning true from INavigationAware.IsNavigationTarget) and INavigationAware.OnNavigatedTo can be used to update the view.

    Example:

    internal class MainWindowViewModel : BindableBase
    {
        public MainWindowViewModel( IRegionManager regionManager )
        {
            NavigateCommand = new DelegateCommand<string>( x => regionManager.RequestNavigate( "MainRegion", "PersonView", new NavigationParameters {{"Id", x}} ) );
        }
    
        public DelegateCommand<string> NavigateCommand { get; }
    }
    
    internal class PersonViewModel : BindableBase, INavigationAware
    {
        public PersonViewModel()
        {
            Debugger.Break();
        }
    
        private string _name;
    
        public string Name
        {
            get { return _name; }
            set { SetProperty( ref _name, value ); }
        }
    
        public void OnNavigatedTo( NavigationContext navigationContext )
        {
            Name = (string)navigationContext.Parameters[ "Id" ];
        }
    
        public bool IsNavigationTarget( NavigationContext navigationContext )
        {
            return true;
        }
    
        public void OnNavigatedFrom( NavigationContext navigationContext )
        {
        }
    }
    
    public partial class PersonView : UserControl
    {
        public PersonView()
        {
            Debugger.Break();
            InitializeComponent();
        }
    }
    
    <Window x:Class="PrismApplication1.Views.MainWindow"
                    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                    xmlns:mvvm="http://prismlibrary.com/"
                    Title="MainWindow"
                    Width="525"
                    Height="350"
                    mvvm:ViewModelLocator.AutoWireViewModel="True"
                    mc:Ignorable="d">
        <DockPanel LastChildFill="True">
            <Button Command="{Binding NavigateCommand}"
                            CommandParameter="Person A"
                            Content="Person A" />
            <Button Command="{Binding NavigateCommand}"
                            CommandParameter="Person B"
                            Content="Person B" />
            <ContentControl mvvm:RegionManager.RegionName="MainRegion" />
        </DockPanel>
    </Window>
    

    Click the two buttons in turns, and you'll see that you hit each break point only once. The view model as well as the view are retained and updated with the new navigation parameters.

    If you return false from INavigationAware.IsNavigationTarget, the view won't be reused.