Animating a TextBox.Foreground in WPF

16,047

Solution 1

Well, thanks to all that were trying to help me, I found my answer. It seems when we set TextBox.Foreground property to a resource, the storyboard cannot animate it. So, the style should be something like this:

<Style x:Key="RegularTextBox" TargetType="{x:Type TextBox}">
    <Setter Property="Foreground">
        <Setter.Value>
            <SolidColorBrush Color="{DynamicResource NormalColor}"/>
        </Setter.Value>
    </Setter>
    <Setter Property="Template" Value="{StaticResource RegularTextBoxTemplate}"/>
</Style>

This was the only problem I had. But there is a note to remember. When we want to target a templated parent in a storyboard, it's not necessary to bind to it. We just need to leave it:

<!-- It's not necessary to set Storyboard.TargetName in storyboard -->
<!-- It will automatically target the TemplatedParent -->
<ColorAnimationUsingKeyFrames
              Storyboard.TargetProperty="(TextBox.Foreground).(SolidColorBrush.Color)">
    <EasingColorKeyFrame KeyTime="0" Value="{DynamicResource MouseOverColor}" />
</ColorAnimationUsingKeyFrames>

This works for me.


Here is a working example.

Solution 2

This was more problematic than I thought. Here is my original answer:


It's definitely possible - that's what the ColorAnimationXXX classes are for.

Your code is very similar to the code example here, which animates a colour with the ColorAnimation instead. The property in the example takes a Brush (just like TextBox.Foreground) which is defined in XAML and given a name so that it can be referenced easily by the animation.

So in your case the pertinent code would be:

<VisualState Name="...">
   <Storyboard>
      <ColorAnimation To="Green" 
                      Storyboard.TargetName="tbBrush" 
                      Storyboard.TargetProperty="Color"/>
    </Storyboard>
</VisualState>

and:

<TextBox.Foreground>
  <SolidColorBrush x:Name="tbBrush" Color="#FF666666"/>
</TextBox.Foreground>

That was all very well, in theory, until I realised it didn't work in a style. Whereas the Background property of the Grid within the style is easily animatable, with something like:

Storyboard.TargetProperty="(Grid.Background).(SolidColorBrush.Color)"

it is significantly more difficult to find a property to animate that will have an effect on the Foreground of the text. Initially I tried TextElement.Foreground, which seems intuitive, and I was able to set this property at the Grid and ScrollViewer levels which I expected to have an effect on all the child objects underneath - including whatever object it is at the bottom level that contains the text of the TextBox. My assumption was that the TextBox content would be internally set to a TextBlock, which would obey the value of the Foreground attached property set on it. It seems that my assumption was incorrect, and the content of the PART_ContentHost ScrollViewer is set by the control logic within TextBox to a lower level object that does not obey any of the Foreground dependency properties in the object tree between the top level TextBox and itself.

The problem then is how to set the Foreground property of the TextBox within the style of the TextBox being styled. For testing, I tried to set this with a TwoWay TemplatedParent binding. I think I got the PropertyPath to the Color of the SolidColorBrush right, but it still didn't work as the Color property was apparently immutable at that point. I believe this issue is documented here.

On top of the fact that it doesn't work, setting the Foreground property internally did not seem right as external consumers would expect to be in control of the value of that property. So, given that the Foreground of a TextBox will not obey anything in a style, I came to the conclusion that the functionality is best implemented with a nested TextBox within the TextBox style. The outer style contains the state manager and most of the layout, then the inner TextBox has its own style and control template that is designed just to display the text bit. The outer style is able to set the Foreground property of the inner TextBox, which the inner one will obey, and crucially the outer one can set this value in the state manager.

