Thursday, December 22, 2011

Customize TabControl in Silverlight (create tab control with side bar)


The TabControl control brings flexibility to your websites navigation by allowing you to create a set of Tabs that can be used to organize page content.


TabControl is Silverlight toolkit control, so first you have to install latest toolkit download from http://www.silverlight.net
After installing toolkit you will see TabControl in your toolbox. 

In Silverlight Development Environment ,it is difficult to change the layout of TabControl.

Sometimes we have requirement to customize our tab control layout, So we need to create custom template for TabControl and TabItems.

In this article we walk-through how to Create Sidebar control with TabControl on right of side bar and show/hide side bar.

Go Through Code


Step 1 : Create Custom ScalePanel Control for Side bar

ScalePanel.cs 
  public class ScalePanel : Panel
    {
        #region ScaleXProperty
        public Double ScaleX
        {
            get { return (Double)GetValue(ScaleXProperty); }
            set { SetValue(ScaleXProperty, value); }
        }
        public static readonly DependencyProperty ScaleXProperty =
            DependencyProperty.Register("ScaleX", typeof(Double),
            typeof(ScalePanel),  
           new PropertyMetadata(1.0d, new PropertyChangedCallback(ScaleXChanged)));

        public static void ScaleXChanged(DependencyObject sender, 
               DependencyPropertyChangedEventArgs e)
        {
            ScalePanel obj = sender as ScalePanel;
            if (obj != null)
            {
                obj.OnScaleXChanged(e);
            }
        }
        private void OnScaleXChanged(DependencyPropertyChangedEventArgs e)
        {
            InvalidateMeasure();
        }
        #endregion

        #region ScaleYProperty
        public Double ScaleY
        {
            get { return (Double)GetValue(ScaleYProperty); }
            set { SetValue(ScaleYProperty, value); }
        }

        public static readonly DependencyProperty ScaleYProperty =
            DependencyProperty.Register("ScaleY", typeof(Double), 
            typeof(ScalePanel), 
            new PropertyMetadata(1.0d, new PropertyChangedCallback(ScaleYChanged)));

        public static void ScaleYChanged(DependencyObject sender, 
                 DependencyPropertyChangedEventArgs e)
        {
            ScalePanel obj = sender as ScalePanel;
            if (obj != null)
            {
                obj.OnScaleYChanged(e);
            }
        }

        private void OnScaleYChanged(DependencyPropertyChangedEventArgs e)
        {
            InvalidateMeasure();
        }
        #endregion

        protected override Size MeasureOverride(Size availableSize)
        {
            Size finalSize = new Size();

            if (Children.Any())
            {
                UIElement child = Children.First();
                child.Measure(availableSize);
                finalSize.Width = Math.Min(child.DesiredSize.Width, availableSize.Width);
                finalSize.Height = Math.Min(child.DesiredSize.Height, availableSize.Height);
            }

            finalSize.Width = finalSize.Width * ScaleX;
            finalSize.Height = finalSize.Height * ScaleY;

            return finalSize;
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            if (Children.Any())
            {
                UIElement child = Children.First();
                child.Arrange(new Rect(0, 0, finalSize.Width, finalSize.Height));
            }

            return finalSize;
        }
    } 
In this code you notice that ScaleXProperty and ScaleYProperty dependency property defined.
So we arise question what is DepenencyProperty ? and why we need to use.
I give you brief description to aware about DepenencyProperty.
DependencyProperty
Dependency Property is like any other property but can hold a default value, with built in mechanism for property value validation and automatic notification for changes in property value ( for anyone listening to property value - especially UI) and any binding in Silverlight is to binded to a Dependency Property.

Dependency properties are properties that are registered with the Silverlight property system by calling the DependencyProperty.Register method.

The purpose of dependency properties is to provide a way to compute the value of a property based on the value of other inputs.

DependencyObject defines the base class that can register and own a dependency property.
You can go into detail with example to follow this link.

so both scaleY and ScaleY property scales the panel with content.

