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" buttonto 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"> |
<ControlTemplate TargetType="{x:Type Button}"> |
<Border x:Name="ButtonBorder" |
Background="{StaticResource TabItemCloseButtonNormalBackgroundBrush}" |
BorderBrush="{StaticResource TabItemCloseButtonNormalBorderBrush}"> |
<Path x:Name="ButtonPath" |
Data="{StaticResource X_CloseButton}" |
Stroke="{StaticResource TabItemCloseButtonNormalForegroundBrush}" |
StrokeStartLineCap="Round" |
VerticalAlignment="Center" |
HorizontalAlignment="Center"/> |
<ContentPresenter HorizontalAlignment="Center" |
VerticalAlignment="Center"/> |
<ControlTemplate.Triggers> |
<Trigger Property="IsMouseOver" Value="True"> |
<Setter TargetName="ButtonBorder" |
TabItemCloseButtonHoverBackgroundBrush}" /> |
<Setter TargetName="ButtonPath" |
TabItemCloseButtonHoverForegroundBrush}"/> |
<Trigger Property="IsEnabled" Value="false"> |
<Setter Property="Visibility" Value="Collapsed"/> |
<Trigger Property="IsPressed" Value="true"> |
<Setter TargetName="ButtonBorder" |
TabItemCloseButtonPressedBackgroundBrush}" /> |
<Setter TargetName="ButtonBorder" |
TabItemCloseButtonPressedBorderBrush}" /> |
<Setter TargetName="ButtonPath" Property="Stroke" |
TabItemCloseButtonPressedForegroundBrush}"/> |
<Setter TargetName="ButtonPath" |
Property="Margin" Value="2.5,2.5,1.5,1.5" /> |
</ControlTemplate.Triggers> |
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:
Background="{StaticResource TabItem_BackgroundBrush_Unselected}" |
BorderBrush="{StaticResource TabItem_BorderBrush_Selected}" |
Margin="{StaticResource TabItemMargin_Base}" |
BorderThickness="2,1,1,0" |
</Grid.ColumnDefinitions> |
<ContentPresenter x:Name="ContentSite" |
VerticalAlignment="Center" |
HorizontalAlignment="Center" |
RecognizesAccessKey="True"/> |
<Button x:Name="cmdTabItemCloseButton" |
Style="{StaticResource TabItemCloseButtonStyle}" |
Command="{Binding Path=Content.DataContext.CloseCommand}" |
CommandParameter="{Binding |
RelativeSource={RelativeSource FindAncestor, |
AncestorType={x:Type TabItem}}}" |
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.Windows.Input; |
using System.Diagnostics; |
using System.Windows.Controls; |
namespace TabControlStyle |
public partial class TabControl_2_CloseButton : Window |
public TabControl_2_CloseButton() |
#region --- CloseCommand --- |
private Utils.RelayCommand _cmdCloseCommand; |
/// Returns a command that closes a TabItem. |
public ICommand CloseCommand |
if (_cmdCloseCommand == null) |
_cmdCloseCommand = new Utils.RelayCommand( |
param => this.CloseTab_Execute(param), |
param => this.CloseTab_CanExecute(param) |
/// 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; |
/// 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) |
TabItem ti = parm as TabItem; |
if (ti != null && ti != tc.Items[0]) |
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 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)