Wpf – In a button’s control template, how can I set the color of contained text

buttoncontroltemplatesilverlightvisualstatemanagerwpf

Using Silverlight 4 & WPF 4, I'm trying to create a button style that alters the text color of any contained text when the button is mouseover'd. Since I'm trying to make this compatible with both Silverlight & WPF, I'm using the visual state manager:

<Style TargetType="{x:Type Button}">
<Setter Property="Template">
    <Setter.Value>
        <ControlTemplate TargetType="Button">
            <Border x:Name="outerBorder" CornerRadius="4" BorderThickness="1" BorderBrush="#FF757679">
                <VisualStateManager.VisualStateGroups>
                    <VisualStateGroup x:Name="CommonStates">
                        <VisualState x:Name="Normal" />
                        <VisualState x:Name="MouseOver">
                            <Storyboard>
                                <ColorAnimation Duration="0" To="#FFFEFEFE"
                                                Storyboard.TargetProperty="(TextElement.Foreground).(SolidColorBrush.Color)"
                                                Storyboard.TargetName="contentPresenter"/> 
                            </Storyboard>
                        </VisualState>
                    </VisualStateGroup>
                </VisualStateManager.VisualStateGroups>
                <Grid>
                    <Border x:Name="Background" CornerRadius="3" BorderThickness="1" BorderBrush="Transparent">
                        <Grid>
                            <ContentPresenter x:Name="contentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}"/>
                        </Grid>
                    </Border>
                </Grid>
            </Border>
        </ControlTemplate>
    </Setter.Value>
</Setter>

Since this is a template for a regular old button, I know there's no guarantee that there even is a textblock inside of it, and at first I wasn't sure this was even possible. Curiously, the text color does change if the button is declared like:

<Button Content="Hello, World!" />

but it does not change if the button is declared like:

<Button>
    <TextBlock Text="Hello, World!" /> <!-- Same result with <TextBlock>Hello, World </TextBlock> -->
</Button>

Even though the visual tree (when inspected in snoop) is identical (Button -> ContentPresenter -> TextBlock), with the caveat that the textblock created in the 1st version has it's data context set to "Hello, World", whereas the textblock in the second version merely has its text property set. I'm presuming this has something to do with the order of control creation (the first version the button creates the TextBlock, in the second version the textblock might be created first? Really not sure on this).

In the course of researching this, I've seen some solutions that work in Silverlight (like replacing the ContentPresenter with a ContentControl), but that won't work in WPF (program actually crashes).

Since this is in the button's control template, and I'd like to use the VSM if possible, I think that also rules out explicitly changing the Button's own Foreground property (I don't know how I would access that from within the template?)

I'd really appreciate any help, advice anyone could give.

Best Answer

So after some more thinking, the solution I've ultimately arrived at is to add an attached property to the ContentPresenter element within the button's control template. The attached property accepts a Color and when set examines the visual tree of the content presenter for any TextBlocks, and in turn sets their Foreground properties to the value passed in. This could obviously be expanded/made to handle additional elements but for now it works for what I need.

public static class ButtonAttachedProperties
    {
        /// <summary>
        /// ButtonTextForegroundProperty is a property used to adjust the color of text contained within the button.
        /// </summary>
        public static readonly DependencyProperty ButtonTextForegroundProperty = DependencyProperty.RegisterAttached(
            "ButtonTextForeground",
            typeof(Color),
            typeof(FrameworkElement),
            new FrameworkPropertyMetadata(Color.FromArgb(255, 0, 0, 0), FrameworkPropertyMetadataOptions.AffectsRender, OnButtonTextForegroundChanged));

        public static void OnButtonTextForegroundChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            if (e.NewValue is Color)
            {
                var brush = new SolidColorBrush(((Color) e.NewValue)) as Brush;
                if (brush != null)
                {
                    SetTextBlockForegroundColor(o as FrameworkElement, brush);
                }
            }
        }

        public static void SetButtonTextForeground(FrameworkElement fe, Color color)
        {
            var brush = new SolidColorBrush(color);
            SetTextBlockForegroundColor(fe, brush);
        }

        public static void SetTextBlockForegroundColor(FrameworkElement fe, Brush brush)
        {
            if (fe == null)
            {
                return;
            }

            if (fe is TextBlock)
            {
                ((TextBlock)fe).Foreground = brush;
            }

            var children = VisualTreeHelper.GetChildrenCount(fe);
            if (children > 0)
            {
                for (int i = 0; i < children; i++)
                {
                    var child = VisualTreeHelper.GetChild(fe, i) as FrameworkElement;
                    if (child != null)
                    {
                        SetTextBlockForegroundColor(child, brush);
                    }
                }
            }
            else if (fe is ContentPresenter)
            {
                SetTextBlockForegroundColor(((ContentPresenter)fe).Content as FrameworkElement, brush);
            }
        }
    }

and I modified the template like so:

<ContentPresenter x:Name="contentPresenter" 
                  ContentTemplate="{TemplateBinding ContentTemplate}" 
                  local:ButtonAttachedProperties.ButtonTextForeground="{StaticResource ButtonTextNormalColor}" />
Related Topic