There are two override method defined, One is MeasureOverride and other is ArrangeOverride.

  • MeasureOverride
Provides the behavior for the Measure pass of Silverlight layout.
This method has a default implementation that performs built-in layout for most Silverlight FrameworkElement classes.
So in our code it customize the Measure pass logic for a custom panel implementation and it perform following task.
   1. Iterates over children.
   2.  For each child, calls Measure, using a Size that makes sense based on how the panel logic treats the  number of children and its own known size limit
   3.   Returns its size (determines it needs during layout, based on its calculations of child object allotted sizes)
You can go into detail with example to follow this link   
  • ArrangeOverride
Provides the behavior for the Arrange pass of Silverlight layout.
In simple it Arranges the content of a FrameworkElement.
ArrangeOverride to customize the Arrange pass
1.Iterates over children.
2.For each child, calls Arrange, using a Rect where Height and Width are based on DesiredSize, and X and Y are based on logic that is specific to the panel.
3.Returns its size (The actual size that is used after the element is arranged in layout)
For Silverlight/WPF, the technique by which elements are sized and positioned in a layout is divided into two steps: a Measure pass, and then an Arrange pass.
You can go into detail with example to follow this link

Step 2: Add namespace in your UserControl.
  xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
  xmlns:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit"       
  xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
  xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" 
 
1.  System.windows.Interactivity provides Event Trigger on control to perform action on event fire.
2.  Interaction provides ControlStoryboardAction with EventTrigger.
      3.  Toolkit is used for Transform Layout of Control.

Step 3: Create Template for TabItem.
 <ControlTemplate x:Key="RightMenuTabItem"
                         TargetType="sdk:TabItem">
            <Border>
                <VisualStateManager.VisualStateGroups>
                    <VisualStateGroup x:Name="CommonStates">
                        <VisualState x:Name="Normal" />
                        <VisualState x:Name="MouseOver">
                            <Storyboard>
                                <DoubleAnimation Storyboard.TargetName="TabHeaderHighlightBackgroundBorder"
                                                 Storyboard.TargetProperty="Opacity"
                                                 To="1"
                                                 Duration="0:0:0.25" />
                                <ObjectAnimationUsingKeyFrames Duration="00:00:00.25"
                                                               Storyboard.TargetName="ContControl"
                                                               Storyboard.TargetProperty="Foreground">
                                    <DiscreteObjectKeyFrame KeyTime="00:00:00">
                                        <DiscreteObjectKeyFrame.Value>
                                            <Color>Black</Color>
                                        </DiscreteObjectKeyFrame.Value>
                                    </DiscreteObjectKeyFrame>
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames Duration="00:00:00.25"
                                                               Storyboard.TargetName="TabHeaderBackgroundBorder"
                                                               Storyboard.TargetProperty="BorderThickness">
                                    <DiscreteObjectKeyFrame KeyTime="00:00:00">
                                        <DiscreteObjectKeyFrame.Value>
                                            <Thickness>1 1 0 0</Thickness>
                                        </DiscreteObjectKeyFrame.Value>
                                    </DiscreteObjectKeyFrame>
                                </ObjectAnimationUsingKeyFrames>
                            </Storyboard>
                        </VisualState>
                    </VisualStateGroup>
                    <VisualStateGroup x:Name="SelectionStates">
                        <VisualState x:Name="Unselected" />
                        <VisualState x:Name="Selected">
                            <Storyboard>
                                <DoubleAnimation Storyboard.TargetName="TabHeaderSelectedBackgroundBorder"
                                                 Storyboard.TargetProperty="Opacity"
                                                 To="1"
                                                 Duration="0:0:0.25" />
                                <ObjectAnimationUsingKeyFrames Duration="00:00:00.25"
                                                               Storyboard.TargetName="ContControl"
                                                               Storyboard.TargetProperty="Foreground">
                                    <DiscreteObjectKeyFrame KeyTime="00:00:00">
                                        <DiscreteObjectKeyFrame.Value>
                                            <Color>Black</Color>
                                        </DiscreteObjectKeyFrame.Value>
                                    </DiscreteObjectKeyFrame>
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames Duration="00:00:00.25"
                                                               Storyboard.TargetName="TabHeaderBackgroundBorder"
                                                               Storyboard.TargetProperty="BorderThickness">
                                    <DiscreteObjectKeyFrame KeyTime="00:00:00">
                                        <DiscreteObjectKeyFrame.Value>
                                            <Thickness>1 1 0 0</Thickness>
                                        </DiscreteObjectKeyFrame.Value>
                                    </DiscreteObjectKeyFrame>
                                </ObjectAnimationUsingKeyFrames>
                            </Storyboard>
                        </VisualState>
                    </VisualStateGroup>
                </VisualStateManager.VisualStateGroups>
                <toolkit:LayoutTransformer x:Name="layoutTransformer">
                    <toolkit:LayoutTransformer.LayoutTransform>
                        <RotateTransform Angle="90" />
                    </toolkit:LayoutTransformer.LayoutTransform>
                    <Border x:Name="TabHeaderBackgroundBorder"
                            RenderTransformOrigin="0.5,0.5"
                            BorderBrush="Black"
                            BorderThickness="1,1,0,1"
                            Background="{StaticResource TabHeaderBackground}">
                        <Grid>
                            <Border x:Name="TabHeaderHighlightBackgroundBorder"
                                    Opacity="0"
                                    Background="{StaticResource TabHeaderHighlightBackground}" />
                            <Border x:Name="TabHeaderSelectedBackgroundBorder"
                                    Opacity="0"
                                    Background="{StaticResource TabHeaderSelectedBackground}" />
                            <ContentControl Content="{TemplateBinding Header}"
                                            HorizontalContentAlignment="Center"
                                            VerticalContentAlignment="Center"
                                            Margin="16,10,16,10"
                                            FontFamily="Verdana"
                                            FontSize="15"
                                            Foreground="White"
                                            FontWeight="Bold"
                                            Cursor="Hand"
                                            x:Name="ContControl" />
                        </Grid>
                    </Border>
                </toolkit:LayoutTransformer>
            </Border>
        </ControlTemplate>

