Edit this page

How to Make the Tab Headers Editable

The goal of this tutorial is to create a tab control with editable headers of the tab items. The idea is to allow runtime change of the tab item's header text as shown on the snapshot below.

For the purpose of this example, you will need to create an empty Silverlight Application project and open it in Visual Studio.

If you copy and paste the source code directly from this XAML examples, don't forget to change xmlns:example alias to import the namespace used in your project.

First add references to the assemblies Telerik.Windows.Controls and Telerik.Windows.Controls.Navigation.

Then create a new Silverlight Templated Control - EditableTabHeader that derives from ContentControl and leave it empty for now.

public class EditableTabHeader : ContentControl
{
    public EditableTabHeader()
    {
       this.DefaultStyleKey = typeof(EditableTabHeader);
    }
}
Public Class EditableTabHeader
    Inherits ContentControl
    Public Sub New() 
      Me.DefaultStyleKey = GetType(EditableTabHeader)
    End Sub
End Class

Create a new style for the EditableTabHeader control.

<Style TargetType="example:EditableTabHeader">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="example:EditableTabHeader">
                <Grid>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="EditStates">
                            <VisualState x:Name="EditMode">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName="ContentPresenter"
                                            Storyboard.TargetProperty="Visibility">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Collapsed</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName="TextBox"
                                            Storyboard.TargetProperty="Visibility">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="ViewMode">
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <ContentPresenter Content="{TemplateBinding Content}"
                            x:Name="ContentPresenter"
                            ContentTemplate="{TemplateBinding ContentTemplate}" />
                    <TextBox Text="{TemplateBinding Content}" x:Name="TextBox"
                            Visibility="Collapsed" />
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

In the XAML code above we create new style for the EditableTabHeader control and this style will be the default template for that control. The template is made of ContentPresenter, TextBox and a state group EditStates with two new states EditMode and ViewMode. The "EditMode" state contains a storyboard that hides the content presenter control and makes the text box visible, while the ViewMode state does nothing, which means that when the control is in this state it will have its default appearance.

Add the following implementation to the code behind of the EditableTabHeader class.

[TemplateVisualState(GroupName = "EditStates", Name = "EditMode")]
[TemplateVisualState(GroupName = "EditStates", Name = "ViewMode")]
public class EditableTabHeader : ContentControl
{
    private TextBox textBox;
    private DateTime previosLeftClickTime = DateTime.Now;
    private Point previosLeftClickPoint;
    private TimeSpan doubleClickSpan = TimeSpan.FromSeconds(0.4);
    public static readonly DependencyProperty IsInEditModeProperty = DependencyProperty.Register(
        "IsInEditMode", 
        typeof(bool), 
        typeof(EditableTabHeader), 
        new PropertyMetadata(OnIsInEditModeChanged));

