The Microsoft TabControl is a popular control to use as a region in a WPF Prism application.  Unfortunately, the Microsoft TabControl comes with some limitations, such as the inability to close tab items.  If you want to close tab items in the Microsoft TabControl, you need to implement this functionality yourself.  However, you can make this easy on yourself by using the Infragistics xamTabControl.  The xamTabControl extends the Microsoft TabControl and adds a number of features that you would normally have to write yourself otherwise.  For example; closing tab items is built into the control.  Another great thing is that nothing syntactically really changes when you use the xamTabControl as a Prism Region.  So switching out the TabControl for the Infragistics xamTabControl is no big deal.  Everything is the same.

Let’s take a look at a view that uses the xamTabControl as a region and adds support for closing tab items.

<Window.Resources>
    <Style TargetType="{x:Type igWPF:TabItemEx}">
        <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="ViewA" Content="Navigate A"/>
        <Button Command="{Binding NavigateCommand}" CommandParameter="ViewB" Content="Navigate B"/>
    </StackPanel>

    <igWPF:XamTabControl Grid.Row="1" prism:RegionManager.RegionName="TabRegion" />

</Grid>

As you can see, this is a very simple view.  It uses the xamTabControl as it’s region and it also has a couple of buttons on it that we will use to navigate to different views in the xamTabControl region.  So when you run the application and click the buttons a couple of times, you will end up with something like this:

image

Okay, it’s nothing special right now.  It looks just like it did if we were to use the Microsoft TabControl.  Now let’s say we want to add support for closing tab items.  Well, no problem!  This is as easy as using a property.  The TabItemCloseButtonVisibility property to be exact.

<igWPF:XamTabControl Grid.Row="1" prism:RegionManager.RegionName="TabRegion" TabItemCloseButtonVisibility="Visible" />

        

Run the application and now we get those cool little close buttons automatically.

xamtabcontrol-as-prism-region

Pretty cool right?  Heck yeah it’s cool!  Run the app and start opening and closing tabs until your heart’s content.  But……  We actually have a major issue here.  We are using Prism.  We have this thing called a Region.  This region holds all of the views we add to it until we remove them from the region.  Unfortunately, when you click on that pretty little close button, the view seems to be removed from the control, but in actuality it is still hanging around in our Region.  This is because the close button has no idea that we are using Prism, or that we need to remove the view we just closed form the Region.

Solution

So how do we fix this?  Simple… we need a custom behavior.  This one right here:

public class TabItemRemoveBehavior : Behavior<XamTabControl>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.AddHandler(TabItemEx.ClosingEvent, new RoutedEventHandler(TabItem_Closing));
        AssociatedObject.AddHandler(TabItemEx.ClosedEvent, new RoutedEventHandler(TabItem_Closed));
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.RemoveHandler(TabItemEx.ClosingEvent, new RoutedEventHandler(TabItem_Closing));
        AssociatedObject.RemoveHandler(TabItemEx.ClosedEvent, new RoutedEventHandler(TabItem_Closed));
    }

    void TabItem_Closing(object sender, RoutedEventArgs e)
    {
        IRegion region = RegionManager.GetObservableRegion(AssociatedObject).Value;
        if (region == null)
            return;

        var args = (TabClosingEventArgs)e;

        args.Cancel = !CanRemoveItem(GetItemFromTabItem(args.OriginalSource), region);
    }

    void TabItem_Closed(object sender, RoutedEventArgs e)
    {
        IRegion region = RegionManager.GetObservableRegion(AssociatedObject).Value;
        if (region == null)
            return;

        RemoveItemFromRegion(GetItemFromTabItem(e.OriginalSource), region);
    }

    object GetItemFromTabItem(object source)
    {
        var tabItem = source as TabItemEx;
        if (tabItem == null)
            return null;

        return tabItem.Content;
    }

    bool CanRemoveItem(object item, IRegion region)
    {
        bool canRemove = true;

        var context = new NavigationContext(region.NavigationService, null);

        var confirmRequestItem = item as IConfirmNavigationRequest;
        if (confirmRequestItem != null)
        {
            confirmRequestItem.ConfirmNavigationRequest(context, result =>
            {
                canRemove = result;
            });
        }

        FrameworkElement frameworkElement = item as FrameworkElement;
        if (frameworkElement != null && canRemove)
        {
            IConfirmNavigationRequest confirmRequestDataContext = frameworkElement.DataContext as IConfirmNavigationRequest;
            if (confirmRequestDataContext != null)
            {
                confirmRequestDataContext.ConfirmNavigationRequest(context, result =>
                {
                    canRemove = result;
                });
            }
        }

        return canRemove;
    }

    void RemoveItemFromRegion(object item, IRegion region)
    {
        var context = new NavigationContext(region.NavigationService, null);

        InvokeOnNavigatedFrom(item, context);

        region.Remove(item);
    }

    void InvokeOnNavigatedFrom(object item, NavigationContext navigationContext)
    {
        var navigationAwareItem = item as INavigationAware;
        if (navigationAwareItem != null)
        {
            navigationAwareItem.OnNavigatedFrom(navigationContext);
        }

        FrameworkElement frameworkElement = item as FrameworkElement;
        if (frameworkElement != null)
        {
            INavigationAware navigationAwareDataContext = frameworkElement.DataContext as INavigationAware;
            if (navigationAwareDataContext != null)
            {
                navigationAwareDataContext.OnNavigatedFrom(navigationContext);
            }
        }
    }
}

