7

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?

amiry jd
  • 27,021
  • 30
  • 116
  • 215
  • You are trying to animate the `SolidColorBrush` of `PART_ContentHost`, however it doesn't contain any brush. Did you try assigning some initial brush? (And where does your content go?) – Vlad Aug 24 '13 at 22:04
  • Well, I still see ``. So you are trying to animate a property on `null` object, right? – Vlad Aug 24 '13 at 22:23
  • No I'm not animating `Background` at all. Target property is `Foreground` – amiry jd Aug 24 '13 at 22:24
  • Oh, I see. You are trying to animate an attached property `Textblock.Foreground` on the `PART_ContentHost`. But it's still the same: `PART_ContentHost` doesn't have any, so it's `null`. Could you try to assign some value to it? – Vlad Aug 24 '13 at 22:28
  • No I'm not animating `Textblock.Foreground`, but I tried all storyboards mentioned in question; and yes, on each storyboard usage, I set correct values for all properties (including your mentioned one). – amiry jd Aug 24 '13 at 22:31
  • Okay, I didn't know that ScrollViewer has its own property `Foreground`. But nevertheless, this property is `null`, as it's not assigned for `PART_ContentHost`. As well as attached property `TextElement.Foreground`. And for the unnamed `Grid`, too. – Vlad Aug 24 '13 at 23:28

3 Answers3

12

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.

amiry jd
  • 27,021
  • 30
  • 116
  • 215
  • 2
    I don't know how you managed to make that work. For me, it still results in the immutable Color exception. – nmclean Aug 25 '13 at 02:15
  • 1
    @nmclean My mistake. Use `(TextBox.Foreground).(SolidColorBrush.Color)` as target property. It works just fine – amiry jd Aug 25 '13 at 02:25
  • 1
    I can't get that to work either... I get the immutable error that I had to work around in my answer. – Stephen Hewlett Aug 25 '13 at 02:27
  • I'm creating a sample project. Please wait for a few minutes that I can to upload it on github. – amiry jd Aug 25 '13 at 02:29
  • 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. – Stephen Hewlett Aug 25 '13 at 04:10
  • Well, I tested it by `StaticResource`, `DynamicResource`, and hard coded values (including named colors and hex codes). It works just with `DynamicResource`s; why? I don't know -I'm a newbie WPFer actually. But the problem of your solution is that we have actually two `TextBox`s. And if we want to stylize other `VisualState`s, we'll have a more complicated un-readable code. – amiry jd Aug 25 '13 at 08:53
9

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.

Stephen Hewlett
  • 2,415
  • 1
  • 18
  • 31
  • 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. – nmclean Aug 25 '13 at 02:17
  • 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. – Stephen Hewlett Aug 25 '13 at 02:20
2

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.

nmclean
  • 7,564
  • 2
  • 28
  • 37