Introduction
In Silverlight, animations can be defined in XAML using the VisualStateManager
. However, there are some significant disadvantages to this technology, especially when attempting to create controls that can be reused. Also, it is best to attempt to keep the definition of the view either in code or XAML. Having it in both places makes maintenance harder. Ideally as much as possible should be done in XAML, which is the whole point of creating XAML instead of sticking to the technology of WinForms.
Background
The group I work for now has several people responsible for the design of the user interface. They have made extensive use buttons that consist only of an icon. These icons have behaviors associated with mouse over and pressed states. The initial icons I had to deal with were defined with a single path and there was only a single color change. This meant that the fill of the path had to change depending on the VisualStateManager
CommonStates
MouseOver
and Pressed
. The way that this had been solved originally was to create a control template for each different icon that served as a Button. This is not at all desirable since it means that many changes will require changing all or many the buttons. Then there could be both Button
and ToggleButton
controls using the same icon. My initial concept was a ControlTemplate
that took a Content
which was a string
that was the path value for a Path
control, and the Fill
was defined as the Foreground
:
<Style x:Key="PathButton" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid Background="Transparent">
<Path Name="ButtonPath"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Fill="{TemplateBinding Foreground}"
Data="{TemplateBinding Content}" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Pressed">
<Storyboard>
<ColorAnimation Duration="0"
Storyboard.TargetName="ButtonPath"
Storyboard.TargetProperty="(Shape.Fill)
.(SolidColorBrush.Color)"
To="{StaticResource AccentColor}" />
</Storyboard>
</VisualState>
<VisualState x:Name="MouseOver" />
<VisualState x:Name="Disabled" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
This initially looked like it was right solution, but it did not handle all the icon buttons.
The first recognized issue was when there was more than a single path. To handle this situation, one of my co-workers created a control that basically managed all the content in a ControlTemplate
for a Button
. Not only did it handle colors for multiple paths, but it was possible to state which paths would be affected, and also apply render transforms. It seemed to handle all the button behaviors that we needed.
I then ran into a case where there were multiple color changes, and the ControlTemplate
did not handle multiple colors. I could have probably extended the existing ControlTemplate
to handle multiple colors, but I did not like the concept. Basically we were providing another implementation of the VisualStateManager
. The VisualStateManager
is a standard in Silverlight the Silverlight developers understand. It also provides a very obvious definition of the defined behavior. Unfortunately, Silverlight does not let you just arbitrarily create DependencyProperties
for a control, not even the ContentlPresenter
. It seemed to me that it would be best to create a ContentControl
that contained the properties that I needed to create the desired behavior.
I therefore created a control that contained two new DependencyProperties
: Color1
and Color2
. When the control is initialized, or the content changes, then the controls in the Content are scanned using VisualTreeHelper.GetChild
method. Each control that is to be changed using the VisualStateManager
has a DependencyProperty
set that contains the string
that will indicate that the control (Shape
) fill color will be controlled by either Color1
or Color2
DependencyProperty
, and will be put in the List
associated with that Color
property.
The Color1
and Color2
DependencyProperties
each have a change event handler that will update all the controls in their associated list so that the fill is set to a SolidColorBrush
with the new color.
Using the Code
For anyone familiar with using the VisualStateManager
, the use of this control is actually very simple. Unfortunately, a ControlTemplate
has to be created to contain the VisualStateManager
XAML. In this case, I am using a control for a Button
:
<Style x:Key="TwoColorChangeButton" TargetType="Button">
<Setter Property="VerticalContentAlignment" Value="Top" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="TabNavigation" Value="Local" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0:0:0.1">
<VisualTransition.GeneratedEasingFunction>
<QuadraticEase EasingMode="EaseOut" />
</VisualTransition.GeneratedEasingFunction>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Normal" />
<VisualState x:Name="MouseOver">
<Storyboard>
<ColorAnimation Duration="0"
To="LightGreen" Storyboard
.TargetProperty="Color1"
Storyboard.TargetName="ContentControl" />
<ColorAnimation Duration="0"
To="Black" Storyboard.TargetProperty="Color2"
Storyboard.TargetName="ContentControl" />
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ColorAnimation Duration="0"
To="Red" Storyboard.TargetProperty="Color1"
Storyboard.TargetName="ContentControl" />
<ColorAnimation Duration="0" To="Green"
Storyboard.TargetProperty="Color2"
Storyboard.TargetName="ContentControl" />
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ColorAnimation Duration="0"
To="LightGray"
Storyboard.TargetProperty="Color1"
Storyboard.TargetName="ContentControl" />
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<views:ColorAdapterControl x:Name="ContentControl"
Color2="White"
Color1="Black"
Content="{TemplateBinding Content}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style><span style="margin: 0px; line-height: 107%; font-family: "Segoe UI", sans-serif; font-size: 10.5pt">Buttons </span>
You could directly code the paths within the content, but since this was part of a larger project I defined the paths in the Grid.Resources
:
<ControlTemplate x:Key="ButtonContent_Plus2" T
argetType="ContentControl">
<Canvas Width="12.1092"
Height="12.1096">
<Path Width="12.1092"
Height="12.1096"
Canvas.Left="0.390177"
Canvas.Top="0.391614"
Stretch="Fill"
views:ColorAdapterExt.FillColorSelector="Color1"
Data="F1 M 7.25134,0.446381C 10.5649,0.892731 12.8903,3.94067 12.4446,
7.25317C 11.9994,10.5666 8.95322,12.8927 5.63794,12.4463C 2.32419,
12.0019 0,8.95447 0.444702,5.64102C 0.890839,2.32654 3.93753,
0.000457764 7.25134,0.446381" />
<Path Width="5.63623"
Height="5.6366"
Canvas.Left="3.63184"
Canvas.Top="3.6283"
Stretch="Fill"
views:ColorAdapterExt.FillColorSelector="Color2"
Data="F1 M 6.99142,9.26489L 5.90842,9.26489L 5.90842,6.98758L 3.63184,
6.98758L 3.63184,5.90463L 5.90842,5.90463L 5.90842,3.6283L 6.99142,
3.6283L 6.99142,5.90463L 9.26807,5.90463L 9.26807,
6.98758L 6.99142,6.98758L 6.99142,9.26489 Z " />
</Canvas>
Then in the Silverlight page, all I need is the following XAML to create my button:
Hide Copy Code
<Button x:Name="SelectButton"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource TwoColorChangeButton}"
Background="Transparent">
<ContentControl Template="{StaticResource ButtonContent_Plus2}" />
<Button.RenderTransform>
<ScaleTransform ScaleX="4" ScaleY="4" />
</Button.RenderTransform>
</Button>
Points of Interest
I only used two colors and only changed the Fill
color of paths. The code is set up so that any Shape
can be used within the content. This technique can be used for any number of values on many different DependencyProperties
. One example may be controlling the height of a row or width of a column in a Grid
. These properties are of the type GridLength
, so cannot be controlled by an animation, but with an Adapter like this, it is possible.
History
- 5th May, 2011: Initial post