Now simply apply the Behavior to our xamTabControl by using System.Windows.Interactivity.  Like so:

<igWPF:XamTabControl Grid.Row="1" prism:RegionManager.RegionName="TabRegion" TabItemCloseButtonVisibility="Visible" >
    <i:Interaction.Behaviors>
        <core:TabItemRemoveBehavior />
    </i:Interaction.Behaviors>
</igWPF:XamTabControl>

What this Behavior does is hooks into the TabItemEx.Closing and TabItemEx.Closed events so that we can execute our Prism logic during the tab item closing process.  First, we need to respect whether we can even close the tab.  This is done by checking the IConfirmNavigationRequest interface on the view we are trying to remove in the Closing event.  If our View or ViewModel returns false during the ConfirmNavigationRequest method call, then the tab item cannot be closed.  Otherwise, we will allow the tab item to be closed.  For example; let’s say I had some rule that wouldn’t allow ViewB to be closed.  I simply pass “false” in the continuationCallback within the ConfirmNavigationRequest method, and prompt the user letting them know why this tab couldn’t be closed.

image

This bring us to the TabItemEx.Closed event.  Now that we know if we can or cannot close a tab, we simply get the view from the tab item, and then remove it from the region.  But, before we remove it, we want to invoke the NavigatedFrom method on the INavigationAware interface so that we can respond to when a tab is closed from within our View or ViewModel to do any last minute clean up or state management.

That’s it!  You have now added complete Prism Navigation support for closing tab items when using the xamTabControl as a Prism region.  Hopefully you will find this useful, and maybe even use this in your WPF Prism applications.  Be sure to check out the source code, and start playing with it.  As always, feel free contact me on my blog, connect with me on Twitter (@brianlagunas), or leave a comment below for any questions or comments you may have.

Brian Lagunas

View all posts

6 comments

  • Setting a theme on the XamTabControl breaks the Header Binding of TabItemEx

    • Of course, you are overriding an implicit style with a locally scoped style when you set the Theme property. You would need to define your style locally to the control, or set it directly on the TabItmEx as they are injected.

  • Thanks for this, helped me understand behaviors better. One question though, in your AddHandler, you are adding a new RoutedEventHandler. In your RemoveHandler, you are removing a new RoutedEventHandler – since you are newing it, won’t that be a completely different reference than the one you registered initially? How will it be able to unsubscribe it?

Follow Me

Follow me on Twitter, subscribe to my YouTube channel, and watch me stream live on Twitch.