<ControlTemplate x:Key="RegularTextBoxTemplate" TargetType="{x:Type TextBox}"> 
    <Grid>
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="CommonStates">
                <VisualStateGroup.Transitions>
                    <VisualTransition GeneratedDuration="0:0:0.1"/>
                </VisualStateGroup.Transitions>
                <VisualState x:Name="Normal"/>
                <VisualState x:Name="MouseOver">
                    <Storyboard>
                        <ColorAnimation To="HotPink"
                            Storyboard.TargetName="InternalTextBox"
                            Storyboard.TargetProperty="(TextBox.Foreground).(SolidColorBrush.Color)"/>
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <TextBox Foreground="Black" Text="{TemplateBinding Text}" x:Name="InternalTextBox">
            <TextBox.Style>
                <Style TargetType="{x:Type TextBox}">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="{x:Type TextBox}">
                                <Grid Background="{x:Null}">
                                    <ScrollViewer x:Name="PART_ContentHost"
                                        BorderThickness="0"
                                        IsTabStop="False"
                                        Background="{x:Null}" />
                                </Grid>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </TextBox.Style>
        </TextBox>
    </Grid>
</ControlTemplate>

<Style x:Key="RegularTextBox" TargetType="{x:Type TextBox}">
    <Setter Property="Template" Value="{StaticResource RegularTextBoxTemplate}"/>
</Style>

I would be interested in hearing other people's comments on this approach, and whether there are any problems with it that I have not considered. Based on my attempts at solving the problem and snooping the resultant application, it is the simplest solution I can see at the moment.

Solution 3

You can bind Storyboard.Target to the TemplatedParent:

<ColorAnimationUsingKeyFrames
        Storyboard.Target="{Binding RelativeSource={RelativeSource TemplatedParent}}"
        Storyboard.TargetProperty="(TextBox.Foreground).(SolidColorBrush.Color)">
    <EasingColorKeyFrame KeyTime="0" Value="{StaticResource MouseOverColor}" />
</ColorAnimationUsingKeyFrames>

Unfortunately, I could only get this to work when the normal foreground brush is not set in the style, and directly set on the TextBox element:

<TextBox Foreground="{StaticResource NormalBrush}" Style="{StaticResource RegularTextBox}" />

If it is set in the style, triggering the MouseOver state throws "Cannot animate '(0).(1)' on an immutable object instance." edit: This also happens if you set the TextBox foreground again after it is initialized.

Share:
16,047
amiry jd
Author by

amiry jd

My other SO-Profile: https://stackoverflow.com/users/974276/j-amiry I am a hard-working and innovative developer with near 18 years of experience mostly based on .NET (Framework &amp; Core) technology stack. For the last few years, alongside developing some awesome applications, I was focused on some non-coding tasks such as building up development teams, team leading, tech leading, problem solving, consulting, architecting, reviewing other's code, and mentoring. As a self-motivated fast-learner experienced code-guy, who has a proactive personality to step up to new challenges, I think I'm the man who can get the job done.

Updated on June 17, 2022

