If you haven’t noticed already, I happen to like the Navigation feature in Silverlight quite a bit (I wonder why? :)).  In my other posts on Navigation, I’ve spent some time exploring how you can navigate to Pages in assemblies other than the main application assembly and how those assemblies can be loaded on-demand (granted, it uses some workarounds, but it gets us where we want to go!).

This is all well and good, but it presents an annoying problem that impacts the maintainability of such code.  Specifically, it forces every hyperlink within each of the external assemblies to know how to refer to its assembly by name (this is akin to the problem with absolute URLs in hyperlinks on web pages – those links become tightly coupled to the page for which they were created, and can’t be copied into other projects).

Technically, almost all Uris used by the navigation framework today are relative Uris (unless UriMappings are used to turn absolute ones into relative ones), but they are application-relative and never Page-relative.  This means that if you have a page at “/Views/Page1.xaml” and want to link to another page at “/Views/Page2.xaml” from within Page1, you must refer to the entire path (“/Views/Page2.xaml”) rather than just the relative path to the other page (e.g. “Page2.xaml” or “./Page2.xaml”).

In this post, we’ll look at a way to allow Page-relative navigation within your Silverlight Pages.  The approach I’ll use will take advantage of the INavigate interface that was added in Silverlight 3 and the HyperlinkButton control that uses this interface to perform navigation when clicked.

The INavigate interface looks like this:

public interface INavigate
{
    // Methods
    bool Navigate(Uri source);
}

It’s a simple interface with a simple purpose: allow a component to provide a way to handle navigation to a Uri.  Right now, the only component built into Silverlight that actually takes advantage of it is the HyperlinkButton control.  We use this control quite often in Navigation-enabled applications, since it allows us to target a particular Frame control to navigate to a Page by Uri.  In the Silverlight Navigation Application project template, you’ll see XAML like this:

<navigation:Frame x:Name="ContentFrame" Style="{StaticResource ContentFrameStyle}"
                  Source="/Home" Navigated="ContentFrame_Navigated" NavigationFailed="ContentFrame_NavigationFailed">
</navigation:Frame>

And:

<HyperlinkButton x:Name="Link2" Style="{StaticResource LinkStyle}"
                 NavigateUri="/About" TargetName="ContentFrame" Content="about"/>

This all works because the Frame control implements INavigate.  In other words, there’s no magic there – you can use it too!  Here’s the way the HyperlinkButton works when you’re trying to target an INavigate (approximately :)):

  • For each parent FrameworkElement going up the visual tree from the HyperlinkButton…
    • Check to see if the FrameworkElement is an INavigate and that its name matches TargetName (ignored if TargetName is null or empty)
      • If so, call INavigate.Navigate() on the FrameworkElement
      • Otherwise, recursively search for an INavigate that’s properly named within each of the children of the FrameworkElement
    • If no properly named INavigate was found, keep moving up the visual tree

In other words, the HyperlinkButton will search its way up and down the visual tree (technically doing a pre-order breadth-first search from each parent of the HyperlinkButton, working its way up the visual tree) for an appropriate INavigate to call.

So, what does all this INavigate stuff mean to me?

With that technical detail out of the way, the question that arises is: how can we use this to enable Page-relative navigation?  Well, the reason navigation of a Frame works within Pages today is because the Frame control implements INavigate, and the HyperlinkButton works its way up the visual tree until it finds this.

For our purposes, this is great, since it means we can intercept the call to INavigate.Navigate() by implementing the interface somewhere between the HyperlinkButton and the Frame.  There’s a convenient place for this, since applications that use Page/Frame and HyperlinkButtons within those pages have a visual tree like this:

  • Application
    • Layout
      • Frame
        • Page
          • HyperlinkButton
          • Other controls
      • Other Controls

What I’m suggesting is that the Page we navigate to implement INavigate and turn page-relative Uri’s into application-relative Uri’s before handing them off the NavigationService (or the Frame) to actually perform the navigation.

