Professional, Software

Templatable Control: ProgressBar

 

Overview

Last May, I had created a ProgressBar control for Silverlight Alpha. It was about time to update that code. The control that I had created was pretty limited in the sense it was a user control and control consumer’s ability to change the look and feel of the control were next to none. Since Silverlight Beta1 supports templatable/stylable control, I wanted to update the code to take advantage of this functionality

Some of the concepts I end up using

  1. Control Templates
    1. Template Binding
    2. Parts/States
    3. Generic.xaml
    4. GetTemplateChild and OnApplyTemplate
    5. TemplatePart
  2. Styling
    1. Control Author: To set Default Values
    2. Control Consumer: For setting properties.
  3. Dependency Properties
    1. Registration
    2. PropertyChangedCallback

The basic look of ProgressBar is pretty easy and familiar. It has track and progress indicator and text to show the value.

basic

For the APIs, I really looked at WPF ProgressBar and try to be the subset of those APIs.

My Progressbar had following public APIs…

  1. Minimum: Minimum value for the range
  2. Maximum: Maximum value for the range
  3. BorderBrush: Border brush for the track
  4. Background: Background for the track
  5. IsIndeterminate: Bool to set the indeterminate state.

Build Control

Control has Parts and States. Parts are the elements in visual tree and State defines the way they look at any given time. Here are the basic steps…

