Telerik blogs

Let’s go through implementing several scenarios for the TabControl with quick, short explanations:

   1. How to databind the TabControl?
   2. How to enable drag-rearrange of items?
   3. How to animate rearranging items?
   4. How to add a Close Button to the TabItems using a Command?
   5. How to animate content changes?
   6. How to add a “New Tab” button when databound?
   7. How to animate item add / removes?

Here is the end result: 

 

And the demo project with everything included:

 tabcontrol-howto-demos.zip

And of course the details, in short -

How to databind the TabControl?

The TabControl is a standard ItemsControl and can be databound to any IEnumerable. It is preferable to bind to an observable collection since any changes in it will be reflected by the TabControl.

In this case we are going to bind to a simple Document class:

 

public class Document
{
    public string Title { get; set; }
 
    public string Content { get; set; }
}

The TabControl will use its ItemTemplate for the Headers of the TabItems and its ContentTemplate for their content. We can simply define them as:

 

<DataTemplate x:Name="content">
    <Grid>
        <TextBox Text="{Binding Content, Mode=TwoWay}"
                 Margin="5"/>
    </Grid>
</DataTemplate>

 

and

<DataTemplate>
    <Grid>
        <TextBlock Text="{Binding Title}" />
    </Grid>
</DataTemplate>

 

A small note: When a control is databound we refer to our business objects as its “items” and to the generated TabItems as “item containers” or just “containers”.

How to enable drag rearrange of items?

This can be enabled by setting a property:

<telerik:RadTabControl
    x:Name="tabControl"
    AllowDragReorder="True"/>

 

How to animate rearranging items?

Every ItemsControl does not arrange its containers but uses a panel for this purpose. This panel can be changed by setting an ItemsPanel. There we can make use of Silverlight 4’s FluidLayout:

 

<telerik:RadTabControl.ItemsPanel>
    <ItemsPanelTemplate>
        <tabCotnrol:TabWrapPanel>
            <i:Interaction.Behaviors>
                <in:FluidMoveBehavior AppliesTo="Children"
                                      InitialTag="DataContext"
                                      Tag="DataContext"
                                      Duration="0:0:0.2">
                    <in:FluidMoveBehavior.EaseX>
                        <CubicEase />
                    </in:FluidMoveBehavior.EaseX>
                </in:FluidMoveBehavior>
            </i:Interaction.Behaviors>
        </tabCotnrol:TabWrapPanel>
    </ItemsPanelTemplate>
</telerik:RadTabControl.ItemsPanel>

Please note that the fluid layout behavior will normally compare the containers of items to but when we rearrange them we need to add & remove an item. This means that a new container will be generated for it. We need to tell the FluidLayout behavior to compare the items (Tag=”DataContext”) and not containers (Tag=”Element”).

How to add a Close Button to the TabItems using a Command?

Adding an ‘x’ button in the ItemTemplate should be straightforward. Then comes the question how to handle the Click event on the button.

Generally the Click can indeed be handled if the DataTemplate is in the resources of a UserControl for example. This way the DataTemplate becomes stuck to the particular user control and cannot be easily moved and shared - not at all the nature of DataTemplates.

One way around this is to use the Telerik’s RadButton and its routed Click event. Since it is a routed event, it can be handled at any of the visual parents of the button, e.g. the TabControl itself. This not bad be we can go one better – use a command.

You may have seen examples of DelegateCommands - they simply wrap two methods and must be exposed on the ViewModel.

In this case we are going to use Telerik’s Routed Commands which work very much like the commands in WPF. They are slightly more abstract but offer greater flexibility. Which you use is up to you.

We define the commands like so:

public static class DocumentCommands
{
    public static readonly ICommand AddItem =
        new RoutedUICommand("Add Item", "AddItem", typeof(DocumentCommands));

    public static readonly ICommand CloseItem =
        new RoutedUICommand("Close Item", "CloseItem", typeof(DocumentCommands));
}

 

