Press "Enter" to skip to content

Common Libraries – Un TabItem Chiudibile – Modificare un Template WPF

Un post che riporta ed estende un tutorial di csharpcorner e dimostra come modificare un controllo standard microsoft (il TabItem del TabControl) in modo da creare un interfaccia simile all’editor di visual studio, da utilizzare come bozza applicativa per le nostre interfacce WPF.

Quando abbiamo iniziato a pensare a come realizzare la Console per il Servizio Web che stiamo costruendo come tutorial nei post del nostro blog, la prima cosa che ci è venuta in mente è Visual Studio, ma anche le MDI di windows forms che siamo abituati ad utilizzare per le nostre applicazioni. Il primo shock in merito è che non esiste il flag IsMdiContainer sulle window WPF, ma passato quello, la speranza era, allora ci sarà un Tab Control che ci permetta di simulare quelli di Visual Studio. Sfortunatamente il tab control e i suoi tab item standard sono esattamente identici a quelli di windows forms, hanno solo un titolo.

Così ovviamente visto che  in Internet c’è sempre tutto, abbiamo iniziato una ricerca per capire se vi fosse qualcosa magari su codeplex per ottenere quanto necessario, in effetti ci sono progetti sia su Codeplex che su CordeProject che mostrano come implementare qualcosa del genere, i link sono i seguenti: AvalonDock su Codeplex un componente molto avanzato che imita quello che si può fare in Visual Studio, e Tabbed MDI in WPF più tutorial style e semplice. Per i nostri scopi ci  bastava qualcosa di semplice, continuando nella ricerca abbiamo trovato questo articolo su csharpcorner Closeable Tab Control che è quello che ci serviva ed è uno spunto per imparare a modificare i Template dei controlli standard di WPF, pertanto abbiamo provato a replicare quanto era spiegato creando un progetto di test Dnw Style in cui proveremo a spiegare un po’più in dettaglio come si fa a modificare in modo significativo la forma ed il comportamento di un controllo standard WPF per i nostri scopi.

La prima cosa da fare per modificare il template di un controllo WPF è accedere ad una sua copia, visto che ricrearlo da zero potrebbe essere molto complicato, soprattutto per controlli più avanzati di un TextBlock. Vediamo come Visual Studio 2012 ci aiuta in merito:

make_template_copy_01

Per creare una copia del template di qualsiasi controllo WPF, dalla finestra del designer fare click destro sul controllo, nel nostro caso il primo TabItem del nostro Tab Control, selezionare l’opzione Edit Template e nel sottomenu, Edit a Copy.

make_template_copy_02

Dopo aver dato un nome al nuovo template e selezionato l’opzione relativa al luogo ove porlo, nel file XAML indicato troveremo una copia del template per il TabItem.

Quello che sarà generato sarà la seguente lunga sequenza XAML all’interno di un resource dictionary:

<Style x:Key="TabItemFocusVisual">
    <Setter Property="Control.Template">
        <Setter.Value>
            <ControlTemplate>
                <Rectangle Margin="3,3,3,1" 
                    SnapsToDevicePixels="true" 
                    Stroke="Black" 
                    StrokeThickness="1" 
                    StrokeDashArray="1 2"/>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<SolidColorBrush x:Key="TabControlNormalBorderBrush" Color="#8C8E94"/>
<LinearGradientBrush x:Key="ButtonNormalBackground" EndPoint="0,1" StartPoint="0,0">
    <GradientStop Color="#F3F3F3" Offset="0"/>
    <GradientStop Color="#EBEBEB" Offset="0.5"/>
    <GradientStop Color="#DDDDDD" Offset="0.5"/>
    <GradientStop Color="#CDCDCD" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="TabItemHotBackground" EndPoint="0,1" StartPoint="0,0">
    <GradientStop Color="#EAF6FD" Offset="0.15"/>
    <GradientStop Color="#D9F0FC" Offset=".5"/>
    <GradientStop Color="#BEE6FD" Offset=".5"/>
    <GradientStop Color="#A7D9F5" Offset="1"/>