ProgressBar Class Code
  1. Create a class that inherits from Control class
  2. Class Exposes DependencyProperties and Setter/Getters for the public APIs
  3. I needed to run logic when IsInDeterminate and (Value/Minimum/Maximum) is changed so I registered PropertyChangedCallsbacks for those properties
  4. The most important method is OnApplyTemplate. This method lets me get handles to the Parts in the control. This is where I get references to Parts such as Storyboards and then control logic defines when to start/stop those storyboards
  5. Class has TemplatePart attributes though they are not really used anywhere in my code. They are really meant for Tools so that they can build experience around bill of materials for those parts.
   1: using System;
   2: using System.Windows;
   3: using System.Windows.Controls;
   4: using System.Windows.Documents;
   5: using System.Windows.Ink;
   6: using System.Windows.Input;
   7: using System.Windows.Media;
   8: using System.Windows.Media.Animation;
   9: using System.Windows.Shapes;
  10: using System.ComponentModel;
  11: 
  12: namespace MyControls
  13: {
  14:     /// <summary>
  15:     /// Metadata to be used by tools
  16:     /// </summary>
  17:     [TemplatePart(Name = "PART_Indicator", Type = typeof(FrameworkElement))]
  18:     [TemplatePart(Name = "PART_Value", Type = typeof(TextBlock))]
  19:     [TemplatePart(Name = "STATE_InDeterminate", Type = typeof(Storyboard))]
  20:     [TemplatePart(Name = "STATE_InProgress", Type = typeof(Storyboard))]
  21: 
  22:     public class ProgressBar:Control
  23:     {
  24:         #region Public Methods
  25: 
  26:         //sets the border brush for the track
  27:         public Brush BorderBrush
  28:         {
  29:             get { return (Brush)GetValue(ProgressBar.BorderBrushProperty); }
  30:             set { SetValue(ProgressBar.BorderBrushProperty, value); }
  31:         }
  32:         public static readonly DependencyProperty BorderBrushProperty = DependencyProperty.Register("BorderBrush", typeof(Brush), typeof(MyControls.ProgressBar), null);
  33: 
  34:         //sets the background brush for the track
  35:         public Brush Background
  36:         {
  37:             get { return (Brush)GetValue(ProgressBar.BackgroundProperty); }
  38:             set { SetValue(ProgressBar.BackgroundProperty, value); }
  39:         }
  40:         public static readonly DependencyProperty BackgroundProperty = DependencyProperty.Register("Background", typeof(Brush), typeof(MyControls.ProgressBar), null);
  41: 
  42:         //sets the value 
  43:         public Double Value
  44:         {
  45:             get { return (Double)GetValue(ProgressBar.ValueProperty); }
  46:             set { SetValue(ProgressBar.ValueProperty, value); }
  47:         }
  48:         public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(Double), typeof(MyControls.ProgressBar), new PropertyChangedCallback(Recalculate));
  49: 
  50:         //sets the minimum value
  51:         public Double Minimum
  52:         {
  53:             get { return (Double)GetValue(ProgressBar.MinimumProperty); }
  54:             set { SetValue(ProgressBar.ValueProperty, value); }
  55:         }
  56:         public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register("Minimum", typeof(Double), typeof(MyControls.ProgressBar), new PropertyChangedCallback(Recalculate));
  57: 
  58:         //sets the maximum value
  59:         public Double Maximum
  60:         {
  61:             get { return (Double)GetValue(ProgressBar.MaximumProperty); }
  62:             set { SetValue(ProgressBar.ValueProperty, value); }
  63:         }
  64:         public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register("Maximum", typeof(Double), typeof(MyControls.ProgressBar), new PropertyChangedCallback(Recalculate));
  65: 
  66:         //sets the bool for InDeterminate state
  67:         public bool IsInDeterminate
  68:         {
  69:             get { return (bool)GetValue(ProgressBar.IsInDeterminateProperty); }
  70:             set { SetValue(ProgressBar.IsInDeterminateProperty, value); }
  71:         }
  72:         public static readonly DependencyProperty IsInDeterminateProperty = DependencyProperty.Register("IsInDeterminate", typeof(bool), typeof(MyControls.ProgressBar), new PropertyChangedCallback(OnIsInDeterminatePropertyChanged));
  73: 
  74:         #endregion
  75: 
  76:         # region Private Methods
  77:         private FrameworkElement PART_Indicator;
  78:         private Storyboard STATE_InDeterminate;
  79:         private Storyboard STATE_InProgress;
  80:         private TextBlock PART_Value;
  81: 
  82:         //When IsInDeterminate is set to true, progress bar is in "Forever" animation mode
  83:         private static void OnIsInDeterminatePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
  84:         {
  85:             ProgressBar p = (ProgressBar)obj;
  86:             if (args.Property == IsInDeterminateProperty && p.STATE_InDeterminate != null)
  87:             {
  88:                 bool oldval = (bool)args.OldValue;
  89:                 bool newval = (bool)args.NewValue;
  90: 
  91:                 if (newval == true && oldval == false)
  92:                 {
  93:                     p.Value = p.Maximum;
  94:                     p.STATE_InProgress.Stop();
  95:                     p.STATE_InDeterminate.Begin();
  96:                 }
  97: 
  98:                 if (newval == false && oldval == true)
  99:                 {
 100:                     p.Value = p.Value;
 101:                     p.STATE_InDeterminate.Stop();
 102:                     p.STATE_InProgress.Begin();
 103:                 }
 104:             }
 105:         }
 106: 
 107:         //reacalculates the width whenever Min, Max or Value changes
 108:         private static void Recalculate(DependencyObject obj, DependencyPropertyChangedEventArgs args)
 109:         {
 110:             ProgressBar p = (ProgressBar)obj;
 111:             if (args.Property == MinimumProperty || args.Property == MaximumProperty || args.Property == ValueProperty)
 112:             {
 113:                 if (p.Minimum < p.Maximum && p.Value <= p.Maximum && p.Value >= p.Minimum && p.PART_Indicator != null)
 114:                 {
 115:                     p.PART_Indicator.Width = p.Value * p.Width / (p.Maximum - p.Minimum);
 116:                     p.PART_Value.Text = p.Value.ToString();
 117:                 }
 118:             }
 119: 
 120:         }
 121: 
 122:         #endregion
 123: 
 124:         #region Protected Methods
 125: 
 126:         //Gets handles to all the template part
 127:         protected override void OnApplyTemplate()
 128:         {
 129:             PART_Indicator = GetTemplateChild("PART_Indicator") as FrameworkElement;
 130:             STATE_InDeterminate = GetTemplateChild("STATE_InDeterminate") as Storyboard;
 131:             STATE_InProgress = GetTemplateChild("STATE_InProgress") as Storyboard;
 132:             PART_Value = GetTemplateChild("PART_Value") as TextBlock;
 133:             //since OnApplyTemplate gets called at layout time, Property change notification fire
 134:             //for the first change before this gets called.
 135:             if (IsInDeterminate == true)
 136:             {
 137:                 Value = Maximum;
 138:                 STATE_InDeterminate.Begin();
 139:             }
 140:             else
 141:             {
 142:                 STATE_InProgress.Begin();
 143:             }
 144: 
 145: 
 146:             if (Minimum < Maximum && Value <= Maximum && Value >= Minimum)
 147:             {
 148:                 PART_Indicator.Width = (Value * Width) / (Maximum - Minimum);
 149:             }
 150:             else
 151:             {
 152:                 if(Value < Minimum)
 153:                     Value = Minimum;
 154:                 if (Value > Maximum)
 155:                     Value = Maximum;
 156:             }
 157:             PART_Value.Text = Value.ToString();
 158:         }
 159: 
 160:         #endregion
 161: 
 162:     }
 163: }
