WPF: TabControl series - Part 4: Closeable TabItems

by Olaf Rabbachin 15. February 2010 19:10

Introduction

In the last part of the series, I'd like to present one last extension to the TabControl's TabItems - a close-button.

 

Overview

This is the last article in the multi-part series about the WPF TabControl.

Here's the four parts of the series:

 

Outcome: the result of what's covered in this article

Here's what we'll be left with at the end of this article (click to enlarge):

If you downloaded the sample solution (see the bottom for the link), click the "2. TabItem Close Button" button to show the above window.


Status quo (after Part Three)

As noted before, this article is based upon the stuff I introduced in the other parts, hence I'll simply assume that you read and understood what has been discussed there. Please see the other parts in case you find that I am assuming something you don't see discussed here.

Here's where we'll start in this part, that is, what the TabControl and its "sub-controls" looked like at the end of Part Three:

If you downloaded the sample solution (see the bottom for the link), click the "1. Base-style (ScrollableTabPanel)" button to show the above window.

 

Why closing TabItems

Actually, I saw the need for an easy way to close (read "remove") tab last year when I was working on a project in which we decided to have a "sort of" MDI application. That is, windows aren't windows but rather UserControls which are then loaded as a TabPage into a TabControl, much like what you see in recent versions of browsers like Firefox and IE (funny enough - in German, pronouncing "IE" could be translated as "yuck"; SCNR).

In the end, what I came up with is what I think is a very versatile approach as it is open to both a code-behind approach or MVVM (which is what was used in the aforementioned project), also allowing for a TabItem-related determination about whether it should even be allowed to close a TabItem or not. More on that later, let's first focus on how we deal with ...

 

Extending the TabItem-style

As for the style of the Button control that is to be rendered in TabItems, there isn't really anything special. Here's the style/template of the button:

<Style x:Key="TabItemCloseButtonStyle" TargetType="{x:Type Button}">
   <Setter Property="SnapsToDevicePixels" Value="false"/>
   <Setter Property="Height" Value="{StaticResource CloseButtonWidthAndHeight}"/>
   <Setter Property="Width" Value="{StaticResource CloseButtonWidthAndHeight}"/>
   <Setter Property="Cursor" Value="Hand"/>
   <Setter Property="Focusable" Value="False"/>
   <Setter Property="OverridesDefaultStyle" Value="true"/>
   <Setter Property="Template">
      <Setter.Value>
         <ControlTemplate TargetType="{x:Type Button}">
            <Border x:Name="ButtonBorder"  
                          CornerRadius="2" 
                          BorderThickness="1"
                          Background="{StaticResource TabItemCloseButtonNormalBackgroundBrush}"
                          BorderBrush="{StaticResource TabItemCloseButtonNormalBorderBrush}">
               <Grid>
                  <!-- The Path below will render the button's X. -->
                  <Path x:Name="ButtonPath" 
                              Margin="2"
                              Data="{StaticResource X_CloseButton}"
                              Stroke="{StaticResource TabItemCloseButtonNormalForegroundBrush}" 
                              StrokeThickness="2"
                              StrokeStartLineCap="Round"
                              StrokeEndLineCap="Round"
                              Stretch="Uniform"
                              VerticalAlignment="Center"
                              HorizontalAlignment="Center"/>
                  <!-- We don't really need the ContentPresenter, but what the heck ... -->
                  <ContentPresenter HorizontalAlignment="Center"
                                          VerticalAlignment="Center"/>
               </Grid>
            </Border>
            <ControlTemplate.Triggers>
               <Trigger Property="IsMouseOver" Value="True">
                  <Setter TargetName="ButtonBorder" 
                          Property="Background" 
                          Value="{StaticResource 
                             TabItemCloseButtonHoverBackgroundBrush}" />
                  <Setter TargetName="ButtonPath" 
                          Property="Stroke"
                          Value="{StaticResource 
                             TabItemCloseButtonHoverForegroundBrush}"/>
               </Trigger>
               <Trigger Property="IsEnabled" Value="false">
                  <Setter Property="Visibility" Value="Collapsed"/>
               </Trigger>
               <Trigger Property="IsPressed" Value="true">
                  <Setter TargetName="ButtonBorder" 
                                Property="Background" 
                                Value="{StaticResource 
                                   TabItemCloseButtonPressedBackgroundBrush}" />
                  <Setter TargetName="ButtonBorder" 
                                Property="BorderBrush" 
                                Value="{StaticResource 
                                   TabItemCloseButtonPressedBorderBrush}" />
                  <Setter TargetName="ButtonPath" Property="Stroke" 
                                Value="{StaticResource 
                                   TabItemCloseButtonPressedForegroundBrush}"/>
                  <Setter TargetName="ButtonPath" 
                          Property="Margin" Value="2.5,2.5,1.5,1.5" />
               </Trigger>
            </ControlTemplate.Triggers>
         </ControlTemplate>
      </Setter.Value>
   </Setter>