</LinearGradientBrush>
<SolidColorBrush x:Key="TabItemSelectedBackground" Color="#F9F9F9"/>
<SolidColorBrush x:Key="TabItemHotBorderBrush" Color="#3C7FB1"/>
<SolidColorBrush x:Key="TabItemDisabledBackground" Color="#F4F4F4"/>
<SolidColorBrush x:Key="TabItemDisabledBorderBrush" Color="#FFC9C7BA"/>
<Style x:Key="TabItemStyle1" TargetType="{x:Type TabItem}">
    <Setter Property="FocusVisualStyle" Value="{StaticResource TabItemFocusVisual}"/>
    <Setter Property="Foreground" Value="Black"/>
    <Setter Property="Padding" Value="6,1,6,1"/>
    <Setter Property="BorderBrush" Value="{StaticResource TabControlNormalBorderBrush}"/>
    <Setter Property="Background" Value="{StaticResource ButtonNormalBackground}"/>
    <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
    <Setter Property="VerticalContentAlignment" Value="Stretch"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TabItem}">
                <Grid SnapsToDevicePixels="true">
                    <Border x:Name="Bd" 
                        BorderBrush="{TemplateBinding BorderBrush}" 
                        BorderThickness="1,1,1,0" 
                        Background="{TemplateBinding Background}" 
                        Padding="{TemplateBinding Padding}">
                        <ContentPresenter x:Name="Content" 
                            ContentSource="Header" 
                            HorizontalAlignment="{Binding HorizontalContentAlignment, 
                            RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" 
                            RecognizesAccessKey="True" 
                            SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" 
                            VerticalAlignment="{Binding VerticalContentAlignment, 
                            RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
                    </Border>
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="true">
                        <Setter Property="Background" 
                            TargetName="Bd" 
                            Value="{StaticResource TabItemHotBackground}"/>
                    </Trigger>
                    <Trigger Property="IsSelected" Value="true">
                        <Setter Property="Panel.ZIndex" Value="1"/>
                        <Setter Property="Background" 
                            TargetName="Bd" 
                            Value="{StaticResource TabItemSelectedBackground}"/>
                    </Trigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsSelected" Value="false"/>
                            <Condition Property="IsMouseOver" Value="true"/>
                        </MultiTrigger.Conditions>
                        <Setter Property="BorderBrush" 
                            TargetName="Bd" 
                            Value="{StaticResource TabItemHotBorderBrush}"/>
                    </MultiTrigger>
                    <Trigger Property="TabStripPlacement" Value="Bottom">
                        <Setter Property="BorderThickness" TargetName="Bd" Value="1,0,1,1"/>
                    </Trigger>
                    <Trigger Property="TabStripPlacement" Value="Left">
                        <Setter Property="BorderThickness" TargetName="Bd" Value="1,1,0,1"/>
                    </Trigger>
                    <Trigger Property="TabStripPlacement" Value="Right">
                        <Setter Property="BorderThickness" TargetName="Bd" Value="0,1,1,1"/>
                    </Trigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsSelected" Value="true"/>
                            <Condition Property="TabStripPlacement" Value="Top"/>
                        </MultiTrigger.Conditions>
                        <Setter Property="Margin" Value="-2,-2,-2,-1"/>
                        <Setter Property="Margin" TargetName="Content" Value="0,0,0,1"/>
                    </MultiTrigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsSelected" Value="true"/>
                            <Condition Property="TabStripPlacement" Value="Bottom"/>
                        </MultiTrigger.Conditions>
                        <Setter Property="Margin" Value="-2,-1,-2,-2"/>
                        <Setter Property="Margin" TargetName="Content" Value="0,1,0,0"/>
                    </MultiTrigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsSelected" Value="true"/>
                            <Condition Property="TabStripPlacement" Value="Left"/>
                        </MultiTrigger.Conditions>
                        <Setter Property="Margin" Value="-2,-2,-1,-2"/>
                        <Setter Property="Margin" TargetName="Content" Value="0,0,1,0"/>
                    </MultiTrigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsSelected" Value="true"/>
                            <Condition Property="TabStripPlacement" Value="Right"/>
                        </MultiTrigger.Conditions>
                        <Setter Property="Margin" Value="-1,-2,-2,-2"/>
                        <Setter Property="Margin" TargetName="Content" Value="1,0,0,0"/>
                    </MultiTrigger>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Background" 
                            TargetName="Bd" 
                            Value="{StaticResource TabItemDisabledBackground}"/>
                        <Setter Property="BorderBrush" 
                            TargetName="Bd" Value="{StaticResource 
                            TabItemDisabledBorderBrush}"/>
                        <Setter Property="Foreground" 
                            Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Lo stile che definisce come un TabItem viene rappresentato sul video è complesso ed ha una miriade di trigger che ne modificano la forma e l’aspetto, discuterlo tutto è fuori dall’obiettivo di questo post, in cui invece vogliamo vedere come modificare il template per aggiungere il pulsante di chiusura e la logica per gestirlo.