    public EditableTabHeader()
    {
        DefaultStyleKey = typeof(EditableTabHeader);
    }
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        this.textBox = this.GetTemplateChild("TextBox") as TextBox;
        this.textBox.LostFocus += new RoutedEventHandler(textBox_LostFocus);
    }
    private void textBox_LostFocus(object sender, RoutedEventArgs e)
    {
        this.IsInEditMode = false;
    }
    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        base.OnMouseLeftButtonDown(e);
        var currentTime = DateTime.Now;
        var currentPoint = e.GetPosition(this);
        var durationBetweenClicks = currentTime - previosLeftClickTime;
        if (currentPoint == previosLeftClickPoint && durationBetweenClicks < this.doubleClickSpan)
        {
            e.Handled = true;
            this.IsInEditMode = !this.IsInEditMode;
        }
        this.previosLeftClickTime = DateTime.Now;
        this.previosLeftClickPoint = e.GetPosition(this);
    }
    public bool IsInEditMode
    {
        get { return (bool)GetValue(IsInEditModeProperty); }
        set { SetValue(IsInEditModeProperty, value); }
    }
    private static void OnIsInEditModeChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var editableContentControl = sender as EditableTabHeader;
        var newValue = (bool)e.NewValue;
        if (!newValue)
        {
            editableContentControl.Content = editableContentControl.textBox.Text;
        }
        editableContentControl.ChangeVisualStates();
    }
    public void ChangeVisualStates()
    {
        if (this.IsInEditMode)
        {
            VisualStateManager.GoToState(this, "EditMode", true);
        }
        else
        {
            VisualStateManager.GoToState(this, "ViewMode", true);
        }
    }
}
    <TemplateVisualState(GroupName:="EditStates", Name:="EditMode")>
    <TemplateVisualState(GroupName:="EditStates", Name:="ViewMode")>
    Public Class EditableTabHeader
        Inherits ContentControl
        Private textBox As TextBox
        Private previosLeftClickTime As DateTime = DateTime.Now
        Private previosLeftClickPoint As Point
        Private doubleClickSpan As TimeSpan = TimeSpan.FromSeconds(0.4)

        Public Shared ReadOnly IsInEditModeProperty As DependencyProperty = DependencyProperty.Register("IsInEditMode", GetType(Boolean), GetType(EditableTabHeader), New PropertyMetadata(AddressOf OnIsInEditModeChanged))

        Public Sub New()
            DefaultStyleKey = GetType(EditableTabHeader)
        End Sub

        Public Overloads Overrides Sub OnApplyTemplate()
            MyBase.OnApplyTemplate()

            Me.textBox = TryCast(Me.GetTemplateChild("TextBox"), TextBox)

            AddHandler Me.textBox.LostFocus, AddressOf textBox_LostFocus
        End Sub

        Private Sub textBox_LostFocus(ByVal sender As Object, ByVal e As RoutedEventArgs)
            Me.IsInEditMode = False
        End Sub

        Protected Overloads Overrides Sub OnMouseLeftButtonDown(ByVal e As MouseButtonEventArgs)
            MyBase.OnMouseLeftButtonDown(e)

            Dim currentTime = DateTime.Now
            Dim currentPoint = e.GetPosition(Me)
            Dim durationBetweenClicks = currentTime - previosLeftClickTime

            If currentPoint = previosLeftClickPoint AndAlso durationBetweenClicks < Me.doubleClickSpan Then
                e.Handled = True
                Me.IsInEditMode = Not Me.IsInEditMode
            End If

            Me.previosLeftClickTime = DateTime.Now
            Me.previosLeftClickPoint = e.GetPosition(Me)
        End Sub

        Public Property IsInEditMode() As Boolean
            Get
                Return CBool(GetValue(IsInEditModeProperty))
            End Get
            Set(ByVal value As Boolean)
                SetValue(IsInEditModeProperty, value)
            End Set
        End Property

        Private Shared Sub OnIsInEditModeChanged(ByVal sender As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
            Dim editableContentControl = TryCast(sender, EditableTabHeader)
            Dim newValue = CBool(e.NewValue)

            If Not newValue Then
                editableContentControl.Content = editableContentControl.textBox.Text
            End If

            editableContentControl.ChangeVisualStates()
        End Sub

        Public Sub ChangeVisualStates()
            If Me.IsInEditMode Then
                VisualStateManager.GoToState(Me, "EditMode", True)
            Else
                VisualStateManager.GoToState(Me, "ViewMode", True)
            End If
        End Sub
    End Class

The major changes in the implementation of the EditableTabHeader control are:

  • The control contract definition is done using two attributes of type "TemplateVisualState" placed right above the class definition. With this contract we declare all of the existing states and parts for that control.

  • New dependency property IsInEditMode of Boolean type was added.

  • One text box field declaration plus three additional fields related to the editing. The text box field is initialized with the reference from the text box defined in the template when the base method OnApplyTemplate is invoked.

  • Call back method OnIsInEditModeChanged invoked every time the value of the dependency property IsInEditMode is changed. This method changes the state of the control depending on the value of the IsInEditMode property.

  • The method OnMouseLeftButtonDown was overridden to move the control from View to EditMode state and vice versa depending on some internal logic for edit.

Now you can add a RadTabControl to the MainPage.xaml and EditableTabHeader control to define the TabItems Header:

<UserControl x:Class="CSharp.RadTabControl.HowTo_EditableTabHeader.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"&#13;    xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"   
    xmlns:example="clr-namespace:CSharp.RadTabControl.HowTo_EditableTabHeader"
    Width="400" Height="300">
    <Grid x:Name="LayoutRoot" Background="White">

        <telerik:RadTabControl x:Name="radTabControl">
            <telerik:RadTabControl.ContentTemplate>
                <!--The Content Template:-->
                <DataTemplate>
                    <Grid Background="WhiteSmoke">
                        <TextBlock Text="{Binding Content}" />
                    </Grid>
                </DataTemplate>
            </telerik:RadTabControl.ContentTemplate>
            <telerik:RadTabControl.ItemTemplate>
                <!--The Header Template:-->
                <DataTemplate>
                    <example:EditableTabHeader Content="{Binding Name, Mode=TwoWay}" />
                </DataTemplate>
            </telerik:RadTabControl.ItemTemplate>
        </telerik:RadTabControl>
    </Grid>
</UserControl>

In the XAML code above we create new rad tab control and predefine its ItemTempalte and ItemContainerStyle. In the ItemContainerStyle definition we set the HeaderTemplate of the control to the EditableTabHeader control. EditableTabHeader control will use automatically the default template defined in the Themes\Generic.xaml file.

Open the appropriate MainPage.xaml code behind class and paste the following content to bind and populate the tab control to a collection of the custom object TabItemModel:

public partial class MainPage: UserControl
{
    public MainPage()
    {
        InitializeComponent();
        radTabControl.ItemsSource = Enumerable.Range(1, 5).Select(num =>
            new TabItemModel()
            {
                Name = String.Format("Header {0}", num),
                Content = String.Format("Content {0}", num)
            });
    }
}
public class TabItemModel : ViewModelBase
{
 private String name; 
 private String content;
 public String Name
 {
  get
  {
   return this.name;
  }
  set
  {
   if (this.name != value)
   {
    this.name = value;
    OnPropertyChanged("Name");
   }
  }
 }

 public String Content
 {
  get
  {
   return this.content;
  }
  set
  {
   if (this.content != value)
   {
    this.content = value;
    OnPropertyChanged("Content");
   }
  }
 }

}
Imports Telerik.Windows.Controls

    Partial Public Class MainPage
        Inherits UserControl

        Public Sub New()
            InitializeComponent()

            radTabControl.ItemsSource = Enumerable.Range(1, 5).Select
        End Sub
    End Class
    Public Class TabItemModel
        Inherits ViewModelBase
        Private _name As [String]
        Private _content As [String]

        Public Property Name() As [String]
            Get
                Return Me._name
            End Get
            Set(value As [String])
                If Me._name <> value Then
                    Me._name = value
                    OnPropertyChanged("Name")
                End If
            End Set
        End Property

        Public Property Content() As [String]
            Get
                Return Me._content
            End Get
            Set(value As [String])
                If Me._content <> value Then
                    Me._content = value
                    OnPropertyChanged("Content")
                End If
            End Set
        End Property
    End Class

See Also