</Style>

There's only few pieces worth noting:

  • The style includes a Trigger that is applied when the button is down/pressed; here, a Margin shifts the button's content down and to the right. To only have a slight change (a shift by 1px would be to drastic), the Margin is incremented by 0.5 for top/left and decremented by 0.5 for bottom/right. To make this work, SnapToDevicePixels is explicitly set to False in the style. While this setter isn't required (SnapToDevicePixels is False by default), I prefer to explicitly point this out.
  • The button's "image" is, again (see the previous parts), a Path. In this case, it's really only two lines plus round start-/end-caps (the latter being defined in the Path, of course):
    <Geometry x:Key="X_CloseButton">M0,0 L10,10 M0,10 L10,0</Geometry>

To actually integrate this button into the TabItem's ControlTemplate only takes a few minor changes. Here's the part of the template that contains the amendments:

<Border Name="Border"
        Background="{StaticResource TabItem_BackgroundBrush_Unselected}"
        BorderBrush="{StaticResource TabItem_BorderBrush_Selected}" 
        Margin="{StaticResource TabItemMargin_Base}"
        BorderThickness="2,1,1,0" 
        CornerRadius="3,3,0,0">
   <Grid>
      <Grid.ColumnDefinitions>
         <!-- Text / TabItem's Caption -->
         <ColumnDefinition/>
         <!-- Close button -->
         <ColumnDefinition/>
      </Grid.ColumnDefinitions>
      <!-- This is where the Content of the TabItem will be rendered. -->
      <ContentPresenter x:Name="ContentSite"
                        VerticalAlignment="Center"
                        HorizontalAlignment="Center"
                        ContentSource="Header"
                        Margin="7,2,12,2"
                        RecognizesAccessKey="True"/>
      <Button x:Name="cmdTabItemCloseButton"
              Style="{StaticResource TabItemCloseButtonStyle}"
              Command="{Binding Path=Content.DataContext.CloseCommand}"
              CommandParameter="{Binding  
              RelativeSource={RelativeSource FindAncestor, 
              AncestorType={x:Type TabItem}}}"
              Grid.Column="1"
              Margin="-7,5,7,5"/>
   </Grid>
</Border>

So, all we really do in the above is to add another ColumnDefinition to the (already existing) Grid control, placing the button into the second column. This doesn't influence i.e. the TabItemMenu, where the textual content is shown, as that is refering to the ContentPresenter's content. The only thing noteable here is the definition of the button's margin, which sort of "replaces" the right margin of the TabItem by applying a negative left margin - this helps to consider the fact that the button may not be visible at all times, in which case the TabItem's Margin should remain as it was before we added the button.

Now, showing a button wasn't much work, but we're talking about a ControlTemplate for the TabControl, so how can we react to a button-click ..?

 

Enter ICommand

The close-button itself is not worth much if there isn't an easy, versatile and independent way to react to clicks. The "WPF-way" of dealing this is, of course, to use the ICommand interface. For the sake of simplicity and since there's already plenty of tutorials on the web, I won't dig into the specifics of ICommand here. Instead, I've taken over the RelayCommand class, an approach that Josh Smith's introduced in his article on MVVM, published in the MSDN magazine and dropped it into the code behind of the TabControl window. The fact that there's now code-behind in the window is actually neglectable - it could as well be part of a ViewModel instead. This is because, if you look at the binding in the XAML above, the Command associated with the Button control is targetting the DataContext rather than any code-behind, and it also passes a reference to the parent TabItem to the Command - this is all we need in order to utilize the command. Here's the complete code-behind of the window (for the VB-version, please refer to the sample solution - see the bottom of this article for the download link):

using System;
using System.Windows;
using System.Windows.Input;
using System.Diagnostics;
using System.Windows.Controls;

namespace TabControlStyle
{
   public partial class TabControl_2_CloseButton : Window
   {
      /// 
      /// C'tor
      /// 
      public TabControl_2_CloseButton()
      {
         InitializeComponent();
         //For the sample, the Window's DataContext is its code-behind.
         this.DataContext = this;
      }

      #region --- CloseCommand ---

