Archive for September, 2009

Relative hyperlinks with Silverlight navigation

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. :)

, , , ,

6 Comments

Update 2: Displaying background activity in a Silverlight RIA application

Hi folks!  It’s been a little while since I’ve blogged, but fear not, I’m still watching and hoping to blog more in the coming weeks.

In the meantime, it’s been brought to my attention by a few people now that there are a few issues with the Activity control, and I wanted to address them.

  1. Performance – A bit of a mea culpa on my part.  I included a feature for the control that I’ve called “AutoBind”, whereby it would watch for changes to the visual tree of its contents and subscribe to any control that has a property whose name matches “ActivityPropertyName”.  By default, this is great when working with .NET RIA Service’s DomainDataSource, since ActivityPropertyName is “IsBusy” by default, but it also turns out to be a hefty amount of work, constantly searching the visual tree and registering/unregistering event handlers.  This wouldn’t be so bad, except that AutoBind defaults to true, meaning even if you’re not using this functionality, the Activity control is doing the work.  Below, you’ll find a new version of the Activity control that amends the situation, making AutoBind default to false.  This is the only change to the control since my last post on the matter, but I’d love to hear if you have thoughts about the control, this feature, or other requests!
  2. .NET RIA Services’ Business Application Template – So, the .NET RIA Services guys included with their July Preview a project template to help you get started with a RIA Services application.  In this template, they included a dll with the Activity control in it.  A few people have noted that it has a slightly different API than they’re used to seeing with the control, which is due to their use of the original version of the control that I posted (its API has changed a bit since then, and a number of bugs were fixed, including some layout issues).  Anyhoo, feel free to pick up the latest version below!

So there you have it – just a few changes and things to note.  You can find the control here:

Thanks for all the great feedback since I first posted this control.  As usual, let me know if you have questions or issues!

, , , , ,

33 Comments