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.
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.
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
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.