Freigeben über


Why can't I change the Background of my Button on a Click event?

I've been meaning to post this for a while, since the question comes up from time to time (internally and externally)

The typical problem is that you have some XAML like this:

<Button Content="Click Me" x:Name="ClickMe" Click="ClickMe_Click" Background="Red"/>

and some code-behind like this:

private void ClickMe_Click(object sender, RoutedEventArgs e)
{
  (sender as Button).Background = new SolidColorBrush(Colors.Green);
}

But when you click the button, it briefly turns White and then goes back to Red... what happened to the Green value? You might even try to set it to Green at some other point in time - say on the Click of another button - and it works great. So why doesn't it work in this particular case?

The answer is that controls in Silverlight use the VisualStateManager to show different visual states (like Normal, Pressed, and so forth). Under the covers, Silverlight creates animations for each visual state transition, and as noted here the currently-animated value of a property always trumps the base value.

So what happens is that your button is Red, and when you press it the VSM kicks in and generates some animations. The current state is Normal and the colour is Red, and it needs to animate it to White for the Pressed state (this is set in the control template). It also needs to generate the reverse animation (from Pressed back to Normal) and so it generates an animation from White back to Red. When you lift your finger the VSM runs the animation to go back to the Normal state, using the "remembered" value of Red. Even though you have set the underlying property value to Green, the animation from the VSM is going to always go back to Red.

"But why doesn't this happen in desktop Silverlight?" you might ask. Good question. The underlying problem exists in desktop Silverlight, but the desktop control templates are much more complex than the simple templates used for the phone. On the desktop, there are all sorts of bells and whistles to give a Windows-ish gradient look with a focus state and so on, and the animations used in the VSM all affect elements that aren't directly tied to the Background property (eg, the gradient you see is actually an overlay with a linear opacity change).

On the phone, we have very simple templates that both match the Windows Phone look-and-feel and are more efficient. This means that the template directly uses the Background property and you can get caught up in this bug. The good news is that there is a simple solution for when you need to do this - you can re-template the button to use more layers, just like the desktop. (The default templates don't do this because then every button in every application would pay a performance penalty for a theoretical feature that most people will never use... and that means the folks that actually need this feature need to do a bit more work).

Here's an example of a button with a template that gives the desired result; the new or modified bits are highlighted:

<Button Content="Click Me" x:Name="ClickMe" Click="ClickMe_Click" Background="Red">
<Button.Template>
<ControlTemplate TargetType="Button">
<Grid Background="Transparent">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver"/>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentContainer"
Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneBackgroundBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PressedHighlightBackground"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneForegroundBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonBackground"
Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneForegroundBrush}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentContainer"
Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneDisabledBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonBackground"
Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneDisabledBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonBackground"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="Transparent" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border x:Name="ButtonBackground" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="0"
Background="{TemplateBinding Background}" Margin="{StaticResource PhoneTouchTargetOverhang}" >
<Border x:Name="PressedHighlightBackground" Background="Transparent">
<ContentControl x:Name="ContentContainer" Foreground="{TemplateBinding Foreground}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Padding="{TemplateBinding Padding}"
Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}"/>
</Border>
</Border>
</Grid>
</ControlTemplate>
</Button.Template>
</Button>

There are still some caveats with this approach:

  1. You will want to update the template to match the final templates we ship in the RTM tools so that you match the platform as close as possible (the template above should be pretty close, but you never know)
  2. The Disabled state will still trip you up as it sets the Background to Transparent; you will either have to ensure you don't rely on the Disabled state, or come up with another technique for handling this case (eg, using an overlay)

Ideally there would be a way for the VSM to just "let go" of all animations once it had completed a transition back to the "base" state, letting the property system use your (modified) local Background property value, but the problem is that due to the fact you can have multiple, independent state groups there is no easy way to know when it is safe to undo all the animations and go back to the plain vanilla state.

Comments

  • Anonymous
    June 18, 2010
    Very nice Peter, thanks much!

  • Anonymous
    June 23, 2010
    Wow, am I the only one going uh.... to this? Seems a little much for something so simple...

  • Anonymous
    June 24, 2010
    The comment has been removed