Opening up Silverlight 4 Navigation: Introduction to INavigationContentLoader
Quick links to followup posts:
- Event-based and Error-Handling INavigationContentLoaders
- Authentication/Authorization in an INavigationContentLoader
- On-demand loading of assemblies with Silverlight Navigation – Revisited for Silverlight 4 Beta
If you haven’t noticed by now (or been following my previous blog posts), I happen to really enjoy exploring the Navigation feature in Silverlight. A while back, I posted a number of workarounds and tips for some desirable scenarios using the Navigation feature in Silverlight 3. Then… I went silent. And the reason: I’ve been waiting for an extensibility point to be opened up in Silverlight navigation. In the Silverlight 4 beta, that extensibility point has arrived as the INavigationContentLoader.
The ContentLoader extensibility point allows developers to handle the page loading process themselves, much as you would with a GenericHandler in ASP.NET. You’re given some context on what to load (in this case a target Uri), then it’s up to you as the developer to decide how to load it. Your handling of these requests can be as specialized or as generalized as you find appropriate.
On the surface, opening up this extensibility point is a rather small change, but it’s actually very powerful when used to solve general problems. In this post, I’ll build a basic INavigationContentLoader that loads pages based on class name rather than project file structure. In future posts, I hope to explore some more general (and possibly composable) ContentLoaders.
So… Tell me about this INavigationContentLoader interface.
Fundamentally, the INavigationContentLoader interface just provides a cancellable asynchronous pattern for loading content based upon a target Uri. You can set a ContentLoader on the Frame control, which will cause the NavigationService to use your ContentLoader rather than built-in one, which is publicly available as the PageResourceContentLoader.
Here’s what the interface looks like:
public interface INavigationContentLoader { IAsyncResult BeginLoad(Uri targetUri, Uri currentUri, AsyncCallback userCallback, object asyncState); void CancelLoad(IAsyncResult asyncResult); bool CanLoad(Uri targetUri, Uri currentUri); LoadResult EndLoad(IAsyncResult asyncResult); }
The basic flow of its use with the Frame control and the NavigationService in the Silverlight SDK is as follows:
- Frame/NavigationService receives a request to navigate to a Uri
- Frame/NavigationService uses its UriMapper to map the Uri it was given to a Uri for use by the content loader
- Frame/NavigationService passes the current (post-mapping) Uri to the content loader as well as the target (post-mapping) Uri to the content loader, calling BeginLoad
- Frame/NavigationService waits for a callback from the content loader. If StopLoading() was called, the Frame/NavigationService calls CancelLoad
- Upon being called back, the Frame/NavigationService calls EndLoad() on the content loader, receiving a LoadResult
- If the LoadResult contains a redirect Uri, the Frame/NavigationService begins a new load with that Uri as the target – without adding a history entry for the target Uri
- Otherwise, the Frame/NavigationService attempts to display the fully-initialized UserControl or Page in the LoadResult
Great, I guess… Can you show me how to write one?
Sure! First, let’s choose a type of ContentLoader we’d like to create. For this ContentLoader, we’ll begin to break free of the file structure-driven bonds of the existing ContentLoader provided in Silverlight 3. Instead, we’ll allow navigation to Pages based upon the type name of the Page. For example, if I have a page called “MyNamespace.MyPage”, I should be able to navigate a frame to the relative Uri “MyNamespace.MyPage” or “/MyNamespace.MyPage” rather than having to point to the XAML file for the Page.
There are two parts to writing a ContentLoader: implementing the interface and implementing IAsyncResult to store the results of your asynchronous operation. There is ample documentation for writing an IAsyncResult out there, so I’ll conveniently ignore that for now :).
So, here’s our plan:
- Implement INavigationContentLoader
- Write a helper function that extracts a type name from a Uri
- Implement CanLoad to check to see if the type specified by the targetUri can be found and has a default constructor
- Implement BeginLoad to produce an instance of the page specified by the type in targetUri, store it in an IAsyncResult, and call the consumer’s callback
- Implement EndLoad to wrap the page in a LoadResult and return it to the consumer
- Implement IAsyncResult
- Add a “Result” property to hold the constructed page
- Use the ContentLoader
- Specify the ContentLoader in XAML
- Update Hyperlinks to navigate using our new Uri scheme
Alright, let’s hop to it!
Implementing INavigationContentLoader
Let’s start by implementing the interface:
public class TypenameContentLoader : INavigationContentLoader { }
Ok – easy! Granted, it’s a little empty, but it’s a start! Now, let’s flesh it out!
To make sense of our Uri scheme, we’ll write a method to extract a Type name from a relative Uri:
private string GetTypeNameFromUri(Uri uri) { if (!uri.IsAbsoluteUri) uri = new Uri(new Uri("dummy:///", UriKind.Absolute), uri.OriginalString); return Uri.UnescapeDataString(uri.AbsolutePath.Substring(1)); }
This method is pretty straightforward. First, it turns the relative Uri into an absolute one, using a dummy protocol. Then, it uses the Path of that Uri as the Type name (minus a leading slash and after unescaping any Uri-encoded values (e.g. %20 becomes a space character, so assembly-qualified type names can be used). The reason we don’t just use the OriginalString on the uri is because the Uri may contain a query string or a fragment (e.g. “TypeName?query=string#fragment”) – in this case we only want the Path in the Uri.
Next, we’ll implement CanLoad, which is called by the Frame/NavigationService before attempting to load. Here, we’ll check to see that the type exists and has a default constructor – both of which we’ll need in order to create an instance of the page:
public bool CanLoad(Uri targetUri, Uri currentUri) { string typeName = GetTypeNameFromUri(targetUri); Type t = Type.GetType(typeName, false, true); if (t == null) return false; var defaultConstructor = t.GetConstructor(new Type[0]); if (defaultConstructor == null) return false; return true; }
By implementing CanLoad, we can now safely assume (barring any concurrency-related race condition that could arise, but doesn’t apply for the ContentLoader we’re writing in this post because it operates neither statefully nor asynchronously) that the Frame/ContentLoader will only call BeginLoad after making this check. As such, our BeginLoad can just go ahead and try to create an instance of the type specified in the targetUri. Then, it must store the instance in an IAsyncResult, invoke the userCallback, and wait for EndLoad to be called:
public IAsyncResult BeginLoad(Uri targetUri, Uri currentUri, AsyncCallback userCallback, object asyncState) { var result = new TypenameContentLoaderAsyncResult(asyncState); Type t = Type.GetType(GetTypeNameFromUri(targetUri), false, true); object instance = Activator.CreateInstance(t); result.Result = instance; userCallback(result); return result; }
Our implementation of EndLoad will extract the instance of the page from the IAsyncResult and wrap it in a LoadResult. LoadResult is a simple class we introduced to represent the result of a load operation. It currently allows for two possible outcomes from a ContentLoader: (1) return/display a Page or UserControl, (2) provide a Uri to which users should be redirected (no history entry for the original page will be created).
public LoadResult EndLoad(IAsyncResult asyncResult) { return new LoadResult(((TypenameContentLoaderAsyncResult)asyncResult).Result); }
The final method of the INavigationContentLoader interface is CancelLoad. In our case, creating an instance of the page happens synchronously, so cancellation doesn’t really have any meaning or value. Other ContentLoaders, however, might use this method to stop a download or abort a large operation:
public void CancelLoad(IAsyncResult asyncResult) { return; }
And that’s it! Our Typename-based content loader is complete! A simple implementation of IAsyncResult will allow us to compile:
internal class TypenameContentLoaderAsyncResult: IAsyncResult { public object Result { get; set; } // Other IAsyncResult members }
Consuming the ContentLoader
Consuming our new ContentLoader is as simple as setting the ContentLoader property on your Frame in XAML:
<navigation:Frame xmlns:loader="clr-namespace:TypenameContentLoader.ContentLoader" x:Name="ContentFrame" Source="/TypenameContentLoader.Views.Home"> <navigation:Frame.ContentLoader> <loader:TypenameContentLoader /> </navigation:Frame.ContentLoader> </navigation:Frame>
You’ll note that in addition to setting the ContentLoader, I’ve set the source for the Frame control to match our new Uri scheme. We must also do the same for other hyperlinks in the application:
<HyperlinkButton x:Name="Link1" Style="{StaticResource LinkStyle}" NavigateUri="/TypenameContentLoader.Views.Home" TargetName="ContentFrame" Content="home"/> <HyperlinkButton x:Name="Link2" Style="{StaticResource LinkStyle}" NavigateUri="/TypenameContentLoader.Views.About, TypenameContentLoader, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" TargetName="ContentFrame" Content="about"/> <HyperlinkButton x:Name="Link3" Style="{StaticResource LinkStyle}" NavigateUri="/TypenameContentLoader.Views.ParameterizedPage?a=100&b=1000" TargetName="ContentFrame" Content="parameterized?a=100&b=1000"/>
And there you have it! The application is fully configured to use the new ContentLoader. You’ll notice that the Frame will happily pass along the complex Uris in all three of the hyperlinks above, and the loader will load them correctly. Query string values still get parsed by the navigation service, and all is well with the world :)
Ok, so now I can build a ContentLoader – can I actually see the code?
Yep, and I’ll do you one better! You can also try the live sample!
And, just to prove that query strings “just work”, try this link into the application with a long list of key/value pairs that get used by the page:
Feel free to play around with it and let me know what you think!
(Also note the screenshot above using Google Chrome – now a supported browser with Silverlight 4!)
Ok, so all of this is cool, but why is it useful? What’s next?
This Typename-based content loader is a neat trick to try, but its usefulness is still pretty limited. The extensibility point itself, however, is very powerful. Instead of having to come up with hacks and workarounds for things like on-demand downloading of assemblies containing pages as I did before, we can bake that logic right into a content loader! If you are an adherent of the MVVM pattern, you can use your content loader to connect your view to your model. If all you need to do is specify constructor parameters to your pages, you now have that option! If you need to pre-load data from a web service before navigating to a page, a content loader can help!
The ContentLoader extensibility point introduced in the Silverlight 4 beta really opens up Silverlight Navigation, allowing you to interject in the page loading process and enabling you to make navigation work with whatever framework or application structure you choose.
I’ve got a bunch of ideas for useful ContentLoaders that I’ll explore (and hopefully blog about) going forward. If you’ve got ideas, questions, or feedback, please let me know! I look forward to hearing from you!