Above control template provides style and template for TabItems.

VisualStateManager Manages states and the logic for transitioning between states for controls.

Created customStates which defines MouseOver, Normal and Selected visualstate object for each TabItmes. When user put mouse over the tab items, its background color changes to yellow and on mouse leave it will back to red color.
 
 <toolkit:LayoutTransformer.LayoutTransform>
                        <RotateTransform Angle="90" />
                    </toolkit:LayoutTransformer.LayoutTransform> 
 
 
Above snippet rotate TabItems from horizontal to Vertiacal.
 
Step 4 : Create Template for TabControl.
 <ControlTemplate x:Key="RightMenuTabControl"
                         TargetType="sdk:TabControl">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>
                <Grid Grid.Column="0">
                    <Button Template="{StaticResource PolygonButton}"
                            x:Name="CloseCall"
                            Cursor="Hand">
                        <Polyline HorizontalAlignment="Center"
                                  VerticalAlignment="Center"
                                  Stroke="Black"
                                  StrokeThickness="2"
                                  Points="0,0 4,4 0,8" />
                        <i:Interaction.Triggers>
                            <i:EventTrigger EventName="Click">
                                <ei:ControlStoryboardAction ControlStoryboardOption="Play">
                                    <ei:ControlStoryboardAction.Storyboard>
                                        <Storyboard>
                                            <DoubleAnimation Storyboard.TargetName="TabContentScalePanel"
                                                             Storyboard.TargetProperty="Width"
                                                             From="300"
                                                             To="0"
                                                             Duration="00:00:00.25" />
                                            <ObjectAnimationUsingKeyFrames Duration="00:00:00.25"
                                                                           Storyboard.TargetName="CloseCall"
                                                                           Storyboard.TargetProperty="Visibility">
                                                <DiscreteObjectKeyFrame KeyTime="00:00:00">
                                                    <DiscreteObjectKeyFrame.Value>
                                                        <Visibility>Collapsed</Visibility>
                                                    </DiscreteObjectKeyFrame.Value>
                                                </DiscreteObjectKeyFrame>
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Duration="00:00:00.25"
                                                                           Storyboard.TargetName="OpenCall"
                                                                           Storyboard.TargetProperty="Visibility">
                                                <DiscreteObjectKeyFrame KeyTime="00:00:00">
                                                    <DiscreteObjectKeyFrame.Value>
                                                        <Visibility>Visible</Visibility>
                                                    </DiscreteObjectKeyFrame.Value>
                                                </DiscreteObjectKeyFrame>
                                            </ObjectAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </ei:ControlStoryboardAction.Storyboard>
                                </ei:ControlStoryboardAction>
                            </i:EventTrigger>
                        </i:Interaction.Triggers>
                    </Button>                   
                </Grid>
                <Border Grid.Column="1"
                        x:Name="TabContent">
                    <control:ScalePanel x:Name="TabContentScalePanel"
                                        Width="300">
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="60" />
                                <RowDefinition Height="*" />
                            </Grid.RowDefinitions>
                            <Border Grid.RowSpan="2">
                                <Border.Background>
                                    <RadialGradientBrush Center="0.5,0"
                                                         GradientOrigin="0.5,0"
                                                         RadiusX="0.6"
                                                         RadiusY="0.2">
                                        <GradientStop Color="#FFFFCA3C"
                                                      Offset="1" />
                                        <GradientStop Color="#FFFFFFD5" />
                                    </RadialGradientBrush>
                                </Border.Background>
                            </Border>
                            <Border Grid.RowSpan="2">
                                <Border.Background>
                                    <RadialGradientBrush Center="0.5,1"
                                                         GradientOrigin="1,0"
                                                         RadiusX="1"
                                                         RadiusY="0.8"
                                                         Opacity="0.25">
                                        <GradientStop Color="#FFFFCA3C"
                                                      Offset="1" />
                                        <GradientStop Color="#FFFFFFD5"
                                                      Offset="1" />
                                    </RadialGradientBrush>
                                </Border.Background>
                            </Border>
                            <Border Grid.Row="0"
                                    VerticalAlignment="Top"
                                    HorizontalAlignment="Stretch"
                                    Margin="0,5,10,15">
                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="*" />
                                    </Grid.ColumnDefinitions>
                                    <TextBlock Grid.Column="0"
                                               HorizontalAlignment="Center"
                                               Text="Common Part" />
                                </Grid>
                            </Border>
                            <Border Grid.Row="1"
                                    VerticalAlignment="Stretch">
                                <Grid>
                                    <ContentPresenter x:Name="ContentRight"
                                                      HorizontalAlignment="Stretch"
                                                      VerticalAlignment="Stretch"></ContentPresenter>
                                </Grid>
                            </Border>
                        </Grid>
                    </control:ScalePanel>
                </Border>
                <Grid Grid.Column="2">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*" />
                    </Grid.RowDefinitions>
                    <ItemsPresenter Grid.Row="0" />
                </Grid>
            </Grid>
        </ControlTemplate> 
 