Generic.Xaml

Next step is to define the default look of the control. This is defining a ResourceDictionary where Style tag is used. This ResourceDictionary must live inside a file named Generic.xaml. Platform actually looks for that file.

  1.  
    1. Create a ResourceDictionary that defines the template of the control
    2. This dictionary has a Style tag with TargetType = ProgressBar
    3. One of the setters sets the Template Property for the control
      1. Template has actual Visual tree.
      2. It also as Storyboards that define what happens in different states.
    4. Some other setters define the default values for those properties.
   1: <ResourceDictionary
   2:         xmlns="http://schemas.microsoft.com/client/2007"
   3:         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:         xmlns:local="clr-namespace:MyControls;assembly=MyControls"
   5:         >
   6:     <!--Progressbar Template-->
   7:     <Style TargetType="local:ProgressBar">
   8:         <Setter Property="Template">
   9:             <Setter.Value>
  10:                 <ControlTemplate TargetType="local:ProgressBar">
  11:                     <Grid ShowGridLines="False" Name="LayoutRoot">
  12:                         <Grid.Resources>
  13:                             <Storyboard x:Name="STATE_InDeterminate"
  14:                                         AutoReverse="False"
  15:                                         RepeatBehavior="Forever">
  16:                                 <DoubleAnimationUsingKeyFrames
  17:                                     Storyboard.TargetName="PART_Indicator"
  18:                                     Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)">
  19:                                     <SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
  20:                                     <SplineDoubleKeyFrame KeyTime="00:00:05" Value="1"/>
  21:                                 </DoubleAnimationUsingKeyFrames>
  22:                                 <PointAnimationUsingKeyFrames Storyboard.TargetName="PART_Indicator" Storyboard.TargetProperty="(Border.Background).(LinearGradientBrush.StartPoint)">
  23:                                     <SplinePointKeyFrame KeyTime="00:00:01" Value="0.503,-0.42"/>
  24:                                     <SplinePointKeyFrame KeyTime="00:00:02" Value="0.497,-0.076"/>
  25:                                     <SplinePointKeyFrame KeyTime="00:00:03" Value="0.497,0.35"/>
  26:                                     <SplinePointKeyFrame KeyTime="00:00:04" Value="0.494,-0.036"/>
  27:                                 </PointAnimationUsingKeyFrames>
  28:                                 <PointAnimationUsingKeyFrames
  29:                                     Storyboard.TargetName="PART_Indicator"
  30:                                     Storyboard.TargetProperty="(Border.Background).(LinearGradientBrush.EndPoint)">
  31:                                     <SplinePointKeyFrame KeyTime="00:00:01" Value="0.503,0.58"/>
  32:                                     <SplinePointKeyFrame KeyTime="00:00:02" Value="0.497,0.92"/>
  33:                                     <SplinePointKeyFrame KeyTime="00:00:03" Value="0.497,1.345"/>
  34:                                     <SplinePointKeyFrame KeyTime="00:00:04" Value="0.494,0.96"/>
  35:                                 </PointAnimationUsingKeyFrames>
  36:                                 <DoubleAnimationUsingKeyFrames
  37:                                     Storyboard.TargetName="PART_Value"
  38:                                     Storyboard.TargetProperty="(UIElement.Opacity)">
  39:                                     <SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
  40:                                 </DoubleAnimationUsingKeyFrames>
  41:                             </Storyboard>
  42:                             <LinearGradientBrush x:Key="ProgressBrush" EndPoint="0.5,1" StartPoint="0.5,0">
  43:                                 <GradientStop Color="#FF8FB8EA"/>
  44:                                 <GradientStop Color="#FF8FB8EA" Offset="1"/>
  45:                                 <GradientStop Color="#FF1970B9" Offset="0.5220000147819519"/>
  46:                             </LinearGradientBrush>
  47:                             <Storyboard x:Name="STATE_InProgress" RepeatBehavior="Forever">
  48:                                 <PointAnimationUsingKeyFrames
  49:                                     Storyboard.TargetName="PART_Indicator"
  50:                                     Storyboard.TargetProperty="(Border.Background).(LinearGradientBrush.StartPoint)">
  51:                                     <SplinePointKeyFrame KeyTime="00:00:01" Value="0.503,-0.42"/>
  52:                                     <SplinePointKeyFrame KeyTime="00:00:02" Value="0.497,-0.076"/>
  53:                                     <SplinePointKeyFrame KeyTime="00:00:03" Value="0.497,0.35"/>
  54:                                     <SplinePointKeyFrame KeyTime="00:00:04" Value="0.494,-0.036"/>
  55:                                 </PointAnimationUsingKeyFrames>
  56:                                 <PointAnimationUsingKeyFrames
  57:                                     Storyboard.TargetName="PART_Indicator"
  58:                                     Storyboard.TargetProperty="(Border.Background).(LinearGradientBrush.EndPoint)">
  59:                                     <SplinePointKeyFrame KeyTime="00:00:01" Value="0.503,0.58"/>
  60:                                     <SplinePointKeyFrame KeyTime="00:00:02" Value="0.497,0.92"/>
  61:                                     <SplinePointKeyFrame KeyTime="00:00:03" Value="0.497,1.345"/>
  62:                                     <SplinePointKeyFrame KeyTime="00:00:04" Value="0.494,0.96"/>
  63:                                 </PointAnimationUsingKeyFrames>
  64:                                 <DoubleAnimationUsingKeyFrames
  65:                                     Storyboard.TargetName="PART_Value"
  66:                                     Storyboard.TargetProperty="(UIElement.Opacity)">
  67:                                     <SplineDoubleKeyFrame KeyTime="00:00:00" Value="100"/>
  68:                                 </DoubleAnimationUsingKeyFrames>
  69:                             </Storyboard>
  70:                         </Grid.Resources>
  71:                         <Grid.RowDefinitions>
  72:                             <RowDefinition Height="{TemplateBinding Height}"/>
  73:                         </Grid.RowDefinitions>
  74:                         <Grid.ColumnDefinitions>
  75:                             <ColumnDefinition Width="{TemplateBinding Width}"/>
  76:                         </Grid.ColumnDefinitions>
  77:                         <Border
  78:                             Name="PART_Track"
  79:                             CornerRadius="2"
  80:                             Background="{TemplateBinding Background}"
  81:                             BorderBrush="{TemplateBinding BorderBrush}"
  82:                             BorderThickness="2,2,2,2"
  83:                             MinWidth="{TemplateBinding Width}">
  84:                             <Border
  85:                                 x:Name="PART_Indicator"
  86:                                 CornerRadius="2"
  87:                                 HorizontalAlignment="Left"
  88:                                 RenderTransformOrigin="0,0"
  89:                                 BorderBrush="#FF000000"
  90:                                 Background="{StaticResource ProgressBrush}">
  91:                                 <Border.RenderTransform>
  92:                                     <TransformGroup>
  93:                                         <ScaleTransform/>
  94:                                         <SkewTransform/>
  95:                                         <RotateTransform/>
  96:                                         <TranslateTransform/>
  97:                                     </TransformGroup>
  98:                                 </Border.RenderTransform>
  99:                             </Border>
 100:                         </Border>
 101:                         <TextBlock
 102:                             HorizontalAlignment="Center"
 103:                             VerticalAlignment="Center"
 104:                             Name="PART_Value"/>
 105:                     </Grid>
 106:                 </ControlTemplate>
 107:             </Setter.Value>
 108:         </Setter>
 109:         <Setter Property="Width" Value="200"/>
 110:         <Setter Property="Height" Value="30"/>
 111:         <Setter Property="Minimum" Value="0"/>
 112:         <Setter Property="Maximum" Value="100"/>
 113:         <Setter Property="Value" Value="0"/>
 114:         <Setter Property="IsInDeterminate" Value="False"/>
 115:         <Setter Property="Background" Value="#FFFFFFFF"/>
 116:         <Setter Property="BorderBrush" Value="#FF000000"/>
 117:     </Style>
 118: </ResourceDictionary>

