C.B.R. – How to make Visual Studio like tabs

In advance to the release of CBR version 0.7, we will have a look on how to make a single row tab control for multi-document display. CBR is having a home/start page, can display comics or epub books and also edit them.

First of all, a big tip for the developer who want to extend any WPF control : Use Blend and the “Edit Template” function. This will extract an easy and readable Xaml from any existing control.

PS : Sorry for the xaml formatting but it’s a pain to get something working not bad… 🙂

Create a TabControl in your project form, and use the Edit Template=>Edit a copy. You will get the next xaml:

<SolidColorBrush x:Key=”TabControlNormalBorderBrush” Color=”#8C8E94″/>
<Style x:Key=”TabControlStyle1″ TargetType=”{x:Type TabControl}”>
<Setter Property=”Foreground” Value=”{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}”/>
<Setter Property=”Padding” Value=”4,4,4,4″/>
<Setter Property=”BorderThickness” Value=”1″/>
<Setter Property=”BorderBrush” Value=”{StaticResource TabControlNormalBorderBrush}”/>
<Setter Property=”Background” Value=”#F9F9F9″/>
<Setter Property=”HorizontalContentAlignment” Value=”Center”/>
<Setter Property=”VerticalContentAlignment” Value=”Center”/>
<Setter Property=”Template”>
<Setter.Value>
<ControlTemplate TargetType=”{x:Type TabControl}”>
<Grid ClipToBounds=”true” SnapsToDevicePixels=”true” KeyboardNavigation.TabNavigation=”Local”>
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name=”ColumnDefinition0″/>
<ColumnDefinition x:Name=”ColumnDefinition1″ Width=”0″/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition x:Name=”RowDefinition0″ Height=”Auto”/>
<RowDefinition x:Name=”RowDefinition1″ Height=”*”/>
</Grid.RowDefinitions>
<TabPanel x:Name=”HeaderPanel” Grid.Column=”0″ IsItemsHost=”true” Margin=”2,2,2,0″ Grid.Row=”0″ KeyboardNavigation.TabIndex=”1″ Panel.ZIndex=”1″/>
<Border x:Name=”ContentPanel” BorderBrush=”{TemplateBinding BorderBrush}” BorderThickness=”{TemplateBinding BorderThickness}” Background=”{TemplateBinding Background}” Grid.Column=”0″ KeyboardNavigation.DirectionalNavigation=”Contained” Grid.Row=”1″ KeyboardNavigation.TabIndex=”2″ KeyboardNavigation.TabNavigation=”Local”>
<ContentPresenter x:Name=”PART_SelectedContentHost” ContentSource=”SelectedContent” Margin=”{TemplateBinding Padding}” SnapsToDevicePixels=”{TemplateBinding SnapsToDevicePixels}”/>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property=”TabStripPlacement” Value=”Bottom”>
<Setter Property=”Grid.Row” TargetName=”HeaderPanel” Value=”1″/>
<Setter Property=”Grid.Row” TargetName=”ContentPanel” Value=”0″/>
<Setter Property=”Height” TargetName=”RowDefinition0″ Value=”*”/>
<Setter Property=”Height” TargetName=”RowDefinition1″ Value=”Auto”/>
<Setter Property=”Margin” TargetName=”HeaderPanel” Value=”2,0,2,2″/>
</Trigger>
<Trigger Property=”TabStripPlacement” Value=”Left”>
<Setter Property=”Grid.Row” TargetName=”HeaderPanel” Value=”0″/>
<Setter Property=”Grid.Row” TargetName=”ContentPanel” Value=”0″/>
<Setter Property=”Grid.Column” TargetName=”HeaderPanel” Value=”0″/>
<Setter Property=”Grid.Column” TargetName=”ContentPanel” Value=”1″/>
<Setter Property=”Width” TargetName=”ColumnDefinition0″ Value=”Auto”/>
<Setter Property=”Width” TargetName=”ColumnDefinition1″ Value=”*”/>
<Setter Property=”Height” TargetName=”RowDefinition0″ Value=”*”/>
<Setter Property=”Height” TargetName=”RowDefinition1″ Value=”0″/>
<Setter Property=”Margin” TargetName=”HeaderPanel” Value=”2,2,0,2″/>
</Trigger>
<Trigger Property=”TabStripPlacement” Value=”Right”>
<Setter Property=”Grid.Row” TargetName=”HeaderPanel” Value=”0″/>
<Setter Property=”Grid.Row” TargetName=”ContentPanel” Value=”0″/>
<Setter Property=”Grid.Column” TargetName=”HeaderPanel” Value=”1″/>
<Setter Property=”Grid.Column” TargetName=”ContentPanel” Value=”0″/>
<Setter Property=”Width” TargetName=”ColumnDefinition0″ Value=”*”/>
<Setter Property=”Width” TargetName=”ColumnDefinition1″ Value=”Auto”/>
<Setter Property=”Height” TargetName=”RowDefinition0″ Value=”*”/>
<Setter Property=”Height” TargetName=”RowDefinition1″ Value=”0″/>
<Setter Property=”Margin” TargetName=”HeaderPanel” Value=”0,2,2,2″/>
</Trigger>
<Trigger Property=”IsEnabled” Value=”false”>
<Setter Property=”Foreground” Value=”{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}”/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

