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
- Control Templates
- Template Binding
- Parts/States
- Generic.xaml
- GetTemplateChild and OnApplyTemplate
- TemplatePart
- Styling
- Control Author: To set Default Values
- Control Consumer: For setting properties.
- Dependency Properties
The basic look of ProgressBar is pretty easy and familiar. It has track and progress indicator and text to show the value.
For the APIs, I really looked at WPF ProgressBar and try to be the subset of those APIs.
My Progressbar had following public APIs…
- Minimum: Minimum value for the range
- Maximum: Maximum value for the range
- BorderBrush: Border brush for the track
- Background: Background for the track
- 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
- Create a class that inherits from Control class
- Class Exposes DependencyProperties and Setter/Getters for the public APIs
- I needed to run logic when IsInDeterminate and (Value/Minimum/Maximum) is changed so I registered PropertyChangedCallsbacks for those properties
- 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
- 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.
-
- Create a ResourceDictionary that defines the template of the control
- This dictionary has a Style tag with TargetType = ProgressBar
- One of the setters sets the Template Property for the control
- Template has actual Visual tree.
- It also as Storyboards that define what happens in different states.
- 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…
- 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
- 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.
- TemplateBinding does not use TypeConverter so to be able to show the val
- 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.
- 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.