La soluzione del progetto di test

make_template_solution_01

Nella nostra soluzione oltre alla Window per il test abbiamo creato due cartelle, Components e Themes e all’interno di Themes abbiamo creato un Resource Dictionary (Add new Item – WPF – ResourceDictionary in Visual Studio 2012), che abbiamo chiamato seguendo una convenzione indicata nei pattern di Microsoft, Generic.xaml, in quanto contiene dei template e stili che coinvolgono l’intero progetto.

Nella cartella Components andremo a creare la classe CloseableTabItem, derivata da TabItem che fornirà la logica al nostro nuovo tipo di TabItem.

Il file Original.xaml è stato inserito solo ai fini della stesura di questo articolo, contiene il template non modificato per il TabItem e sarà eliminato nel progetto caricato in area articoli.

Vediamo ora punto per punto cosa va modificato nel template per inserire il close button.

Prima serie di modifiche il Template

La porzione ControlTemplate di un Template è quella che determina gli oggetti grafici che compongono un componente, in questa porzione del codice XAML andremo ad introdurre il button close.

<ControlTemplate TargetType="{x:Type TabItem}">
<Grid SnapsToDevicePixels="true">
    <Border x:Name="Bd" 
        BorderBrush="{TemplateBinding BorderBrush}"  BorderThickness="1,1,1,0"  Background="{TemplateBinding Background}"  Padding="{TemplateBinding Padding}">
        <ContentPresenter x:Name="Content"  ContentSource="Header" 
            HorizontalAlignment="{Binding HorizontalContentAlignment,  RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"  RecognizesAccessKey="True" 
            SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"  VerticalAlignment="{Binding VerticalContentAlignment,  RelativeSource={RelativeSource 
            AncestorType={x:Type ItemsControl}}}"/>
    </Border>
</Grid>

Qui sopra il control template originale.