Comments

  • amiry jd
    amiry jd almost 2 years

    Is there anyway to animate a TextBox.ForegroundProperty?

    <Color x:Key="NormalColor">#FF666666</Color>
    <SolidColorBrush x:Key="NormalBrush" Color="{StaticResource NormalColor}" />
    
    <Color x:Key="MouseOverColor">#FF666666</Color>
    <SolidColorBrush x:Key="MouseOverBrush" Color="{StaticResource MouseOverColor}" />
    
    <ControlTemplate x:Key="RegularTextBoxTemplate" TargetType="{x:Type TextBox}">
        <Grid>
            <VisualStateManager.VisualStateGroups>
                <VisualStateGroup x:Name="CommonStates">
                    <VisualStateGroup.Transitions>
                        <VisualTransition GeneratedDuration="0:0:0.1"/>
                    </VisualStateGroup.Transitions>
                    <VisualState x:Name="Normal"/>
                    <VisualState x:Name="MouseOver">
                        <Storyboard>
                            <!-- storyboard to animating foreground here... -->
                        </Storyboard>
                    </VisualState>
                </VisualStateGroup >
            </VisualStateManager>
            <ScrollViewer x:Name="PART_ContentHost" 
                          BorderThickness="0"
                          IsTabStop="False"
                          Background="{x:Null}"/>
        </Grid>
    </ControlTemplate>
    
    <Style x:Key="RegularTextBox" TargetType="{x:Type TextBox}">
        <Setter Property="Foreground" Value="{StaticResource NormalBrush}"/>
        <Setter Property="Template" Value="{StaticResource RegularTextBoxTemplate}"/>
    </Style>
    

    My tried storyboards are:

    <ColorAnimationUsingKeyFrames Storyboard.TargetName="PART_ContentHost"
                      Storyboard.TargetProperty="(Foreground).(SolidColorBrush.Color)">
        <EasingColorKeyFrame KeyTime="0" Value="{StaticResource MouseOverColor}" />
    </ColorAnimationUsingKeyFrames>
    
    <ColorAnimationUsingKeyFrames Storyboard.TargetName="PART_ContentHost"
                  Storyboard.TargetProperty="(Control.Foreground).(SolidColorBrush.Color)">
        <EasingColorKeyFrame KeyTime="0" Value="{StaticResource MouseOverColor}" />
    </ColorAnimationUsingKeyFrames>
    
    <ColorAnimationUsingKeyFrames Storyboard.TargetName="PART_ContentHost"
              Storyboard.TargetProperty="(TextElement.Foreground).(SolidColorBrush.Color)">
        <EasingColorKeyFrame KeyTime="0" Value="{StaticResource MouseOverColor}" />
    </ColorAnimationUsingKeyFrames>
    
    <ColorAnimationUsingKeyFrames
                      Storyboard.TargetProperty="(Foreground).(SolidColorBrush.Color)">
        <EasingColorKeyFrame KeyTime="0" Value="{StaticResource MouseOverColor}" />
    </ColorAnimationUsingKeyFrames>
    
    <ColorAnimationUsingKeyFrames
                  Storyboard.TargetProperty="(TextBox.Foreground).(SolidColorBrush.Color)">
        <EasingColorKeyFrame KeyTime="0" Value="{StaticResource MouseOverColor}" />
    </ColorAnimationUsingKeyFrames>
    
    <ColorAnimationUsingKeyFrames
                  Storyboard.TargetProperty="(Control.Foreground).(SolidColorBrush.Color)">
        <EasingColorKeyFrame KeyTime="0" Value="{StaticResource MouseOverColor}" />
    </ColorAnimationUsingKeyFrames>
    
    <ColorAnimationUsingKeyFrames
              Storyboard.TargetProperty="(TextElement.Foreground).(SolidColorBrush.Color)">
        <EasingColorKeyFrame KeyTime="0" Value="{StaticResource MouseOverColor}" />
    </ColorAnimationUsingKeyFrames>
    

    None of them work. Any idea? Is it even possible?

  • nmclean
    nmclean over 10 years
    I don't know how you managed to make that work. For me, it still results in the immutable Color exception.
  • nmclean
    nmclean over 10 years
    Does this still work if you set Foreground="{TemplateBinding Foreground}" on the inner TextBox? With the current version there doesn't seem to be a way for the user to set the "normal" color.
  • Stephen Hewlett
    Stephen Hewlett over 10 years
    I was pleased to have got that far to be honest so I didn't check - I think it would work OK, and if not then the Foreground of the inner textbox could be set in the other states of the visual state manager.
  • amiry jd
    amiry jd over 10 years
    @nmclean My mistake. Use (TextBox.Foreground).(SolidColorBrush.Color) as target property. It works just fine
  • Stephen Hewlett
    Stephen Hewlett over 10 years
    I can't get that to work either... I get the immutable error that I had to work around in my answer.
  • amiry jd
    amiry jd over 10 years
    I'm creating a sample project. Please wait for a few minutes that I can to upload it on github.
  • Stephen Hewlett
    Stephen Hewlett over 10 years
    I was honestly surprised to see that work... I had to look closely to see what was different! I've noticed a couple of subtleties that might make my solution better in some circumstances :P. If I change the Color of the SolidColorBrush of Foreground in the Style to be either a hard coded value ('Red', 'Violet' etc.), or it is bound with StaticResource as opposed to DynamicResource, then it doesn't work. This is a potential 'surprise problem' that could occur later on... I would recommend commenting it in your code in case someone else changes it further down the line.
  • amiry jd
    amiry jd over 10 years
    Well, I tested it by StaticResource, DynamicResource, and hard coded values (including named colors and hex codes). It works just with DynamicResources; why? I don't know -I'm a newbie WPFer actually. But the problem of your solution is that we have actually two TextBoxs. And if we want to stylize other VisualStates, we'll have a more complicated un-readable code.