Closable Tabbed Views in Prism
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!
Comments
Anonymous
January 23, 2011
I belive there is something wrong with wpf+prism or I'm just too stupid to use it.Anonymous
January 23, 2011
Hi David, thanks for a great article.. i am trying to compile the attached solution but it seems that there is a file missing ("ConfirmNavigationAction.cs").. Could you please throw some light on that..Anonymous
January 24, 2011
Doh! Yes sorry, there was a file missing :-( I fixed the sample so it should compile ok now...Anonymous
January 25, 2011
The comment has been removedAnonymous
January 30, 2011
Hi David, First of all I would like to say thank you for the templates. They are brillant. I was struggling to understand how it all worked in Prism. I had seen the StockTraiderRI demo and source code and other quick start tutorials but they were so long and had so much code that I got lost. I have installed your templates and run the quickstart template and things have started to make sense the code is simple and short. I have a couple of questions and would appreciate if you could clarify them to me: 1 - For me to add a new module, should I add a new Silverlight Project or class library? 2 - Do you have any simple sample code where you use MEF instead of unity? I really would like to use the new MEF support in Prism but not know how. 3 - I have noticed that you are passing a string ID to the event aggregator. Would it be possible to pass a whole object to the event aggreagato like DataItem? 4 - Do you have produced any video tutorials (or know of any video tutorial) where they show how to use MEF with PRESM v4? Cheers CAnonymous
January 30, 2011
Hi David, Is it possible to use a View model Locator class with yout templates? I do not get any design data when in VS 2010. Cheers CAnonymous
February 07, 2011
David. Excellent work and the templates will be very useful. I am trying to construct a UX framework for Silverlight 4 using Prism 4. and am keen to provide a TabCotrol region for the main work area. I want the contained veiw/viewmodels to supply the tabItem header name as in your example. I have used the TabHeaderStle style from your ShellView.xaml (I am not using an image so have commented that peice out). I have also copied your CloseTabbedViewAction.cs to handle Tab closing. However, though the style is being applied to the TabItems, TabItems are created with the 'x' Button showing and closing TabItems when pressed the TextBlock text is not binding to the required property in the View's datacontext. Oviously I am missing something, First how does the 'binding' in your example bind to the ViewModels property as you do not reference the View (the tabItems content) in the Xaml. I would have expected something like <TextBlock Text="{Binding Content.DataContext.ViewName}"...../>. I am pretty new to Xaml and WPF/Silverlight not to mention Prism so any help would be much apreciatedAnonymous
February 15, 2011
Disregard the previous post. Completely self inflicted ignorance at workAnonymous
February 19, 2011
Great post David! This is something that everybody is still missing from PRISM. I did the same with Avalon and it is good to have a blog post that explains exactly how it works!Anonymous
April 08, 2011
Very nice example how can do the same thing with MEF and Jounce Framework ref : http://jounce.codeplex.com/