Closable Tabbed Views in Prism

image

Prism regions make it easy to change the layout of views within an application. A region is a logical placeholder associated with a specific layout control. Displaying a view in a region causes the view to be added to the layout control. But because the region and the layout control are loosely coupled, you can easily swap the layout control for a different one without having to change the modules in the application. For example, you can change from a tabbed view to a single document view.

While Prism makes it easy to add views to regions, it isn’t obvious how to close and remove a view from a region, especially from the UI. A common scenario is when you have multiple views displayed in tab control and the user can close individual views using a close button on the tab header. You can see this tabbed view style of UI in Internet Explorer and in Visual Studio.

This sample shows how this can be done. It’s based on the quick start solution that’s provided by the Prism 4.0 Template Pack. There are really only a couple of minor changes to the quick start solution’s shell. All of the real work is done in a new re-usable Blend action class called CloseTabbedViewAction.

The Shell View

Before we dig into that class, let’s take a look at the changes in the shell’s view. The TabControl that is used to layout views in the Main region is declared as:

    1: <sdk:TabControl prism:RegionManager.RegionName="MainRegion" ...
    2:     prism:TabControlRegionAdapter.ItemContainerStyle=
    3:            "{StaticResource TabHeaderStyle}">
    4:     ...
    5: </sdk:TabControl>

 

with the item container style defined as:

    1: <Style x:Key="TabHeaderStyle" TargetType="sdk:TabItem">
    2:   <Setter Property="HeaderTemplate">
    3:     <Setter.Value>
    4:        <DataTemplate>
    5:         <StackPanel Orientation="Horizontal">
    6:           <Image ... />
    7:           <TextBlock ... />
    8:           <Button Content="x" ToolTipService.ToolTip="Close this view." ...>
    9:             <ei:Interaction.Triggers>
   10:               <ei:EventTrigger EventName="Click">
   11:                 <local:CloseTabbedViewAction />
   12:               </ei:EventTrigger>
   13:             </ei:Interaction.Triggers>
   14:           </Button>
   15:         </StackPanel>
   16:       </DataTemplate>
   17:     </Setter.Value>
   18:   </Setter>
   19: </Style>

 

The tab header styles defines the header template for each tab item. This template defines a button that’s simply triggers the CloseTabbedViewAction on line 12. That’s all the changes we need in the shell. This approach allows us to keep the logic behind this action nicely encapsulated.

 

The CloseTabbedViewAction Class

When the user invokes the CloseTabbedView action, we just want the view to be removed from the region. The Prism Region class defines a Remove method for just this situation. Once we call the Remove method, the associated TabControl will be automatically updated and the TabItem that’s being used to display the view will be cleanly removed. Pretty easy right? Well, unfortunately it’s not quite that easy…

 

The Remove method takes a reference to the view to be removed. So in the Invoke method of the action class we need to somehow get a reference to the view and to the region that’s (logically) hosting it.

 

The parameter to the Invoke method provides a reference to the element that triggered the action (in this case the button defined as part of the tab header template). This element is available through the OriginalSource property. From there we can get a reference to the parent TabItem and TabControl by traversing up the visual tree. The CloseTabbedViewAction class defines a helper method called FindVisualParent to help us do this:

    1: private T FindVisualParent<T>( DependencyObject node ) where T : DependencyObject
    2: {
    3:     DependencyObject parent = VisualTreeHelper.GetParent( node );
    4:     if ( parent == null || parent is T ) return (T)parent;
    5:  
    6:     // Recurse up the visual tree.
    7:     return FindVisualParent<T>( parent );
    8: }

 