And we add commands bindings in xaml, attaching them to the MainPage user control:

<telerik:CommandManager.CommandBindings>
    <telerik:CommandBindingCollection>
        <telerik:CommandBinding Command="local:DocumentCommands.AddItem"
                                CanExecute="OnAddCanExecute"
                                Executed="OnAddExecute" />
        <telerik:CommandBinding Command="local:DocumentCommands.CloseItem"
                                CanExecute="OnCloseCanExecute"
                                Executed="OnCloseExecute" />
    </telerik:CommandBindingCollection>
</telerik:CommandManager.CommandBindings>

 

Note that the command bindings can be added in code as well. They are just a shortcut for handling the CommandManager.CanExecuteEvent and CommandManager.ExecutedEvent.

We handle the removal of items like so:

 
private void OnCloseCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    e.CanExecute = true;
    e.Handled = true;
}
 
private void OnCloseExecute(object sender, ExecutedRoutedEventArgs e)
{
    this.source.Remove(e.Parameter);
}

How to animate content changes?

We can use Telerik’s TransitioningContentControl for this purpose. It provides several built-in content transitions. We can replace the TabControl’s ContentPresenter with this control by editing its Template and replacing the “ContentElement” with this:

 

<telerik:RadTransitionControl x:Name="ContentElement"
                              Content="{TemplateBinding SelectedContent}"
                              Duration="0:0:01"
                              IsTabStop="False"
                              ContentTemplate="{TemplateBinding SelectedContentTemplate}" >
    <telerik:RadTransitionControl.Easing>
        <CubicEase EasingMode="EaseOut"/>
    </telerik:RadTransitionControl.Easing>
    <telerik:RadTransitionControl.Transition>
        <telerik:SlideAndZoomTransition SlideDirection="RightToLeft"
                                        MinAlpha="1"
                                        MinZoom="1"/>
    </telerik:RadTransitionControl.Transition>
</telerik:RadTransitionControl>

Note that there are other available transitions as well, they can be previewed in the demos.

In this case the SlideAndZoomTransition as a simple slide by setting its MinZoom and MinAlpha properties to 1.

This animates the content changes always in a single direction.

How to change the direction depending on whether the selected index is increased or decreased?

There are two tricky bits here – when to handle the selection change and how to get to the Transitioning control.

We can use the PreviewSelectionChanged, but when we are not implementing an instance-specific handler, we can use Class Routed Event Handlers. They are called every time a particular event passes though an instance of the control. They are registered statically and in essence can be used for extending the functionality of all controls of a given type.

In our case:

 

EventManager.RegisterClassHandler(
    typeof(RadTabControl),
    RadTabControl.PreviewSelectionChangedEvent,
    new Telerik.Windows.Controls.SelectionChangedEventHandler(OnTabSelectionSetSlideDirection));

and the handler itself:

 

private static void OnTabSelectionSetSlideDirection(object sender, Telerik.Windows.Controls.SelectionChangedEventArgs e)
{
    var tabControl = sender as RadTabControl;
    if (VisualTreeHelper.GetChildrenCount(tabControl) > 0
        && e.RemovedItems.Count > 0
        && e.AddedItems.Count > 0)
    {
        var firstChild = VisualTreeHelper.GetChild(tabControl, 0) as FrameworkElement;
        var transitioningControl = firstChild.FindName("ContentElement") as RadTransitionControl;
         
        if (transitioningControl != null)
        {
            var slideTransition = transitioningControl.Transition as SlideAndZoomTransition;
            if (slideTransition != null)
            {
                var oldIndex = tabControl.Items.IndexOf(e.RemovedItems[0]);
                var newIndex = tabControl.Items.IndexOf(e.AddedItems[0]);
                slideTransition.SlideDirection = oldIndex < newIndex ? FlowDirection.RightToLeft : FlowDirection.LeftToRight;
            }
        }
    }
}

Note how we can retrieve a template element from outside a control. The FindName method works looks for an element in the current NameScope and the first visual child of a control comes from its ControlTemplate and has the namescope we need.

