Introduction
In this earlier blog, I looked at how to create a non-rectangular ProgressBar. As the next step, I want to look at how you can change properties dynamically as the current Value of the ProgressBar changes.
I'll start with the easy (and fairly realistic) scenario where you want to tone down the ProgressBar once it has reached its maximum value.
Currently the ProgressBar coloring looks like this:

If you want to apply changes to the look of the ProgressBar as it is running, you can use a DataTrigger in XAML or you can simply use the ValueChanged event of the ProgressBar in the code-behind. I say "simply", but you do in fact have to understand how to dig into the ControlTemplate and drill down in order to make the code-behind approach work.
There's a trade-off between these two approaches – DataTrigger or code-behind. DataTrigger offers a slightly more concise syntax, but only as long as your requirements are very basic. Once you step beyond the absolute basics, the XAML becomes quite complex. And as you are no doubt more familiar with VB, what's the point in struggling just to prove the point that it can be created in XAML? Sometimes, common sense has to win over technical ego.
Overall, in most cases, code-behind is the easiest answer. But just to be sure you know what a DataTrigger is and how it works, let's run through it.
DataTrigger
A DataTrigger is fired (and as a result some predefined change happens) when a value is reached. In the scenario we are looking at here, a DataTrigger could be set to fire when the Value property of a ProgressBar reaches its maximum. So in the case of the ProgressBar defined below that would of course be when it reaches 100.
<ProgressBar x:Name="CurvyPB" Width="300" Height="60"
Template="{StaticResource PBCurvy}"
Foreground="{StaticResource BlueGreenRed}"
Minimum="0" Maximum="100" />
The 'predefined change' might be to alter the color of the outside edge of the control and also to dim its Opacity, so that it can be more easily ignored now that its work is done.
The DataTrigger sits in a Triggers collection in the ControlTemplate for the ProgressBar. The syntax is as follows:
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=Value}"
Value="100">
<Setter TargetName="PART_Track" Property="Stroke" Value="DarkGray" />
<Setter TargetName="PART_Track" Property="Opacity" Value="0.3" />
</DataTrigger>
</ControlTemplate.Triggers>
(Note that this is only one section of the complete ControlTemplate.)
The DataTrigger Binding looks awkward and is one of those rather peculiar constructs that makes XAML hard to decipher (and create) sometimes. Essentially it translates to :
"Keep checking the Value property of the ProgressBar instance that is using this ControlTemplate. If and when it reaches a Value of 100, fire the trigger."
The Setters both target the Path named 'PART_Track'. The first changes the Stroke to DarkGray and the second one turns down the Opacity of that Path (which effectively reduces the Opacity of the templated ProgressBar).
Here is the full XAML for the Window which contains the ControlTemplate and the ProgressBar instance:
<Window x:Class="CurvyWithTriggers"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converter="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Luna"
Title="Curved ProgressBar" Height="200" Width="400">
<Window.Resources>
<ControlTemplate x:Key="PBCurvy" TargetType="{x:Type ProgressBar}">
<Grid>
<Path x:Name="PART_Track"
Stroke="{StaticResource BlueGreenRed}"
StrokeThickness="5"
Data="F1 M46.802502,0.50000018 C59.803562,0.50000006 71.553123,3.7052743 79.942001,
8.9014616 C88.330879,3.7052746 100.08044,0.5 113.0815,0.50000018 C125.92575,
0.5 137.54851,3.6284194 145.9305,8.6908474 C154.3125,3.6284194 165.93524,
0.50000006 178.7795,0.50000018 C204.35167,0.5 225.082,12.900593 225.082,
28.1975 C225.082,43.494408 204.35167,55.895 178.7795,55.895 C165.93524,
55.895 154.3125,52.766582 145.9305,47.704151 C137.54851,52.766582 125.92575,
55.895 113.0815,55.895 C100.08044,55.895 88.330879,52.689728 79.942001,
47.493538 C71.553123,52.689728 59.803562,55.895 46.802502,55.895 21.230335,
55.895 0.5,43.494408 0.5,28.1975 0.5,12.900593 21.230335,0.5 46.802502,
0.50000018 z"
Stretch="Fill">
<Path.Fill>
<MultiBinding>
<MultiBinding.Converter>
<converter:ProgressBarBrushConverter />
</MultiBinding.Converter>
<Binding Path="Foreground" RelativeSource="{RelativeSource TemplatedParent}" />
<Binding Path="IsIndeterminate" RelativeSource="{RelativeSource TemplatedParent}" />
<Binding Path="ActualWidth" ElementName="PART_Indicator" />
<Binding Path="ActualHeight" ElementName="PART_Indicator" />
<Binding Path="ActualWidth" ElementName="PART_Track" />
</MultiBinding>
</Path.Fill>
</Path>
<Decorator x:Name="PART_Indicator" />
</Grid>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=Value}"
Value="100">
<Setter TargetName="PART_Track" Property="Stroke" Value="DarkGray" />
<Setter TargetName="PART_Track" Property="Opacity" Value="0.3" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Window.Resources>
<Grid>
<ProgressBar x:Name="CurvyPB" Width="300" Height="60"
Template="{StaticResource PBCurvy}"
Foreground="{StaticResource BlueGreenRed}"
Minimum="0" Maximum="100" />
</Grid>
</Window>
This markup in the Application.xaml file creates the LinearGradientBrush:
<Application.Resources>
<LinearGradientBrush x:Key="BlueGreenRed"
EndPoint="1,0.5" StartPoint="0,0.5">
<GradientStop Color="#FF2D3ADD" Offset="0"/>
<GradientStop Color="#FFF13E14" Offset="1"/>
<GradientStop Color="#FF9775D8" Offset="0.192"/>
<GradientStop Color="#FF3F893B" Offset="0.481"/>
<GradientStop Color="#FF2B9518" Offset="0.625"/>
<GradientStop Color="#FECC7638" Offset="0.812"/>
</LinearGradientBrush>
</Application.Resources>
The code-behind to animate the ProgressBar:
Imports System.Windows.Media.Animation
Partial Public Class CurvyWithTriggers
Private Sub CurvyWithTriggers_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
Dim a As New DoubleAnimation
a.From = 0
a.To = 100
a.Duration = New TimeSpan(0, 0,
CurvyPB.BeginAnimation(ProgressBar.ValueProperty, a)
End Sub
End Class
Apologies if you already have all that code and markup from the previous blog. Personally I hate it when someone says "I've used the same code as in my earlier blog", which means I then have to go and find that blog, dig through it and find the missing bits before I can test out what I'm working on now. So I much prefer to repeat it and make life easier for you, at the expense of a slightly longer blog entry.
After the ProgressBar completes its mission, it hits that Value of 100 and the look changes to this:

So if you're sat there saying to yourself that there's nothing there that you couldn't have easily done in code-behind, I almost agree with you. Certainly the Opacity change would be easy:
If progressBar1.Value = 100 Then
progressBar1.Opacity = 0.3
End If
But what about changing the value of the Stroke property though? The ProgressBar doesn't have a Stroke property. That's tucked away inside the ControlTemplate and is a property of the Path named PART_Track. To get to that, we will need a way to access the ControlTemplate and then drill down into the Path.
Before we look at the code-behind approach, I just want to mention a problem with DataTriggers, especially as they relate to ProgressBars. The only arithmetic operator available to you in the DataTrigger in XAML is the equals operator. For fairly obvious reasons, the less-than and greater-then operator symbols have the potential to cause problems in a language that uses them as element delimiters.
The reason that this is a particular problem with the ProgressBar is that the algorithm that breaks the ProgressBar movement into its time chunks will rarely, if ever, space them across whole numbers. That is to say if the Minimum Value is 0 and the Maximum Value is 100 and the duration is 10 seconds, you might reasonably suppose that each new block will appear at intervals of 0, 10, 20, 30, etc. However it doesn't work that way and the values are more likely to be something like :
0
0.252538
0.299015
0.613981
: etc
: ending with
99.769844
99.890324
100
This makes it impossible for you to set a DataTrigger on a value that you can be certain will be matched exactly, except for the starting and ending values of 0 and 100. In theory, you could tweak the size settings of the PART_Track and PART_Indicator to ensure whole number partitions (these being the key factors in the breakdown), but none of my experimenting with this approach worked. An alternative approach, which I haven't tried, would be to create a ValueConverter which would then allow you to set value parameters, such as < 20 or > 40 and so on. However, I didn't really see any gain in going that route, as I can use the ValueChanged event of the ProgressBar directly, together with a bit of WPF delving, as we will now see.
FindResource and FindName
When you need to get at a ControlTemplate (or other Resource, for that matter), you can use the FindResource method of the FrameworkElement class. Before, I get into that, I am going to make two changes to the markup in the Window.
The first change is to add a Name property to the Window, placing this inside the opening tag of the Window class markup:
x:Name="CurvyValueChangedWindow"
This is necessary in order for the Window to be accessed from the code-behind.
The second change is purely decorative – changing the Stroke property of the PART_Track path from the LinearGradientBrush to plain Yellow.
<Path x:Name="PART_Track"
Stroke="Yellow"
The ProgressBar now looks like this at startup:

To locate that ControlTemplate, I use the following code, which I have placed in the ValueChanged event of the ProgressBar:
Dim ct As New ControlTemplate()
Try
ct = CType(CurvyValueChangedWindow.FindResource("PBCurvy"), ControlTemplate)
Catch Ex As ResourceReferenceKeyNotFoundException
' Design time message
Console.WriteLine("CT Not found")
' Quit gracefully if not found
Exit Sub
End Try
The third line does all the work and you will see now why I added a name to the WPF Window. This Name is used to identify the container in which the Resource named PBCurvy (i.e. the ControlTemplate) should be found.
Note also that if due to a typo or other error the Resource can't be found, then no further action will be taken. At this stage, the Catch is superfluous. It is however important to avoid the application crashing as we move on to the next step.
FindName
So now we have got ourselves a reference to the ControlTemplate, but we still need to drill down into the Path named PART_Track which is a sub-element of that template. To do this, you can use the following code, placing it immediately below the code used to find the ControlTemplate resource:
Dim p As New Path
p = CType(ct.FindName("PART_Track", PBCurvy), Path)
If IsNothing(p) Then
Console.WriteLine("Path Not found")
Exit Sub
End If
This time the FindName function searches through that ControlTemplate (now referenced as 'ct') in order to find the named Path.
Once again, the test for IsNothing isn't necessary at this stage, because we have not yet tried to do anything with the Path variable 'p'. As before though, when we add further code, this checkpoint – and the Exit Sub if the Path isn't found – are important.
Changing The values
We've now reached the point where we can manipulate values of that Path according to the current values of the ProgressBar. This code placed immediately below the previous snippet will change the color and thickness of the Stroke:
Select Case PBCurvy.Value
Case Is < 25
p.Stroke = New SolidColorBrush(Colors.Yellow)
p.StrokeThickness = 5
Case 25 To 49
p.Stroke = New SolidColorBrush(Colors.Green)
p.StrokeThickness = 7
Case 50 To 75
p.Stroke = New SolidColorBrush(Colors.Orange)
p.StrokeThickness = 9
Case 76 To 99
p.Stroke = New SolidColorBrush(Colors.Red)
p.StrokeThickness = 11
Case Else
p.Stroke = New SolidColorBrush(Colors.DarkGray)
End Select
When you run the application, the colors will change as things progress:



I have chosen to make those particular changes for demo purposes, but you are of course not limited to those. The key take away points in this blog are the availability of the DataTrigger and the use of the very helpful FindResource and FindName methods when you want to trigger a change via the code-behind.