<ControlTemplate TargetType="{x:Type local:CloseableTabItem}">
    <Grid SnapsToDevicePixels="true">
        <Border x:Name="Bd" 
            Background="{TemplateBinding Background}"  BorderBrush="{TemplateBinding BorderBrush}" 
            BorderThickness="1,1,1,0" >
            <DockPanel x:Name="ContentPanel">
                <Button x:Name="PART_Close"  HorizontalAlignment="Center"  Margin="3,0,3,0" 
                    VerticalAlignment="Center"
                    Width="16" 
                    Height="16" 
                    DockPanel.Dock="Right"  Style="{DynamicResource CloseableTabItemButtonStyle}"  ToolTip="Close Tab">
                        <Path x:Name="Path" 
                            Stretch="Fill" 
                            StrokeThickness="0.5"    
                            Stroke="#FF333333" Fill="#FF969696"    
                            Data="F1 M 2.28484e-007,1.33331L 1.33333,0L 4.00001,2.66669L 6.66667,6.10352e-005L 8,1.33331L 5.33334,4L 8,6.66669L 6.66667,8L 4,5.33331L 1.33333,8L 1.086e-007,6.66669L 2.66667,4L 2.28484e-007,1.33331 Z "    
                            HorizontalAlignment="Stretch" 
                            VerticalAlignment="Stretch"/>
                </Button>
                <ContentPresenter x:Name="Content"  SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"  ContentSource="Header"
                     RecognizesAccessKey="True"  HorizontalAlignment="Center" 
                     VerticalAlignment="Center"  Margin="{TemplateBinding Padding}"/>
            </DockPanel>
        </Border>
    </Grid>

Qui sotto quello modificato, possiamo notare che non si tratta di una modifica banale, infatti è stato aggiunto un contenitore (DockPanel) al cui interno abbiamo inserito un Button ed il ContentPresenter originale del TabItem. All’interno del Button abbiamo inserito un oggetto Path, che fornisce la shape vettoriale della X simbolo di chiusura. Non abbiamo ancora mai visto le Shape nei nostri progetti WPF, sono oggetti vettoriali che permettono di rappresentare qualsiasi tipo di oggetto grafico in WPF, non sono un oggetto semplice, vi sono degli strumenti per produrle, InkScape è uno strumento gratuito molto ben fatto in grado di salvare in XAML. Tornando alle modifiche al template vediamo le cose da tener presenti in questa porzione di codice:

  • TargetType –> abbiamo indicato come classe target per il nostro Template il CloseableTabItem, che sarà la classe derivata del nostro nuovo tab.
  • TemplateBinding –> lo trovate in vari punti del codice Xaml, indica che gli attributi sono collegati ai valori indicati per il template per la parte relativa allo stile riflettendo quindi il background, il borderbrush ecc. indicati nello stile dell’intero template.
  • x:Name –> il content panel ha un nome, perché dovremo usarlo nel code behind e nel template stesso.
  • ToolTip –> per ora è una stringa, quando sposteremo questo nuovo controllo nelle nostre librerie dovremo passare all’uso delle risorse per renderlo multilingua.
  • DynamicResource –> non le abbiamo ancora mai usate, servono per permettere di modificare dinamicamente il valore di un attributo, in questo caso lo stile assegnato al button di chiusura.
  • PART_Close –> la nomenclatura da usare per gli oggetti che compongono un componente della UI ha come pattern il fatto di utilizzare il prefisso PART_ si tratta di una convenzione stabilita da Microsoft, che in questo caso siamo lieti di adottare, vedrete che utilizzeremo il nome del nostro button in altri punti del suo template.
<Trigger Property="IsMouseOver" 
    SourceName="PART_Close" 
    Value="True">
    <Setter Property="Fill" 
        TargetName="Path" 
        Value="#FFB83C3D"/>
</Trigger>
<Trigger Property="IsPressed" 
    SourceName="PART_Close" 
    Value="True">
    <Setter Property="Fill" 
        TargetName="Path" 
        Value="#FF9D3838"/>
</Trigger>

La seconda modifica importante al template è l’aggiunta di questi due Trigger a quelli esistenti per il controllo, le due porzioni di codice qui sopra gestiscono la modifica del Brush di riempimento del Path del nostro Button ad un rosso scuro quando il mouse vi si posiziona sopra o quando il button è premuto. Si tratta quindi di modifiche di stile.

<MultiTrigger>
    <MultiTrigger.Conditions>
        <Condition Property="IsSelected" 
            Value="true"/>
        <Condition Property="TabStripPlacement" 
            Value="Top"/>
    </MultiTrigger.Conditions>
    <Setter Property="Margin" 
        Value="-2,-2,-2,-1"/>
    <Setter Property="Margin" 
        TargetName="Content" 
        Value="0,0,0,1"/>