In Above ControlTemplate creates Polygon button to show/hide panel (side bar) and style for TabControl background to set GradientColor.
Created one Trigger for Side bar control and storyboard for animation.
on button click, it executes trigger to show/hide side bar with doubleAnimation.
 
<DoubleAnimation Storyboard.TargetName="TabContentScalePanel"
       Storyboard.TargetProperty="Width"
       From="300"
       To="0"
       Duration="00:00:00.25" />
This code describes to animate scalepanel and set width from 300 to 0.
 
Step 5 : Drag & Drop TabControl into your Page.
 <sdk:TabControl x:Name="MenuItemsTabControl"
                    TabStripPlacement="Right"
                    UseLayoutRounding="True"
                    HorizontalContentAlignment="Stretch"
                    VerticalContentAlignment="Top"
                    Padding="0"
                    Margin="0">
        <sdk:TabControl.Resources>
            <Style TargetType="sdk:TabItem">
                <Setter Property="Template"
                        Value="{StaticResource RightMenuTabItem}" />
            </Style>
            <Style TargetType="sdk:TabControl">
                <Setter Property="Template"
                        Value="{StaticResource RightMenuTabControl}" />
            </Style>
        </sdk:TabControl.Resources>
    </sdk:TabControl> 

In
Above code, applying style and Template to TabControl and TabItems.