I’ve gone ahead and extended my DynamicNavigation library to make DynamicPage implement INavigate to do what we want, but you could do the same on any subclass of Page (including every one of your Pages if you didn’t want a common base class).  Here’s my simple implementation (with a switch to turn off this feature on DynamicPages):

public bool RelativeLinks
{
    get { return (bool)GetValue(RelativeLinksProperty); }
    set { SetValue(RelativeLinksProperty, value); }
}

public static readonly DependencyProperty RelativeLinksProperty =
    DependencyProperty.Register("RelativeLinks", typeof(bool), typeof(DynamicPage), new PropertyMetadata(true));

private static readonly Uri basePlaceHolderUri = new Uri("none:///", UriKind.Absolute);

#region INavigate Members

public bool Navigate(Uri navigateUri)
{
    string original = navigateUri.OriginalString;
    if (RelativeLinks && !navigateUri.IsAbsoluteUri && !original.StartsWith("/"))
    {
        Uri result;
        if (NavigationService.CurrentSource.IsAbsoluteUri)
        {
            result = new Uri(NavigationService.CurrentSource, navigateUri);
        }
        else
        {
            Uri baseUri = new Uri(basePlaceHolderUri, NavigationService.CurrentSource);
            result = new Uri("/" + basePlaceHolderUri.MakeRelativeUri(new Uri(baseUri, navigateUri)).OriginalString,
                UriKind.Relative);
        }
        return NavigationService.Navigate(result);
    }
    else
    {
        return NavigationService.Navigate(navigateUri);
    }
}

#endregion

Now, within the page, you can use Page-relative Uri’s on HyperlinkButtons.  All you need to do is avoid setting a TargetName (either through a style or directly on the HyperlinkButton), and the Page will handle the navigation!

Page-relative Uri’s can take a variety of forms.  Assuming the Page you’re currently on is “/MyLibrary;component/Views/Main/Page1.xaml”, the following transformations will occur (as an example):

  • “Page2.xaml” –> “/MyLibrary;component/Views/Main/Page2.xaml”
  • “./Page2.xaml” –> “/MyLibrary;component/Views/Main/Page2.xaml”
  • “../Page3.xaml” –> “/MyLibrary;component/Views/Page3.xaml”
  • “../Secondary/Page3.xaml” –> “/MyLibrary;component/Views/Secondary/Page3.xaml”
  • “../../BasePage.xaml” –> “/MyLibrary;component/BasePage.xaml”
  • “SubMain/Page4.xaml” –> “/MyLibrary;component/Views/Main/SubMain/Page4.xaml”
  • “SubMain/Page4.xaml?a=b&c=d” –> “/MyLibrary;component/Views/Main/SubMain/Page4.xaml?a=b&c=d”
  • “/MyLibrary;component/Views/Main/Page5.xaml” –> “/MyLibrary;component/Views/Main/Page5.xaml” (no change)

Cool!  Can I see it in action?

Of course! :)  As always, you can play around with a running app that does this here:

Live sample of page-relative navigation

Click the link at the bottom of that page to peform a relative navigation that should take you on a journey between a number of pages – all of which navigate using relative navigation.

For reference, here is the file structure for the sample application.

File structure of the sample application. 

So, what’s your point?

Well, to sum it up – being able to do page-relative navigation increases the portability of your Pages.  It’s especially useful if you use it in conjunction with UriMapping.  With the technique above, the “relative-ness” refers to the user-facing Uri.  This means that you can create UriMappings in order to create a “virtual” file structure, and all of your relative links should continue to work (and new ones might be possible!).  Give it a shot and let me know what you think!  I’m continuing to experiment with Navigation, always on the lookout for things that might improve the experience when working with it.

Enough already!  Give me the goods!

Patience!  You didn’t really think I’d leave you hanging, did you?  Here you go:

Stop talking to yourself!

Okay. :)