      private Utils.RelayCommand _cmdCloseCommand;
      /// 
      /// Returns a command that closes a TabItem.
      /// 
      public ICommand CloseCommand
      {
         get
         {
            if (_cmdCloseCommand == null)
            {
               _cmdCloseCommand = new Utils.RelayCommand(
                   param => this.CloseTab_Execute(param),
                   param => this.CloseTab_CanExecute(param)
                   );
            }
            return _cmdCloseCommand;
         }
      }

      /// 
      /// Called when the command is to be executed.
      /// 
      /// 
      /// The TabItem in which the Close-button was clicked.
      /// 
      private void CloseTab_Execute(object parm)
      {
         TabItem ti = parm as TabItem;
         if (ti != null)
            tc.Items.Remove(parm);
      }

      /// 
      /// Called when the availability of the Close command needs to be determined.
      /// 
      /// 
      /// The TabItem for which to determine the availability of the Close-command.
      /// 
      private bool CloseTab_CanExecute(object parm)
      {
         //For the sample, the closing of TabItems will only be
         //unavailable for disabled TabItems and the very first TabItem.
         TabItem ti = parm as TabItem;
         if (ti != null && ti != tc.Items[0])
            //We have a valid reference to a TabItem, so return 
            //true if the TabItem is enabled.
            return ti.IsEnabled;

         //If no reference to a TabItem could be obtained, the command 
         //cannot be executed
         return false;
      }

      #endregion

   }
}