Step 6: Switch to ViewModel, Create HeaderViewModel.
 
For notify property on change, download Silverlight.Extensions.dll from http://www.Silverligh.net ,and add reference into your project.
It will add VieModelBase class which inherits from INotifyPropertyChanged for propertyChangeNotification.
VieModelBase class adds methods for RegisterPropertyChangeCallback and process child element.

HeaderViewModel.cs 
  public class HeaderViewModel : ViewModelBase
    {
        public String Header { get; set; }

        public HeaderViewModel()
        { }
        public HeaderViewModel(String header)
        {
            Header = header;
        }
    } 
In above class created Header property to set header on TabItems.
Setp 7 : Create ViewModels for Each TabItems.
DepartmentViewModel.cs 
 public class DepartmentViewModel : HeaderViewModel
    {
        protected Department Department { get;  set; }

        public string DepartmentName
        {
            get { return Department.DepartmentName; }
            set
            {
                Department.DepartmentName = value;
                RaisePropertyChanged(() => DepartmentName);
            }
        }
        public string DepartmentCode
        {
            get { return Department.DepartmentCode; }
            set
            {
                Department.DepartmentCode = value;
                RaisePropertyChanged(() => DepartmentCode);
            }
        }

        public DepartmentViewModel()
            : base("Department")
        {
            Department = new Department() { DepartmentName = "Production", DepartmentCode = "123" };
        }
    } 
In above code, created wrapper for Department Class which Defines  properties from department class and raise property that notify to view on change.
Same way create ViewModel for Employee and Product class to bind properties to view.
 
Step 8 : Create TabPropertiesViewModel which holds all viewmodels added into Items collection.
TabPropertiesViewModel.cs 
 public class TabPropertiesViewModel : ViewModelBase
    {
        private DepartmentViewModel _departmentViewModel;
        public DepartmentViewModel DepartmentViewModel
        {
            get { return _departmentViewModel; }
            set
            {
                _departmentViewModel = value;
                RaisePropertyChanged(() => DepartmentViewModel);
            }
        }
        private EmployeeViewModel _employeeViewModel;

        public EmployeeViewModel EmployeeViewModel
        {
            get { return _employeeViewModel; }
            set
            {
                _employeeViewModel = value;
                RaisePropertyChanged(() => EmployeeViewModel);
            }
        }

        private ProductViewModel _productViewModel;

        public ProductViewModel ProductViewModel
        {
            get { return _productViewModel; }
            set
            {
                _productViewModel = value;
                RaisePropertyChanged(() => ProductViewModel);
            }
        }

        public ObservableCollection<HeaderViewModel> Items { get; set; }

        public TabPropertiesViewModel()
        {
            DepartmentViewModel = ProcessChild(new DepartmentViewModel());
            EmployeeViewModel = ProcessChild(new EmployeeViewModel());
            ProductViewModel = ProcessChild(new ProductViewModel());

            Items = new ObservableCollection<HeaderViewModel>{ 
                DepartmentViewModel,
                EmployeeViewModel,
                ProductViewModel,            
            };
        }
    }
Created Properties for each view model.
I have crated collection of HeaderViewModel because it is base class for all view model.
In constructor of class, create instance of each view model and added  into item collection. 
 
Step 9 : Crate Views for Viewmodel.
DepartmentView.xaml

EmployeeView.xaml

ProductView.xaml
In above three view bind view model properties to view.
TabPropertiesView.xaml
 <Grid x:Name="LayoutRoot"
          Background="White">
        <controls:SideMenuControl ItemsSource="{Binding Items}" />
    </Grid>