How to add a “New Tab” button when databound?

It is not a problem to make a TabItem to look like an “Add” button:

<DataTemplate>
    <Image Stretch="None" Source="Images/Add.png"/>
</DataTemplate>

 

Adding a tab after all other containers will be difficult it the “Add” button is not an item itself. But the alternative this will mean to include an alien object in our ItemsSource collection and make sure that it always stays a last element.

An easy way to achieve this is to have a wrapper for the ItemsSource that will pretend to be a collection but it will just delegate all collection method to the ItemsSource – it will just append a special item to end of the collection and it will make sure it stays there (abridged):

public sealed class AddTabCollection : IList, INotifyCollectionChanged
{
    private IList originalCollection;
    private object customAddTabItem;
 
    public AddTabCollection(IList originalCollection, object addTabItem)
    {
        Contract.Requires<ArgumentNullException>(originalCollection != null);
 
        this.originalCollection = originalCollection;
        this.customAddTabItem = addTabItem;
 
        var observable = originalCollection as INotifyCollectionChanged;
        if (observable != null)
        {
            observable.CollectionChanged += this.OnOriginalCollectionChanged;
        }
    }
 
    public int Count
    {
        get { return this.originalCollection.Count + 1; }
    }
 
    public IEnumerator GetEnumerator()
    {
        foreach (var item in this.originalCollection)
        {
            yield return item;
        }
 
        yield return this.customAddTabItem ?? AddTabItemValue;
    }
}

Then we can use it like so:

var wrappedSource = new AddTabCollection(this.source, DocumentCommands.AddItem);
tabControl.ItemsSource = wrappedSource;

 

If we have ViewModel for the page, this may mean creating a second ItemsSource property that just wraps the original.

This way the “add” item will always be there but we need to make it look different as well. Here we can use a custom template selector that will pick between two templates based on whether it is an “Add” button:

public class AddTabTemplateSelector : DataTemplateSelector
{
    public DataTemplate DefaultTemplate { get; set; }
 
    public DataTemplate AddTabTemplate { get; set; }
 
    public object AddTabItem { get; set; }
 
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        if (item == (this.AddTabItem ?? AddTabCollection.AddTabItemValue))
        {
            return this.AddTabTemplate;
        }
        else
        {
            return this.DefaultTemplate;
        }
    }
}

 

Here is how we use it in xaml:

<local:AddTabTemplateSelector x:Name="selector" >
    <local:AddTabTemplateSelector.AddTabTemplate>
        <DataTemplate>
            <Image Stretch="None" Source="Images/Add.png"/>
        </DataTemplate>
    </local:AddTabTemplateSelector.AddTabTemplate>
    <local:AddTabTemplateSelector.DefaultTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Title}" />
        </DataTemplate>
    </local:AddTabTemplateSelector.DefaultTemplate>
</local:AddTabTemplateSelector>

 

We then need to make sure that the “add” tab is never selected. We can do this by handling the PreviewSelectionChanged event. We will go one better and use commands. Thus if the item is a command, we will execute it and cancel the selection:

 

private static void OnTabSelectionRunCommand(object sender,
    Telerik.Windows.Controls.SelectionChangedEventArgs e)
{
    var command = e.AddedItems.OfType<ICommand>().FirstOrDefault();
    if (command != null)
    {
        e.Handled = true;
        if (command.CanExecute(null))
        {
            command.Execute(null);
        }
    }
}

Then we will pass the command to our wrapper and tell the TemplateSelector that this is our “Add Tab”:

(this.Resources["selector"] as AddTabTemplateSelector).AddTabItem = wrappedSource.AddTabItem;

Here is how we handle the command itself:

private void OnAddCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    e.CanExecute = true;
}
 
