Quick Fix for Silverlight HyperlinkButton Rendering
2010-9-3
Text rendering has always been Silverlight's dirty little secret. For every major release I hope that maybe this time fonts won't look terrible. Unfortunately as of 4.0, there are still a lot of improvements that must be made before Silverlight text will match the quality and readability of Flash. Robby Ingebretsen gives some helpful tips that can improve smoothness in Silverlight 4. But one of Silverlight's most basic controls continues to look terrible:
Hover your mouse into the control area, and notice how certain characters appear more blocky (h,p,n) while others get fuzzy (H,l,i). This is totally unexpected since all that's changing is the underline, so let's look at the ControlTemplate for to see what's going on.
<ControlTemplate TargetType="HyperlinkButton">
<Grid Background="{TemplateBinding Background}" Cursor="{TemplateBinding Cursor}">
<TextBlock x:Name="UnderlineTextBlock" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" Text="{TemplateBinding Content}" TextDecorations="Underline" Visibility="Collapsed" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
<TextBlock x:Name="DisabledOverlay" Foreground="#FFAAAAAA" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" Text="{TemplateBinding Content}" Visibility="Collapsed" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Canvas.ZIndex="1"/>
<ContentPresenter x:Name="contentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
<Rectangle x:Name="FocusVisualElement" IsHitTestVisible="false" Opacity="0" Stroke="#FF6DBDD1" StrokeThickness="1"/>
</Grid>
</ControlTemplate>
I didn't include the VisualStateGroups for brevity, but UnderlineTextBlock (which has the TextDecorations set to Underline) is set to Visible on the MouseOver VisualState. Similarly, DisabledOverlay is set to Visible on the Disabled visual state. The fuzziness then is a result of the TextBlock controls (UnderlineTextBlock and DisabledOverlay) getting layered on top of the ContentPresenter (who's control template will default to a TextBlock) which doubles the aliasing, making ugly text.
Why would you create three FrameworkElements in a template and bind them to Content when all you wanted to do is underline some text on MouseOver? Because TextDecorations are actually a TextDecorationCollection which can't be animated in a storyboard. Even more odd is that the TextDecoration enum only has one option: Underline. Perhaps most importantly, the HyperlinkButton is actually a ContentControl which means you might want to stick an Image or a button in there and still have it underline text when it is present.
So obviously someone at Microsoft made some compromises when designing this template, but how do we fix it? The simplest fix is to change the control template and add Visibility="Collapsed" on ContentPresenter for the three affected states: MouseOver, Pressed, and Disabled:
<VisualState x:Name="MouseOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetProperty="Visibility" Storyboard.TargetName="UnderlineTextBlock">
<DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="contentPresenter">
<DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetProperty="Visibility" Storyboard.TargetName="UnderlineTextBlock">
<DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="contentPresenter">
<DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetProperty="Visibility" Storyboard.TargetName="DisabledOverlay">
<DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="contentPresenter">
<DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
You can verify the fix in this side-by-side comparison
This will work as long as you're only binding strings to the HyperlinkButton. Alternatively, you could change the UnderlineTextBlock to a Rectangle, make it 1 pixel high and align it to the bottom. This would have the added benefit of working for all non-text content. For someone that doesn't mind a little UI adjustment in their Code Behind you can do what I did and create a new control. I called mine a TextHyperlinkButton to make it obvious that it should only contain text.
namespace Sleight.Windows.Controls
{
public class TextHyperlinkButton : HyperlinkButton
{
private bool _templateApplied;
private TextBlock contentPresenter;
public TextHyperlinkButton()
{
DefaultStyleKey = typeof(TextHyperlinkButton);
MouseEnter += TextHyperlinkButton_MouseEnter;
MouseLeave += TextHyperlinkButton_MouseLeave;
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
contentPresenter = GetTemplateChild("contentPresenter") as TextBlock;
if (contentPresenter == null)
{
throw new NullReferenceException("Could not find required TextBlock of name 'contentPresenter' in ControlTemplate");
}
_templateApplied = true;
}
void TextHyperlinkButton_MouseLeave(object sender, MouseEventArgs e)
{
if (!_templateApplied) return;
contentPresenter.TextDecorations = null;
}
void TextHyperlinkButton_MouseEnter(object sender, MouseEventArgs e)
{
if (!_templateApplied) return;
contentPresenter.TextDecorations = TextDecorations.Underline;
}
}
}
And the sample:
I did make one other change in my TextHyperlinkButton which was to tweak the FocusVisualElement. That blue rectangle has always driven me crazy, and while my gut wants to remove it entirely I decided just to change the color and make it barely visible.
You can a download the sample project here
Comments
Mister Goodcat (http://www.pitorque.de/MisterGoodcat)
Nice to see someone else who pays attention to these details. I also change hyperlinks in my projects in a similar way because I think the default rendering is horrible. Thank you for that post ;)
Vladimir Kartaviy (http://vkartaviy.name)
Why not to use ContentTemplate property with TextBlock inside and remove UnderlineTextBlock and DisabledOverlay? I'am trying to add code here but it gives me error ;(
Peter Brady (http://peterbrady.net)
Sorry about that Vladimir, you should be able to post code now. Re: ContentTemplate- That would be great if you weren't interested in the underline at all.
Travis
Awesome!!! Thank You! Bummer about the default and this template too is if your link has an underscore in it then the colors get doubled up and the color for the underscore character ends up being darker than the rest of the link on mouseover. Hyperlink hacks. Surely, Microsoft could have come up with something better for something as common as a hyperlink.
Jay
Wow! Thanks to you, Peter, I could understand the problem. The fix works perfectly. Also, it is kind of lame of Microsoft to leave such an issue open.
Raj (http://w3mentor.com)
Amazing fix man..Just what I was looking for in silverlight. thanks