For the sample, the only two conditions that would prevent the Command from being executed (resulting in a disabled button) refer to disabled TabItems and the very first one (this way there'll always be at least one enabled item).

Note that, if you were using the MVVM pattern, the CloseCommand-region would be part of the ViewModel and the window's DataContext (see the constructor) would rather point to that ViewModel.

 

The last word

This concludes the last part of the TabControl series. This series has been way longer than I originally thought, but I had enough fun with it to play around with a couple of things that weren't part of the original plan ... Smile

As always, comments are appreciated. Let me know if you have any questions or suggestions for improving the control.

 

The sample solution

I’ve created a sample solution that contains everything discussed here, containing one project for each the C# and the VB versions.

Download: TabControlStyle - Part Four.zip (76.83 kb)

Tags: , , , , , , , , , ,

TabControl | WPF (.net)

Comments


France Benoît H. 
June 21. 2010 10:57
Benoît H.
Hi Olaf,

Great job with this and thanks for sharing.

I have a problem when I try to apply this to my application.
It's working correctly with the TabItems already added to the TabControl in the xaml, but not with the TabItems I'm dynamically adding to it (with a Hyperlink click system).

Here's how I proceed:

TabItem tabtest = new TabItem();
tabControl1.Items.Add(tabtest);


I'm pretty sure I'm missing something but I'm really a beginner so sorry if that's stupid.

Thanks
Benoît


June 21. 2010 11:19
Olaf Rabbachin
Salut Benoît,

actually what you have should be working "as is", the only side effect being that you don't see any header - simply because you don't set it. Try ...
   TabItem tabtest = new TabItem() { Header = "Test" };
   tc.Items.Add(tabtest);
... to set it. If you i.e. add a button control to the TC of the article's window and paste the above into its click-event, you will see the dynamically added TabItem appear both in the TabPanel as well as the menu (last item).

À bientôt et bon courage,
Olaf


France Benoît H. 
June 21. 2010 11:26
Benoît H.
Thanks for replying so fast Olaf Smile

In fact I didn't explain my problem very clearly.
I indeed set the header of my generated TabItem as you suggest, here's the whole thing:

            TabItem tabtest = new TabItem();
            Hyperlink h = (Hyperlink)sender;
            CasUtilisation c = (CasUtilisation)h.DataContext;
            tabtest.DataContext = c;
            tabtest.Header = c.Libelle;


What I meant by "not working" was about the closeable behavior. The TabItems already added in xaml are closing fine, but not the dynamically added ones. (I imagine I have to attach them the command part but I don't figure out how)

Thanks
Ben


June 21. 2010 11:39
Olaf Rabbachin
Hi Ben,

the closing should be working equally well - at least it does with the sample with the code I showed in my last post above. So I can only guess that you'd need to do some additional clean up in your CasUtilisation class resp. something in this class prevents the tab from being GC'ed, that is, the removal fails. I'd set a break point into the TC's CloseTab_Execute handler and check what happens.

Cheers,
Olaf


France Benoît H. 
June 21. 2010 14:01
Benoît H.
Hi again,

I had a look but I can't find what's wrong for now.

The general structure of the app is an outlook style TabControl menu on the left (like this www.codeproject.com/KB/WPF/XAML_OutlookBar.aspx ) containing hyperlinks, which create TabItems in a second (but basic) TabControl on the main part of the app.
As I'm very new in WPF, it's pretty hard for me to debug, but I'll keep searching.

Thanks again for your help and availability Olaf

Ben


June 21. 2010 14:50
Olaf Rabbachin
Hi Ben,

if you're really only after the closeable tabs, my tutorial - with all the fuzz - might be the wrong choice. I don't have any link at hand that would give you an easier intro to closeable tabs, but I'm sure you'll find bunches of them on the web, meaning intros that only cover the how-to for this specific task.
I know there is closeable tabs in Josh Smith's MVVM-intro, but, again, it's only a secondary area of interest there. In case you still want to peek at the article he wrote for the MSDN mag, here's the link: msdn.microsoft.com/en-us/magazine/dd419663.aspx

Alternatively, if you upload what you have unto this point and post the link here (or e-mail me), I'll take a look, but that might take until later this or even next week (go through the contact-page for my e-mail address), that is, until I find the time (way too limited at present).

Cheers,
Olaf


France Benoît H. 
June 21. 2010 17:02
Benoît H.
Hi Olaf,

I think your tutorial is very convenient with my needs, but it's just that I must be having some details that are messing up the closing function but I'll try to figure that out.

That's funny, at the exact same time you linked Josh Smith's MVVM tutorial I was reading it to know more on MVVMs and indeed I noticed the closeable tabs on his demo application. In fact my app has the exact same system of links opening closeable tabs so I was pretty angry because I could have been looking how he had done to help myself for the last weeks o_o

Anyway I'll dig this and carry on.
I'm not gonna go and loose your time in my stuff, I'll try to make it myself, but I appreciate a lot your help and advices ;)

Ben


United States Jerome 
July 16. 2010 18:49
Jerome
Hi Olaf,

Thanks a lot for this tutorial.  It is the best digging-into the TabControl out there, so far as I am able to determine.  

A question I have for you is: what could I do to reduce the TabItem's height?  I found where I can edit the TabItemPanel's height, but that just cuts off the top of the TabItems.

Thank you,
Jerome

PS> Your commenting system doesn't seem to work in Chrome


United States Jerome 
July 16. 2010 18:52
Jerome
Olaf,

I apologize for wasting your time--I found it right after I made the comment.  Think before you type, eh?

Thanks again,
-Jerome


July 16. 2010 19:00
Olaf Rabbachin
Hi Jerome,

glad you got it yourself - was just about to dig into things here. Smile

As for the commenting system, I know there's a problem which comes from the Captcha plug-in I'm using. Haven't had the time to fix it yet (its code is from somebody else). However, the Captcha is (besides the Aksiment comment checker) the only means to at least dumb most of those dumb spammers' comments ... :-(

FWIW - it works if you click the captcha (to create a new code/image) first, enter the code and then enter the comment (not that I'd think this would help much for those that'd like to post a comment ...).

Cheers,
Olaf


Australia Mark F 
July 18. 2010 13:23
Mark F
Heya Olaf,

thanks so much for writing this series, you saved me hours of work and created a control that did exactly what I was after. It also answered quite a few WPF questions I've had along the way, notably how you do command notification in templated controls (I'm very new to this stuff).

The only small problem I did have was when I added it to a custom control, for some reason the drop-down menu had a 1-pixel black border around it (only that button, not the scroll left/right buttons). Your xaml is very easy to understand though so it only took me a minute fix it by adding BorderBrush="Transparent" to the <Menu> tag.

Thanks again for all your hard work and for making this code public.


South Africa Dean 
July 18. 2010 19:25
Dean
Hi Olaf
This is great thanks
One small thing, when nesting tab controls the navigation icons dont display in the Parent tab control and all of the tabs in the Child tab have "Close" crosses.
Great work none the less
Tx,
Dean


France Benoît H. 
July 21. 2010 14:31
Benoît H.
Hi again Olaf,

Still using your nice piece of work for my project, and I managed to get out of the little problems I had (the ones I posted about earlier).

I'm encountering the same problem Dean gets, in a UserControl nesting 2 of your tabcontrols, and looking for a solution too.

Thanks again
Ben

Add comment


(Will show your Gravatar icon)

  Country flag

Click to change captcha

biuquote
  • Comment
  • Preview
Loading



About

Hi and welcome to my blog!

I'm a developer from Germany, currently focusing on .Net and WPF.

More about me ...