</MultiTrigger>

Il primo dei quattro trigger per controllare il fatto che un TabItem sia selezionato in forma originale.

<MultiTrigger>
    <MultiTrigger.Conditions>
        <Condition Property="IsSelected" 
            Value="true"/>
        <Condition Property="TabStripPlacement" 
            Value="Top"/>
    </MultiTrigger.Conditions>
    <Setter Property="Margin" 
        Value="-2,-2,-2,-1"/>
    <Setter Property="Margin" 
        TargetName="ContentPanel" 
        Value="0,0,0,1"/>
</MultiTrigger>

La modifica, applicata a tutti e quattro i trigger per agganciare il DockPanel, invece del pannello relativo al ContentPresenter e mostrare la linguetta che lo compone dove indicato.

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

Torniamo all’inizio del file, il namespace originale.

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestTemplateModification.Components">

I namespace del nuovo template, in cui abbiamo aggiunto un riferimento al namespace del CloseableTabItem che abbiamo creato.

<LinearGradientBrush x:Key="ButtonNormalBackground" 
    EndPoint="0,1" 
    StartPoint="0,0">
    <GradientStop 
        Color="#F3F3F3" 
        Offset="0"/>
    <GradientStop 
        Color="#EBEBEB" 
        Offset="0.5"/>
    <GradientStop 
        Color="#DDDDDD" 
        Offset="0.5"/>
    <GradientStop 
        Color="#CDCDCD" 
        Offset="1"/>
</LinearGradientBrush>

Lo stile per il background normale del button viene eliminato e sostituito.

<Style TargetType="{x:Type local:CloseableTabItem}">
<Style.Resources>
    <LinearGradientBrush x:Key="ButtonNormalBackground" 
        EndPoint="0,1" 
        StartPoint="0,0">
        <GradientStop 
            Color="#F3F3F3" 
            Offset="0"/>
        <GradientStop 
            Color="#EBEBEB" 
            Offset="0.5"/>
        <GradientStop 
            Color="#DDDDDD" 
            Offset="0.5"/>
        <GradientStop 
            Color="#CDCDCD" 
            Offset="1"/>
    </LinearGradientBrush>
    <LinearGradientBrush x:Key="ButtonOverBackground" 
         EndPoint="0,1" 
         StartPoint="0,0">
        <GradientStop 
            Color="#FFFAFAFA" 
            Offset="0"/>
        <GradientStop 
            Color="#FFE0E0E3" 
            Offset="1"/>
    </LinearGradientBrush>
    <LinearGradientBrush 
        x:Key="ButtonPressedBackground" 
        EndPoint="0,1" 
        StartPoint="0,0">
        <GradientStop 
            Color="#FFE0E0E2" 
            Offset="0"/>
        <GradientStop 
            Color="#FFF8F8F8" 
            Offset="1"/>
    </LinearGradientBrush>
    <SolidColorBrush 
        x:Key="ButtonNormalBorder" 
        Color="#FF969696"/>
    <Style 
        x:Key="CloseableTabItemButtonStyle" 
        TargetType="{x:Type Button}">
        <Setter 
            Property="FocusVisualStyle" 
            Value="{x:Null}"/>
        <Setter 
            Property="Background" 
            Value="{StaticResource ButtonNormalBackground}"/>
        <Setter 
            Property="BorderBrush" 
            Value="{StaticResource ButtonNormalBorder}"/>
        <Setter 
            Property="BorderThickness" 
            Value="1"/>
        <Setter 
            Property="Foreground" 
            Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
        <Setter 
            Property="HorizontalContentAlignment" 
            Value="Center"/>
        <Setter 
            Property="VerticalContentAlignment" 
            Value="Center"/>
        <Setter 
            Property="Padding" 
            Value="4"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate 
                    TargetType="{x:Type Button}">
                    <Grid>
                        <Border 
                            SnapsToDevicePixels="true"  x:Name="Chrome" 
                            Background="{TemplateBinding Background}"  BorderBrush="{TemplateBinding BorderBrush}"  BorderThickness="{TemplateBinding BorderThickness}"  CornerRadius="2" 
                            Opacity="0" />
                        <ContentPresenter 
                            SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"  HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"  Margin="{TemplateBinding Padding}"  VerticalAlignment="{TemplateBinding VerticalContentAlignment}"  RecognizesAccessKey="True"/>
                    </Grid>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsMouseOver" 
                            Value="True">
                            <Setter Property="Opacity" 
                                TargetName="Chrome" 
                                Value="1"/>
                            <Setter Property="Background" 
                                TargetName="Chrome"  Value="{DynamicResource ButtonOverBackground}" />
                        </Trigger>
                        <Trigger Property="IsPressed" 
                            Value="True">
                            <Setter Property="Opacity" 
                                TargetName="Chrome" 
                                Value="1"/>
                            <Setter Property="Background" 
                                TargetName="Chrome"  Value="{DynamicResource ButtonPressedBackground}" />
                        </Trigger>
                        <Trigger Property="IsEnabled" 
                            Value="false">
                            <Setter Property="Foreground" 
                                Value="#ADADAD"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</Style.Resources>