Control Usage

Page.xaml

To be able to use this control in my Application, all I had to was declare the custom xmlns and use the control. I have Styled some of the values and use the StaticResource to set the style for the control instance.

   1: <UserControl x:Class="ControlHost.Page"
   2:     xmlns="http://schemas.microsoft.com/client/2007"
   3:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:     xmlns:control="clr-namespace:MyControls;assembly=MyControls"
   5:     xmlns:app="clr-namespace:ControlHost;assembly=ControlHost">
   6:     <UserControl.Resources>
   7:         <Style x:Key="ProgressBarStyle" TargetType="control:ProgressBar">
   8:             <Setter Property="Width" Value="400"/>
   9:             <Setter Property="Height" Value="40"/>
  10:             <Setter Property="Value" Value="10"/>
  11:             <Setter Property="IsInDeterminate" Value="True"/>
  12:             <Setter Property="Minimum" Value="0"/>
  13:             <Setter Property="Maximum" Value="100"/>
  14:         </Style>
  15:     </UserControl.Resources>
  16: 
  17:     <Grid Name="LayoutRoot" MouseLeftButtonDown="Grid_MouseLeftButtonDown">
  18:         <Grid.RowDefinitions>
  19:             <RowDefinition Height="500"/>
  20:             <RowDefinition Height="500"/>
  21:         </Grid.RowDefinitions>
  22:         <Grid.ColumnDefinitions>
  23:             <ColumnDefinition Width="500"/>
  24:             <ColumnDefinition Width="500"/>
  25:         </Grid.ColumnDefinitions>
  26: 
  27:         <control:ProgressBar x:Name="progress"
  28:                              Style="{StaticResource ProgressBarStyle}"
  29:                              Margin="10,10,10,10"
  30:                              Grid.Row="1" Grid.Column="1"
  31:                              VerticalAlignment="Top"/>
  32:     </Grid>
  33: 
  34: </UserControl>