private void OnAddExecute(object sender, ExecutedRoutedEventArgs e)
{
    var newDocument = new Document { Title = "Untitled.txt", Content = "New Document..." };
 
    Dispatcher.BeginInvoke(() =>
        {
            source.Add(newDocument);
            tabControl.SelectedItem = newDocument;
            tabControl.ScrollIntoView(tabControl.Items[tabControl.Items.Count - 1]);
        });
}

Note that the adding happens in a Dispatcher. This means that the given action will not be executed in the same callstack. This helps in cases when the ItemsSource needs to be changed while it is in the process of changing.

E.g. the user removes the last standard tab. Immediately the TabControl selects the last remaining item, the “Add” item. This fires the command and a new item is added. This will raise an exception since an observable collection cannot be changed in its CollectionChanged handler.

One last thing to do is make sure that our other changes work as well. In particular the DragDrop can be a problem since the “Add” item should not be rearranged / replaced.

A class handler can solve this for every instance:

private static void OnTabItemDropQuery(object sender, DragDropQueryEventArgs e)
{
    var tabItem = sender as RadTabItem;
    var tab = Telerik.Windows.Controls.ItemsControl.ItemsControlFromItemContainer(tabItem);
    if (tab == null)
    {
        return;
    }
 
    var itemsSource = tab.ItemsSource as AddTabCollection;
    if (itemsSource != null)
    {
        if (itemsSource.AddTabItem == tabItem.DataContext)
        {
            e.QueryResult = false;
            e.Handled = true;
        }
    }
}

How to animate item add / removes?

Silverlight 4 comes with a rarely used feature – LayoutStates. They allow us to animate items in and out of view when they are added or removed in an items control.

The states are available for any container but they do not quite appear in Blend so you just need to know that they exist.

To change the visual states we need to edit the TabItem’s Control Template and assign the style to TabControl’s ItemContainerStyle. The only change in the control template is the addition of the three visual states:

 

<VisualStateGroup x:Name="LayoutStates">
    <VisualStateGroup.Transitions>
        <VisualTransition GeneratedDuration="0:0:0.2">
            <VisualTransition.GeneratedEasingFunction>
                <CubicEase EasingMode="EaseOut"/>
            </VisualTransition.GeneratedEasingFunction>
        </VisualTransition>
    </VisualStateGroup.Transitions>
    <VisualState x:Name="BeforeUnloaded">
        <Storyboard>
            <DoubleAnimation Duration="0" To="23"
                             Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateY)"
                             Storyboard.TargetName="wrapper" />
        </Storyboard>
    </VisualState>
    <VisualState x:Name="AfterLoaded"/>
    <VisualState x:Name="BeforeLoaded">
        <Storyboard>
            <DoubleAnimation Duration="0" To="24"
                             Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateY)"
                             Storyboard.TargetName="wrapper" />
        </Storyboard>
    </VisualState>
</VisualStateGroup>

At this point you may notice that the LayoutStates clash with the FluidLayout during drag rearrange since they both are trying to animate the tab items in a similar way. To work around this we can use a custom VisualStateManager that will not play state transitions in the TabItems when DragDrop is active. We define the state manager like so:

public class CustomTabVisualStateManager : VisualStateManager
{
    protected override bool GoToStateCore(
        Control control,
        FrameworkElement templateRoot,
        string stateName,
        VisualStateGroup group,
        VisualState state,
        bool useTransitions)
    {
        var useTransitionsOverride = !RadDragAndDropManager.IsDragging && useTransitions;
        return base.GoToStateCore(control, templateRoot, stateName, group, state, useTransitionsOverride);
    }
}

 

Then we need to assign it xaml:

 

<VisualStateManager.CustomVisualStateManager>
    <local:CustomTabVisualStateManager />
</VisualStateManager.CustomVisualStateManager>

Hopefully you will be able to use some of these ideas in your projects.


About the Author

Kiril Stanoev

Hi, I'm Kiril and I'm the Product Manager of Telerik UI for Android, Windows Universal and Windows Phone. Feel free to ping me on +KirilStanoev or @KirilStanoev

Comments

Comments are disabled in preview mode.