La porzione del template che costruisce lo styling per il closeable tab item, abbiamo gli stili relativi ai trigger, bottone normale, hover e pressed, e poi lo stile ed il template per il close button, la sua forma, dimensione ed il comportamento con i trigger di stile per lo hover ed il press. Anche in questa serie di predisposizioni per gli stili vediamo l’uso di StaticResource, TemplateBinding e DynamicResource per fare in modo che il CloseableTabItem si adatti agli stili relativi all’ambiente in cui è contenuto.

Vediamo ora come va costruita la classe che fornisce al nuovo Tab Item le funzionalità che ci aspettiamo.

La classe CloseableTabItem

public class CloseableTabItem : TabItem
{
    static CloseableTabItem()
    {
        //This style is defined in themes\generic.xaml
        DefaultStyleKeyProperty.OverrideMetadata(typeof(CloseableTabItem),
            new FrameworkPropertyMetadata(typeof(CloseableTabItem)));
    }

    public static readonly RoutedEvent CloseTabEvent =
        EventManager.RegisterRoutedEvent("CloseTab", RoutingStrategy.Bubble,
            typeof(RoutedEventHandler), typeof(CloseableTabItem));

    public event RoutedEventHandler CloseTab
    {
        add { AddHandler(CloseTabEvent, value); }
        remove { RemoveHandler(CloseTabEvent, value); }
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        Button closeButton = base.GetTemplateChild("PART_Close") as Button;
        if (closeButton != null)
            closeButton.Click += new System.Windows.RoutedEventHandler(closeButton_Click);
    }

    void closeButton_Click(object sender, System.Windows.RoutedEventArgs e)
    {
        this.RaiseEvent(new RoutedEventArgs(CloseTabEvent, this));
    }
}

La classe del nuovo Tab Item è compatta, e le operazioni che fa sono le seguenti:

  • Nel costruttore effettua la sostituzione dello stile normale del TabItem con quello che abbiamo definito nel file Generic.xaml
  • Definisce e registra l’evento CloseTab agganciandolo al button definito nel Template.
  • Intercetta l’evento OnApplyTemplate e aggancia l’event handler al bottone Close da noi creato.
  • Implementa il metodo che solleva l’evento CloseTabEvent alla pressione del Button.

La classe MainWindow

Vediamo come implementare la WIndow per testare il nostro nuovo Tab Item.