The Content property of the TabItem provides a reference to the view that’s being displayed. OK, that gives us the view reference, but what the region? Happily, the Prism RegionManager class provides a method called GetObservabelRegion which returns a reference to the region, if any, associated with a control. We can simply call that method and we now have the references we need. The Invoke method now looks something like this:

    1: public class CloseTabbedViewAction : TriggerAction<FrameworkElement>
    2: {
    3:     protected override void Invoke( object parameter )
    4:     {
    5:         RoutedEventArgs args = parameter as RoutedEventArgs;
    6:         if ( args == null ) return;
    7:  
    8:         // Find the parent tab item that contains the view to remove.
    9:         TabItem tabItem = FindVisualParent<TabItem>( args.OriginalSource as DependencyObject );
   10:  
   11:         // Find the parent tab control that represents the region.
   12:         TabControl tabControl = FindVisualParent<TabControl>( tabItem );
   13:  
   14:         if ( tabControl != null && tabItem != null )
   15:         {
   16:             // Get the view.
   17:             object view = tabItem.Content;
   18:  
   19:             // Get the region associated with the tab control.
   20:             IRegion region = RegionManager.GetObservableRegion( tabControl ).Value;
   21:             if ( region != null )
   22:             {
   23:                 region.Remove( view );
   24:             }
   25:         }
   26:     }

 

Integrating with Region Navigation

The code above removes the view from the region ok, but in a real application we probably want the view (or its view model) to be informed that the user is closing the view. This would allow the view to validate itself, save its state, etc. In some cases the view may want to be able to cancel the close operation entirely.

 

In Prism 4.0, we added support for Region Navigation. This extends Prism’s region concept to provide a Uri based navigation mechanism for displaying views in regions. It also includes support for the MVVM pattern by allowing views and view models to participate in navigation. To enable this, two interfaces were defined: INavigationAware and IConfirmNavigationRequest. You can implement these interfaces on your view or view model. The methods they define allow the view or view model to be notified before and after navigation has occurred, and in the latter case, to defer navigation pending confirmation by the user.

 

We can consider the closure of a view a navigation operation. If the view (or it’s view model) implements one of these interfaces, then we can allow them to participate in the operation by calling the appropriate methods. To do this, we need a helper method to find out whether the view or the view model implements the interface and, if so, return a reference to the implementor:

    1: private T Implementor<T>( object content ) where T : class
    2: {
    3:     T impl = content as T;
    4:     if ( impl == null )
    5:     {
    6:         FrameworkElement element = content as FrameworkElement;
    7:         if ( element != null ) impl = element.DataContext as T;
    8:     }
    9:     return impl;
   10: }

 

If either the view or view model implement the specific interface, we need to call the appropriate method on it. We’ll define another helper method for that:

    1: private bool NotifyIfImplements<T>( object content,
    2:                  Action<T> action ) where T : class
    3: {
    4:     bool notified = false;
    5:  
    6:     // Get the implementor of the specified interface -
    7:     // either the view or the view model.
    8:     T target = Implementor<T>( content );
    9:     if ( target != null )
   10:     {
   11:         action( target );
   12:         notified = true;
   13:     }
   14:     return notified;
   15: }

 

Ok, so far so good. Now all we need to do is to modify the Invoke method to call the NotifyIfImplements helper method for the INavigationAware interface.

    1: NavigationContext context = new NavigationContext( region.NavigationService, null );
    2:  
    3: // See if the view (or its view model) supports the INavigationAware interface.
    4: // If so, call the OnNavigatedFrom method.
    5: NotifyIfImplements<INavigationAware>( view, i => i.OnNavigatedFrom( context ) );
    6:  

 

The code above causes the OnNavigatedFrom method to be called on the view or view model when it is closed. This allows it to do whatever it needs to do to save its state, etc.

 

We make a similar call to the NotifyIfImplements helper method for the IConfirmNavigationRequest interface:

    1: // See if the view (or its view model) supports the
    2: // IConfirmNavigation interface.
    3: // If so, call the ConfirmNavigationRequest method.
    4: // If not, just remove the view from the region. 
    5: if ( !NotifyIfImplements<IConfirmNavigationRequest>( view,
    6:     i => i.ConfirmNavigationRequest( context,
    7:     canNavigate => { if ( canNavigate ) if ( region != null )
    8:                                region.Remove( view ); } ) ) )
    9: {
   10:     // Remove the view.
   11:     region.Remove( view );
   12: }

This is a little more complicated because we have to allow the view to prompt the user before closing the view. To do that we defer the removal of the view from the region by defining it within a delegate which gets called once the user interaction is completed. In the quick start template, the Edit View allows you to choose whether or not to confirm navigation away from the view using a check box. If you check it, you will be prompted to confirm the view’s closure.

 

Ok, so there you have it. By using the CloseTabbedViewAction class you can easily implement a tabbed view style interface using Prism. This action class works with both Silverlight and WPF. Both samples are provided here. Let me know what you think!