Hyperlinks to the Web in WPF

The obvious choice for implementing a link to a Web site in WPF is the Hyperlink class. Note, however, that the Hyperlink class is a FrameworkContentElement, which means it doesn’t know how to render itself, but must be hosted by a TextBlock or a FlowDocument viewer like FlowDocumentScrollViewer or RichTextBox. So the easiest way to display a hyperlink is to wrap it in a TextBlock.

    <TextBlock>
        <Hyperlink NavigateUri="http://code.logos.com/blog/">Blog</Hyperlink>
    </TextBlock>

If your application is hosted in Internet Explorer (XBAP or loose XAML), the browser will be navigated accordingly. If the Hyperlink is in the Page of a NavigationWindow or Frame, that window or frame will be navigated.

The Hyperlink doesn’t work at all from a standalone Window, however. Lauren Lavoie discusses the workaround – your application needs to intercept the navigation and use Process.Start with the URL to launch the default browser. The best way to do that is to handle the Hyperlink.RequestNavigate routed event on the Hyperlink or one of its ancestors. (You could also just handle the Hyperlink.Click event, but Lauren’s solution seems better.) And the easiest way to do that is with an attached dependency property:

    <TextBlock lbx:HyperlinkUtility.LaunchDefaultBrowser="True">
        <Hyperlink NavigateUri="http://code.logos.com/blog/">Blog</Hyperlink>
    </TextBlock>

And how is that dependency property implemented? We have some really cool utility code for registering dependency properties in a type-safe manner, but I’ll use traditional methods for now:

public static class HyperlinkUtility
{
    public static readonly DependencyProperty LaunchDefaultBrowserProperty =
        DependencyProperty.RegisterAttached("LaunchDefaultBrowser", typeof(bool),
        typeof(HyperlinkUtility), new PropertyMetadata(false,
        HyperlinkUtility_LaunchDefaultBrowserChanged));

    public static bool GetLaunchDefaultBrowser(DependencyObject d)
    {
        return (bool) d.GetValue(LaunchDefaultBrowserProperty);
    }

    public static void SetLaunchDefaultBrowser(DependencyObject d, bool value)
    {
        d.SetValue(LaunchDefaultBrowserProperty, value);
    }

    private static void HyperlinkUtility_LaunchDefaultBrowserChanged(object
        sender, DependencyPropertyChangedEventArgs e)
    {
        DependencyObject d = (DependencyObject) sender;
        if ((bool) e.NewValue)
            ElementUtility.AddHandler(d, Hyperlink.RequestNavigateEvent, new RequestNavigateEventHandler(Hyperlink_RequestNavigateEvent));
        else
            ElementUtility.RemoveHandler(d, Hyperlink.RequestNavigateEvent, new RequestNavigateEventHandler(Hyperlink_RequestNavigateEvent));
    }

    private static void Hyperlink_RequestNavigateEvent(object sender, RequestNavigateEventArgs e)
    {
        Process.Start(e.Uri.AbsoluteUri);
        e.Handled = true;
    }
}

But wait, what is ElementUtility? I guess I couldn’t avoid our utility code entirely - ElementUtility.AddHandler calls UIElement.AddHandler, ContentElement.AddHandler, or UIElement3D.AddHandler, depending on the type of the element. Similarly with ElementUtility.RemoveHandler. This dependency property should be supported on the TextBlock (a UIElement) or the Hyperlink (a ContentElement), so we need to support both types of elements. The implementation of AddHandler and RemoveHandler is left as an exercise for the reader.

I also want to talk more about Process.Start, but that will have to wait until my next post.

Posted by Ed Ball on January 17, 2008