<Window 
    x:Class="Dnw.TestTemplateModification.Windows.MainWindow"         
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"         
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"         
    Title="Test Modify template" 
    Height="350" 
    Width="525">
 
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <StackPanel 
            Grid.Row="0" 
            Orientation="Horizontal" 
            FlowDirection="LeftToRight" 
            Margin="5,2,5,5">
            <Button Name="AddTab"  
                Margin="5,2,5,2" 
                Padding="5" 
                Click="AddTab_Click" >_
                Add tab
            </Button>
            <Button Name="ClearTabs" 
                Margin="5,2,5,2" 
                Padding="5" 
                Click="ClearTabs_Click">_
                Clear tabs
            </Button>
        </StackPanel>  
        <TabControl Grid.Row="1" Name="tbc">
        </TabControl>
    </Grid>
</Window>

La window che abbiamo creato è molto semplice, contiene uno stack panel con due button, uno per aggiungere l’altro per cancellare i tabs ed un TabControl.

 private int mTabCounter = 0;
public MainWindow()
{
    InitializeComponent();
}

private void AddTab_Click(object sender, RoutedEventArgs e)
{
    CloseableTabItem tab = new CloseableTabItem();
    Grid grid = new Grid();
    grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Auto) });
    grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Auto) });
    grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Auto) });
    tab.Content = grid;
    mTabCounter++;
    tab.Header = string.Format("Tab N. {0}", mTabCounter);
    TextBlock txt = new TextBlock()
    {
        Text = string.Format("Titolo del contenuto {0}", mTabCounter),
        Background = new SolidColorBrush(Colors.Yellow),
    };
    grid.Children.Add(txt);
    Grid.SetRow(txt, 0);

    txt = new TextBlock()
    {
        Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ....";
        Background = new SolidColorBrush(Colors.Aqua),
        TextWrapping = TextWrapping.Wrap
    };
    grid.Children.Add(txt);
    Grid.SetRow(txt, 1);

    txt = new TextBlock()
    {
        Text = "Test the footer text",
        Background = new SolidColorBrush(Colors.Lime)
    };
    grid.Children.Add(txt);
    Grid.SetRow(txt, 2);

    tab.CloseTab+=tab_CloseTab;
    tbc.Items.Add(tab);
    tab.IsSelected = true;
}


void tab_CloseTab(object sender, RoutedEventArgs e)
{
    CloseableTabItem tab = sender as CloseableTabItem;
    if (tab != null)
    {
        tbc.Items.Remove(tab);
    }
}

private void ClearTabs_Click(object sender, RoutedEventArgs e)
{
    mTabCounter = 0;
    tbc.Items.Clear();
}

Il code behind della finestra, vediamo che cosa vi troviamo:

  • L’event handler del bottone AddTab, che genera un CloseableTabItem vi inserisce dei controlli e dei dati e lo aggiunge al tab control da noi definito.
  • L’event handler del bottone ClearTabs, che rimuove tutti i TabItems dal TabControl.
  • L’event handler dell’evento CloseTab, che elimina dal TabControl il tab che ha scatenato l’evento.

Sembra piuttosto semplice estendere un controllo WPF per aggiungervi funzionalità, e siamo certi che coloro che leggono l’articolo diranno “Sicuro, se il codice è già scritto è facile.” più difficile è immaginarsi prima come risolvere il problema e quali controlli introdurre nel Template e negli stili, quali trigger scatenare, come costruire  gli eventi. Probabilmente dobbiamo darci tempo per digerire tutte le novità di WPF, sicuramente questa realizzazione dimostra le enormi potenzialità fornite dal sistema per poter inventare nuove cose.

make_template_window_01

L’interfaccia base

make_template_window_02

L’interfaccia con i tab generati a runtime.

Il codice del progetto di esempio relativo alle librerie è disponibile al link seguente:

Il codice relativo alle librerie di uso comune collegate a questo progetto è disponibile al link seguente:

Per qualsiasi domanda, utilizzate il link al modulo di contatto o il link al forum Microsoft Italia dove rispondo quasi ogni giorno.