First of all we can get ride off the alignment we don’t need…remove the triggers. Then surround the TabPanel with a ScrollViewer like below :

<ScrollViewer VerticalScrollBarVisibility=”Disabled” HorizontalScrollBarVisibility=”Auto” Margin=”0″>
<TabPanel x:Name=”HeaderPanel” Grid.Column=”0″ IsItemsHost=”true” Margin=”2,2,2,0″ Grid.Row=”0″ KeyboardNavigation.TabIndex=”1″ Panel.ZIndex=”1″/>
</ScrollViewer>

Then, once more edit the ScroolViewer template to get the next template : here we can remove the both scroolbars, modify the columns definition and add a right aligned button to display the tab items in a popup menu:

<ControlTemplate x:Key=”ScrollViewerControlTemplate1″ TargetType=”{x:Type ScrollViewer}”>
<Grid x:Name=”Grid” Background=”{TemplateBinding Background}”>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=”*”/>
<ColumnDefinition Width=”Auto”/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height=”*”/>
<RowDefinition Height=”Auto”/>
</Grid.RowDefinitions>
<Rectangle x:Name=”Corner” Grid.Column=”1″ Fill=”{DynamicResource {x:Static SystemColors.ControlBrushKey}}” Grid.Row=”1″/>
<ScrollContentPresenter x:Name=”PART_ScrollContentPresenter” CanContentScroll=”{TemplateBinding CanContentScroll}” CanHorizontallyScroll=”False” CanVerticallyScroll=”False” ContentTemplate=”{TemplateBinding ContentTemplate}” Content=”{TemplateBinding Content}” Grid.Column=”0″ Margin=”{TemplateBinding Padding}” Grid.Row=”0″/>
<ScrollBar x:Name=”PART_VerticalScrollBar” AutomationProperties.AutomationId=”VerticalScrollBar” Cursor=”Arrow” Grid.Column=”1″ Maximum=”{TemplateBinding ScrollableHeight}” Minimum=”0″ Grid.Row=”0″ Visibility=”{TemplateBinding ComputedVerticalScrollBarVisibility}” Value=”{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}” ViewportSize=”{TemplateBinding ViewportHeight}”/>
<ScrollBar x:Name=”PART_HorizontalScrollBar” AutomationProperties.AutomationId=”HorizontalScrollBar” Cursor=”Arrow” Grid.Column=”0″ Maximum=”{TemplateBinding ScrollableWidth}” Minimum=”0″ Orientation=”Horizontal” Grid.Row=”1″ Visibility=”{TemplateBinding ComputedHorizontalScrollBarVisibility}” Value=”{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}” ViewportSize=”{TemplateBinding ViewportWidth}”/>
</Grid>
</ControlTemplate>

We must finish with the following template. Note that I am using Fluent:DropDownButton because they provide a good behaviour with popup menus.

