How to propagate styles to Hyperlinks inside a DataTemplate?
Asked Answered
T

1

7

I'm try to set the Foreground colour on a Hyperlink using a Style object in an ancestor's Resources, but it's not having any effect. I even used the BasedOn tip from Changing Hyperlink foreground without losing hover color, but it's not made any difference - I still get a blue hyperlink that is red on hover.

Here's the XAML for my controls, including an ItemsControl whose items are shown using a hyperlink:

<StackPanel Background="Red" TextElement.Foreground="White">
  <StackPanel.Resources>
    <Style TargetType="Hyperlink" BasedOn="{StaticResource {x:Type Hyperlink}}">
      <Setter Property="Foreground" Value="Yellow"/>
      <Style.Triggers>
        <Trigger Property="IsMouseOver" Value="True">
          <Setter Property="Foreground" Value="White"/>
        </Trigger>
      </Style.Triggers>
    </Style>
  </StackPanel.Resources>
  <TextBlock>Data import errors</TextBlock>
  <ItemsControl ItemsSource="{Binding Errors}"/>
</StackPanel>

And the items in the ItemsControl are picking up the following DataTemplate:

<DataTemplate DataType="{x:Type Importer:ConversionDetailsMessage}">
  <TextBlock>
    <Run Text="{Binding Message, Mode=OneTime}"/>
    <Hyperlink Command="Common:ImportDataCommands.ExplainConversionMessage" CommandParameter="{Binding}">
      <Run Text="{Binding HelpLink.Item2, Mode=OneTime}"/>
    </Hyperlink>
  </TextBlock>
</DataTemplate>

It's worth noting, too, that I don't want to just set the different colours directly on the Hyperlink in the DataTemplate. This is because the template will be used by a number of different ItemsControl objects, most of which will be on a white background and so can use the standard hyperlink colours. (Note that the one in the XAML above, however, has a red background.)

In short, I don't want the DataTemplate to have to know anything about the control in which it's being used. The styles for the template's controls should just filter down to it.

So... can anyone tell me why the style's not filtering down and what I can do to fix it?

Thanks.

Update:
Since I couldn't get Pavlo's answer to work in my production app, I've since tried it in a separate test app. The app is a WinForms app, with the main form containing nothing but an ElementHost, which itself contains a simple WPF usercontrol. Here's its XAML:

<UserControl x:Class="DataTemplateStyling.StylingView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:DataTemplateStyling="clr-namespace:DataTemplateStyling"
             x:Name="root" Loaded="StylingViewLoaded">

  <UserControl.Resources>
    <Style x:Key="MyDefaultHyperlinkStyle" BasedOn="{StaticResource {x:Type Hyperlink}}"/>

    <DataTemplate DataType="{x:Type DataTemplateStyling:ImportMessage}">
      <DataTemplate.Resources>
        <Style TargetType="{x:Type Hyperlink}"
               BasedOn="{StaticResource MyDefaultHyperlinkStyle}"/>
      </DataTemplate.Resources>
      <TextBlock>
        <Run Text="{Binding Message, Mode=OneTime}"/>
        <Hyperlink NavigateUri="{Binding HelpLink.Item1}">
          <Run Text="{Binding HelpLink.Item2, Mode=OneTime}"/>
        </Hyperlink>
      </TextBlock>
    </DataTemplate>
  </UserControl.Resources>

  <Grid DataContext="{Binding ElementName=root}">
    <StackPanel Background="Red" TextElement.Foreground="White">
      <StackPanel.Resources>
        <Style x:Key="MyDefaultHyperlinkStyle" TargetType="Hyperlink" BasedOn="{StaticResource {x:Type Hyperlink}}">
          <Setter Property="Foreground" Value="Yellow"/>
          <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
              <Setter Property="Foreground" Value="White"/>
            </Trigger>
          </Style.Triggers>
        </Style>
      </StackPanel.Resources>
      <TextBlock>Data import errors</TextBlock>
      <ItemsControl ItemsSource="{Binding Messages}"/>
    </StackPanel>
  </Grid>
</UserControl>

As it stands above, this generates an InvalidOperationException, stating "Can only base on a Style with target type that is base type 'IFrameworkInputElement'."