Place SideMenuControl.xaml  into UserControl by adding reference (i.e. xmlns:controls="clr-namespace:SideMenuTabControl.Controls") and bind itemsSource from ViewModel. 
 
Step 10 : Binding TabItes to TabControl into SideMenuControl.
SideMenuControl.xaml.cs 
  public partial class SideMenuControl : UserControl
    {
        #region ItemsSourceProperty
        public IEnumerable<HeaderViewModel> ItemsSource
        {
            get { return (IEnumerable<HeaderViewModel>)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register("ItemsSource"
            typeof(IEnumerable<HeaderViewModel>), 
            typeof(SideMenuControl),
            new PropertyMetadata(null, new PropertyChangedCallback(ItemsSourceChanged)));

        public static void ItemsSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            SideMenuControl obj = sender as SideMenuControl;
            if (obj != null)
            {
                obj.OnItemsSourceChanged(e);
            }
        }

        private void OnItemsSourceChanged(DependencyPropertyChangedEventArgs e)
        {
            MenuItemsTabControl.ItemsSource = BuildItemsSource(ItemsSource);
            MenuItemsTabControl.SelectedIndex = 0;
        }

        private IEnumerable BuildItemsSource(IEnumerable<HeaderViewModel> ItemsSource)
        {
            Dictionary<Type, UserControl> views = new Dictionary<Type, UserControl>() { 
                { typeof(EmployeeViewModel), new EmployeeView() },
                { typeof(DepartmentViewModel), new DepartmentView() },
                { typeof(ProductViewModel), new ProductView() }
            };

            foreach (var item in ItemsSource)
            {
                TabItem tabItem = new TabItem();
                if (views.ContainsKey(item.GetType()))
                {
                    tabItem.Header = item.Header;
                    UserControl view = views[item.GetType()];
                    view.DataContext = item;
                    tabItem.Content = view;
                }
                yield return tabItem;
            }
        }
        #endregion
        public SideMenuControl()
        {
            InitializeComponent();
            MenuItemsTabControl.SizeChanged += new SizeChangedEventHandler(MenuItemsTabControl_SizeChanged);          
        }
        void MenuItemsTabControl_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (ItemsSource == null)
                return;
            Int32 itemCount = ItemsSource.Count();
            if (itemCount >= 2)
            {
                Int32 selectedIndex = MenuItemsTabControl.SelectedIndex;
                MenuItemsTabControl.SelectedIndex = itemCount - 1;
                MenuItemsTabControl.SelectedIndex = selectedIndex;
            }
        }
    } 
 
In above code I have created DepenencyProperty fot itemsSource which type is Collection of IEnumerable. 
  • IEnumerable 
    .NET framework provides IEnumerable and IEnumerator interfaces to implement collection like behavior to user defined classes. A developer can implement these interfaces to provide collection like behavior to their classes.
    The IEnumerable interface contains an abstract member function called GetEnumerator() and return an interface IEnumerator on any success call. This IEnumerator interface will allow us to iterate through any custom collection. 
    The IEnumerable<T> interface is a generic interface that provides an abstraction for looping over elements.
    I have written BuildItemsSource method to return collection of viewmodels and its return type is IEnumerable.
I have used Yield keyword which return enumerator object.

  • Yield
    The yield keyword is used in an iterator block to provide a value to the enumerator object or to signal the end of the iteration. When used the expression is evaluated and returned as a value to the enumerator object.
    The yield keyword signals to the compiler that the method in which it appears is an iterator block. The compiler generates a class to implement the behavior that is expressed in the iterator block. In the iterator block, the yield keyword is used together with the return keyword to provide a value to the enumerator object. This is the value that is returned.
    A yield statement cannot appear in an anonymous method.
I have write SizeChange Event Handler for MenuItemsTabControl which provides user to maintain selected index of TabItems when user show/hide sidebar.

You can download Source from : TabControlWithSideBar

No comments:

Post a Comment