<ControlTemplate x:Key=”ScrollViewerControlTemplate1″ TargetType=”{x:Type ScrollViewer}”>
<Grid x:Name=”Grid” Background=”{TemplateBinding Background}”>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=”*”/>
<ColumnDefinition Width=”Auto”/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<ScrollContentPresenter x:Name=”PART_ScrollContentPresenter” CanContentScroll=”{TemplateBinding CanContentScroll}” CanHorizontallyScroll=”False” CanVerticallyScroll=”False” ContentTemplate=”{TemplateBinding ContentTemplate}” Content=”{TemplateBinding Content}” Grid.Column=”0″ Margin=”0″ Grid.Row=”0″/>
<Fluent:DropDownButton Grid.Column=”1″ ToolTip=”Group by” SizeDefinition=”Small” Icon=”/CBR;Component/Resources/Images/16×16/sort_category.png”
ItemsSource=”{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TabControl}}, Path=Items}”
ItemContainerStyle=”{StaticResource TabMenuItem}”
/>
</Grid>
</ControlTemplate>

After that, we need to define a MenuItem template for the shortcut menu and also the TabItem template, because we want them closeable, selectable and they must fire a close event to the view.

The MenuItem template is very simple : Bind the header to the DisplayName property of our ViewModel, make it checkable and bind the IsChecked property to the IsSelected ViewModel property.

<Style x:Key=”TabMenuItem” TargetType=”{x:Type Fluent:MenuItem}”>
<Setter Property=”Header” Value=”{Binding Path=DisplayName}” />
<Setter Property=”IsCheckable” Value=”True” />
<Setter Property=”IsChecked” Value=”{Binding Path=IsSelected, Mode=TwoWay}” />
</Style>

To create the TabItem template (a bit more complicated) do the same way as before. This must be like below. Note the binding of the IsSelected TabItem property to the ViewModel and the command associated with a generic Close command defined in the ViewModelBase that will send a close event for disposing.

<Style x:Key=”VS2010Tabs” TargetType=”{x:Type TabItem}”>
<Setter Property=”IsSelected” Value=”{Binding IsSelected}” />
<Setter Property=”Template”>
<Setter.Value>
<ControlTemplate TargetType=”{x:Type TabItem}”>
<Grid>
<Border x:Name=”Border” CornerRadius=”4,4,0,0″ >
<Grid Margin=”5,2,5,2″>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=”*”/>
<ColumnDefinition Width=”18″/>
</Grid.ColumnDefinitions>
<ContentPresenter Content=”{Binding DisplayName}” VerticalAlignment=”Center” Margin=”1,1,5,1″/>
<Button x:Name=”CloseBtn” Grid.Column=”1″
Style=”{DynamicResource CBRTabButtonCloseStyle}”
Command=”{Binding CloseCommand}”
Content=”X” Cursor=”Hand”
Focusable=”False”
FontFamily=”Courier” FontSize=”9″ FontWeight=”Bold” 
Margin=”0″ Padding=”0″
VerticalContentAlignment=”Center”
HorizontalContentAlignment=”Center”
Width=”16″ Height=”16″
HorizontalAlignment=”Center”
VerticalAlignment=”Center” Opacity=”0″>
</Button>
</Grid>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property=”IsSelected” Value=”True”>
<Setter TargetName=”Border” Property=”Background” Value=”{DynamicResource ButtonHoverInnerBackgroundBrush}” />
<Setter TargetName=”CloseBtn” Property=”Opacity” Value=”1″ />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property=”IsMouseOver” Value=”True”/>
<Condition Property=”IsSelected” Value=”False”/>
</MultiTrigger.Conditions>
<Setter TargetName=”Border” Property=”Background”>
<Setter.Value>
<SolidColorBrush Color=”LightGray” Opacity=”0.5″/>
</Setter.Value>
</Setter>
<Setter TargetName=”CloseBtn” Property=”Opacity” Value=”1″ />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

The class MultiViewTabControl has got no code associated with this functionnality, but with the fact that TabControl is destroying the view in the MVVM pattern if view class are not the same. You can find the complete code on my coplex project.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s