Page.xaml.cs

To be able to test here is the code I had in my app

   1: using System;
   2: using System.Windows;
   3: using System.Windows.Controls;
   4: using System.Windows.Input;
   5: using System.Windows.Threading;
   6: 
   7: namespace ControlHost
   8: {
   9:     public partial class Page : UserControl
  10:     {
  11:         int i = 0;
  12:         DispatcherTimer timer;
  13:         public Page()
  14:         {
  15:             InitializeComponent();
  16:             timer = new DispatcherTimer();
  17:             timer.Interval = new TimeSpan(0, 0, 1);
  18:             timer.Tick += new EventHandler(timer_Tick);
  19:         }
  20: 
  21:         void timer_Tick(object sender, EventArgs e)
  22:         {
  23:             if (i < 100)
  24:                 i =  i + 10;
  25:             else
  26:                 i = 0;
  27:             progress.Value = i;
  28:         }
  29: 
  30:         private void Grid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
  31:         {
  32:             if (progress.IsInDeterminate == true)
  33:             {
  34:                 progress.IsInDeterminate = false;
  35:                 timer.Start();
  36:             }
  37:             else
  38:             {
  39:                 progress.Value = 0;
  40:                 progress.IsInDeterminate = true;
  41:                 timer.Stop();
  42:             }
  43:         }
  44:     }
  45: }

 

Few things I learned during this exercise are…

  1. Parts should be expressed in terms of properties of control as much as possilble (so track part does not define Width instead it uses TemplateBinding to bind to Width property of control
  2. Don’t use Named Parts in Resources section. This is bad because Resources are not in Visual tree and hence they are not TemplateBindable. I hit this issue when the my first apporach to build the animation for indeterminate state involved animating the width. It meant final value of the animation needed to be width of the control. Thought it was possible for me to hook into SizeChanged event but it just made customizing template very hard. Instead I tried to figure out a way to express that animation in terms of scale transforms.
  3. TemplateBinding does not use TypeConverter so to be able to show the val
  4. ue property of the control in the textblock, I had to write control, I could not use {TemplateBinding Value} where is Value is of type double for the Text property which is of type string.
  5. Part type should be as high as possible. For all Border types, I ended up using FrameworkElement in Code for reference. That is all  I really needed. That lets template write use any other class.

Here is the link to working project.

Advertisements
Standard

One thought on “Templatable Control: ProgressBar

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s