That can be fixed by putting TargetType="Hyperlink" on the Style definition immediately inside the UserControl.Resources element. However, while the messages are being shown, the link part of them still has the default blue hyperlink styling:

Blue hyperlinks persist

In short, it's not working, so I'd welcome any other suggestions/corrections. :(

Update 2:
Thanks to an alternative solution from Pavlo, it now is working. :)

Tufa answered 3/3, 2011 at 14:38 Comment(0)
T
8

After some googling I ran into this post: http://www.11011.net/archives/000692.html.

As it described there, it turns out that elements that are not derived from Control (like TextBlock and Hyperlink) do not look for implicit styles outside the DataTemplate boundary.

Again, as the article says, the possible workaround is to specify the style key explicitly. In your case it might be something like this:

<StackPanel Background="Red" TextElement.Foreground="White">
  <StackPanel.Resources>
    <Style x:Key="MyDefaultHyperlinkStyle" TargetType="Hyperlink" BasedOn="{StaticResource {x:Type Hyperlink}}">
      <Setter Property="Foreground" Value="Yellow"/>
      <Style.Triggers>
        <Trigger Property="IsMouseOver" Value="True">
          <Setter Property="Foreground" Value="White"/>
        </Trigger>
      </Style.Triggers>
    </Style>
  </StackPanel.Resources>
  <TextBlock>Data import errors</TextBlock>
  <ItemsControl ItemsSource="{Binding Errors}"/>
</StackPanel>

Then, you can add an implicit style for Hyperlink that just references our named style in the DataTemplate resources:

<DataTemplate DataType="{x:Type Importer:ConversionDetailsMessage}">
  <DataTemplate.Resources>
    <Style TargetType="{x:Type Hyperlink}"
           BasedOn="{StaticResource MyDefaultHyperlinkStyle}"/>
  </DataTemplate.Resources>
  <TextBlock>
    <Run Text="{Binding Message, Mode=OneTime}"/>
    <Hyperlink Command="Common:ImportDataCommands.ExplainConversionMessage" CommandParameter="{Binding}">
      <Run Text="{Binding HelpLink.Item2, Mode=OneTime}"/>
    </Hyperlink>
  </TextBlock>
</DataTemplate>

And because the data template can be used in different places, there is a possibility that parent container doesn't define a style with key "MyDefaultHyperlinkStyle". In this case an exception will be thrown saying that resource "MyDefaultHyperlinkStyle" cannot be found. To address this, you can define a style with such key that will only inherit default style somewhere in App.xaml:

<Style x:Key="MyDefaultHyperlinkStyle"
       BasedOn="{StaticResource {x:Type Hyperlink}}/>

Update:

The code you included in your update will not work because of the nature of static resources, which means that the following resource reference in date template...

BasedOn="{StaticResource MyDefaultHyperlinkStyle}"

... will always point to the following resource (which is the default style):

<Style x:Key="MyDefaultHyperlinkStyle" BasedOn="{StaticResource {x:Type Hyperlink}}"/>

Static resource references are resolved at compile time, therefore the closest resource in the tree is used.

You might be tempted to use DynamicResource, but unfortunately it is not supported with the BasedOn property.

BUT, Foreground property supports dynamic resources, so we can use the same trick with brushes we use inside our style. Here is your test user control modified to use dynamic brushes:

<UserControl x:Class="DataTemplateStyling.StylingView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:DataTemplateStyling="clr-namespace:DataTemplateStyling"
             x:Name="root"
             Loaded="StylingViewLoaded">

    <UserControl.Resources>
        <SolidColorBrush x:Key="HyperlinkForeground"
                         Color="Blue" />

        <SolidColorBrush x:Key="HyperlinkHoverForeground"
                         Color="Gray" />

        <Style x:Key="MyDefaultHyperlinkStyle"
               TargetType="Hyperlink"
               BasedOn="{StaticResource {x:Type Hyperlink}}">
            <Setter Property="Foreground"
                    Value="{DynamicResource HyperlinkForeground}" />
            <Style.Triggers>
                <Trigger Property="IsMouseOver"
                         Value="True">
                    <Setter Property="Foreground"
                            Value="{DynamicResource HyperlinkHoverForeground}" />
                </Trigger>
            </Style.Triggers>
        </Style>

        <DataTemplate DataType="{x:Type DataTemplateStyling:ImportMessage}">
            <DataTemplate.Resources>
                <Style TargetType="{x:Type Hyperlink}"
                       BasedOn="{StaticResource MyDefaultHyperlinkStyle}" />
            </DataTemplate.Resources>
            <TextBlock>
                <Run Text="{Binding Message, Mode=OneTime}" />
                <Hyperlink NavigateUri="{Binding HelpLink.Item1}">
                    <Run Text="{Binding HelpLink.Item2, Mode=OneTime}" />
                </Hyperlink>
            </TextBlock>
        </DataTemplate>
    </UserControl.Resources>

    <Grid DataContext="{Binding ElementName=root}">
        <StackPanel Background="Red"
                    TextElement.Foreground="White">
            <StackPanel.Resources>
                <SolidColorBrush x:Key="HyperlinkForeground"
                                 Color="Yellow" />

                <SolidColorBrush x:Key="HyperlinkHoverForeground"
                                 Color="White" />
            </StackPanel.Resources>
            <TextBlock>Data import errors</TextBlock>
            <ItemsControl ItemsSource="{Binding Messages}" />
        </StackPanel>
    </Grid>
</UserControl>

It works as expected, i.e. all links inside StackPanel will be Yellow/White, while outside they will be blue.

Hope this helps.

Tilefish answered 3/3, 2011 at 17:32 Comment(10)
Awesome googling skills - thanks! And a very complete answer. I'll just try this out and mark it as accepted once I've got it working.Tufa
Forgot my app is a WinForms app using an ElementHost, so I don't have an app.xaml file. Still, the principle seemed sound, so I tried adding an empty MyDefaultHyperlinkStyle immediately before the DataTemplate in my UserControl's resource dictionary and left the specific StackPanel seen above to replace it with its own definition. Sadly, it's still not working. I'll keep on trying things...Tufa
@Mal - Didn't you forget including style in the data template itself: <DataTemplate.Resources> <Style TargetType="{x:Type Hyperlink}" BasedOn="{StaticResource MyDefaultHyperlinkStyle}"/> </DataTemplate.Resources>?Tilefish
@Pavlo: No, I put that in there. :-/Tufa
@Mal - Just tried inside ElementHost in a WinForms project. It works perfectly. Check your code.Tilefish
Hi Pavlo, I've copied and pasted your code above into a very simple test app and it's still not working. And yes, I have checked my code. ;) Were there any differences between your own test app and the code above? BTW, see the update in my original question to see exactly what XAML I'm using. Thanks for your help.Tufa
@Mal - Yeah, now I see. The problem is that static resources are resolved at runtime, hence the BasedOn="{StaticResource MyDefaultHyperlinkStyle}" reference in the DataTemplate is resolved to nearest resource in the tree (which is <Style x:Key="MyDefaultHyperlinkStyle" BasedOn="{StaticResource {x:Type Hyperlink}}"/>), while the ItemsControl is a dynamic thing, i.e. by the time when items are created (at runtime) all the resources already resolved and the resource defined in StackPanel.Resources is not taken into account.Tilefish
@Mal - That being said, you might be tempted to replace StaticResource to DynamicResource, but, unfortunately, it is not allowed on BasedOn property of the Style... So, the only thing left is to define the correct style BEFORE the DataTemplate in the tree (i.e. if you put the correct style from StackPanel.Resources to UserControl.Resources it will work).Tilefish
Ah, that's a pity. I need different ItemsControls to have different hyperlink styles, yet still use the same DataTemplate. Sounds like I'm trying to do something that's impossible, given the current restrictions in the framework code. I suppose I could always change my ViewModel class (ImportMessage in my code above) to include information about the desired colours and then just bind to the relevant properties. Not ideal, but I guess it's simple enough. Thanks for all of your help - it was enlightening.Tufa
@Pavlo - BTW, if you want to amend your answer to say that I can't do exactly what I want in XAML alone (but leave the rest of the answer there too), I'll mark it as the accepted answer.Tufa

© 2022 - 2024 — McMap. All rights reserved.