Silverlight 4 Rough Notes: Taking Control of Navigation

Note – these posts are put together after a short time with Silverlight 4 as a way of providing pointers to some of the new features that Silverlight 4 has to offer. I’m posting these from the PDC as Silverlight 4 is announced for the first time so please bear that in mind when working through these posts.

Silverlight 3 introduced a new navigation style of application where a Silverlight application is structured out of a set of Pages which are displayed by a Frame that navigates to those pages ( by changing its Uri Source property to point at a specific page ). This is underpinned by a NavigationService which supports the idea of navigating forward/backwards and the functionality also integrates with the browser’s history and address bar. As pages are navigated to there’s a nice overload where they can run code and ( e.g. ) interrogate the query string to grab parameters from it.

It means that a Silverlight application can behave exactly like the rest of the web with forward/backward navigation and can offer hyperlinked resources in a very natural way into content managed by Silverlight. I wrote about this a little when Silverlight 3 was on the horizon ( here and here ).

You can see this by just doing File->New->Project, choosing a Silverlight Navigation Application;

image

and letting the Visual Studio Templates do the work. You’ll find that you have a project that looks like this;

image

and so in VS 2010 B2 with Silverlight 4 I get a MainPage.xaml that has a Frame on it and I also get a folder of Views for About, ErrorWindow and Home made up of XAML and code and I also get some help around mapping URI’s to nicer syntax with the UriMapper that the tooling has thrown in for me.

The application runs and works out of the box and navigates between the About page and the Home page and if you type a bad URI it’ll throw up the ErrorWindow ( which is not itself a page but is produced by the code handling the NavigationFailed event on the Frame ).

It’s not immediately obvious but the target URI of a navigation in Silverlight 3 needs to be a XAML page within the XAP of the Silverlight application doing the navigation.

In some ways that’s a little bit like ASP.NET Web Forms in that in the web forms world your URI had to actually point at a physical [.ASPX/.ASMX/.ASHX] file on the disk in order that the framework could pick up the right handler and do something with the request to render a response.

It’s not a perfect analogy but Silverlight 4 is a little bit more like ASP.NET (MVC) Routing in that you can get involved in the navigation request processing in order to produce a response and the requirement that the target that is being navigated to corresponds an actual XAML-based page is gone although it’s still the default behaviour.

There’s a new interface in Silverlight 4 called INavigationContentLoader;

  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);
  }

and the framework now implements this on its default implementation PageResourceContentLoader which has the same behaviour as found in Silverlight 3.

However, the new Frame.ContentLoader property means that you can plug your own INavigationContentLoader implementation into a Frame and take control of the behaviour yourself and do pretty much whatever you like in order to produce some content to render into the Frame as a response to the navigation request.

As a simple example of this, imagine my Silverlight site of origin has an XML file called navigationData.xml which defines the navigation for my Silverlight app;

<paths>
  <path name="customer">
    <verb name="display"
          controlType="SilverlightApplication67.CustomerDisplay">
      <parameters>
        <parameter name="CustomerName"
                   type="System.String"/>
        <parameter name="CustomerId"
                   type="System.Int32"/>
        <parameter name="CustomerDOB"
                   type="System.DateTime"/>
      </parameters>
    </verb>
    <verb name="edit"
          controlType="SilverlightApplication67.CustomerEdit">
    </verb>
  </path>
  <path name="address">
    <verb name="add"
          controlType="SilverlightApplication67.AddressAdd"/>     
  </path>
</paths>

So the idea is that you can use a URI to with my Silverlight application such as http://mikesapp.html# and then follow it with something like;

  • customer/display/Fred Smith/121/01-01-1950 to display a custom UserControl of type SilverlightApplication67.CustomerDisplay and feed it parameters CustomerName/CustomerId/CustomerDOB
  • customer/edit to display a custom UserControl of type SilverlightApplication67.CustomerEdit
  • address/add to display a custom UserControl of type SilverlightApplication67.AddressAdd

and those correspond to the simple definitions in the XML file.

Note: I started trying to build a simple example of this based on reflection but it turned into too-long-an-example so I switched to XML to chop some code out. It’s still too long 🙂

It’s not meant to be very realistic but hopefully it’s enough to illustrate how that the definition of where and how you find content for navigation is now defined by me. With that in place, I sketched out an implementation of INavigationContentLoader around it – note, there’s quite a chunk of code here but it’s mostly a second, internal class to implement IAsyncResult;

  public class XmlDrivenContentLoader : INavigationContentLoader
  {
    public XmlDrivenContentLoader()
    {
      // Not entirely sure of the threading model here
      threadId = Thread.CurrentThread.ManagedThreadId;
    }
    public IAsyncResult BeginLoad(Uri targetUri, Uri currentUri, 
      AsyncCallback userCallback, object asyncState)
    {
      CheckThread();

      XmlDrivenAsyncResult asyncResult = null;

      if (navigationData == null)
      {
        asyncResult = new XmlDrivenAsyncResult(targetUri, asyncState, userCallback);

        if (!loading)
        {
          loading = true;
          XmlNavigationData.LoadFromXmlCompleted += OnLoadNavigationDataCompleted;
          XmlNavigationData.LoadFromXmlAsync(new Uri("navigationData.xml", UriKind.Relative));
        }
        asyncResult.RegisterWaitForNavigationDataLoaded();
      }
      else
      {
        asyncResult = new XmlDrivenAsyncResult(targetUri, true, userCallback, asyncState);
        asyncResult.SetDone();
      }
      return (asyncResult);
    }
    void OnLoadNavigationDataCompleted(object sender, XmlLoadAsyncCompletedEventArgs e)
    {
      // Note - throwing away all the errors here 😦
      navigationData = e.LoadedData;
    }
    public bool CanLoad(Uri targetUri, Uri currentUri)
    {
      CheckThread();
      // We always say yes because loading may involve loading our XML file
      // for the first time and that's async so...
      return (true);
    }
    public void CancelLoad(IAsyncResult asyncResult)
    {
      CheckThread();
    }
    public LoadResult EndLoad(IAsyncResult asyncResult)
    {
      CheckThread();

      XmlDrivenAsyncResult xmlAsyncResult = (XmlDrivenAsyncResult)asyncResult;

      LoadResult result = null;

      // We do the work here because we're not really asynchronous other than
      // the initial load of the XML definition file.
      if (navigationData != null)
      {
        try
        {
          ParsedUri parsedUri = ParsedUri.Parse(xmlAsyncResult.TargetUri);

          UserControl control = navigationData.FindUserControlForParsedUri(parsedUri);

          result = new LoadResult(control);
        }
        catch (ArgumentException ex)
        {
          // Navigate to an error page?
          result = new LoadResult(new ErrorPage());
        }
      }
      else
      {
        // Navigate to an error page?
        result = new LoadResult(new ErrorPage());
      }
      return (result);
    }
    void CheckThread()
    {
      if (Thread.CurrentThread.ManagedThreadId != threadId)
      {
        throw new NotSupportedException("class has thread affinity");
      }
    }
    XmlNavigationData navigationData;
    bool loading;
    int threadId;


    class XmlDrivenAsyncResult : IAsyncResult
    {
      public XmlDrivenAsyncResult(Uri targetUri, object asyncState, AsyncCallback callback)
      {
        this.targetUri = targetUri;
        this.asyncState = asyncState;
        this.callback = callback;
      }
      public XmlDrivenAsyncResult(Uri targetUri, bool done, AsyncCallback callback, object asyncState) :
        this(targetUri, asyncState, callback)
      {
        this.done = done;
      }
      public object AsyncState
      {
        get { return (asyncState); }
      }
      public System.Threading.WaitHandle AsyncWaitHandle
      {
        get
        {
          if (evt == null)
          {
            evt = new ManualResetEvent(done);
          }
          return (evt);
        }
      }
      public bool CompletedSynchronously
      {
        get { return (false); }
      }

      public bool IsCompleted
      {
        get { return (done); }
      }
      internal void RegisterWaitForNavigationDataLoaded()
      {
        XmlNavigationData.LoadFromXmlCompleted += (s, e) =>
        {
          SetDone();
        };
      }
      internal void CallUsersCallbackOnDone()
      {
        if (callback != null)
        {
          callback(this);
        }
      }
      internal void SetDone()
      {
        done = true;

        if (evt != null)
        {
          evt.Set();
        }
        CallUsersCallbackOnDone();
      }
      public Uri TargetUri
      {
        get
        {
          return (targetUri);
        }
      }
      AsyncCallback callback;
      volatile ManualResetEvent evt;
      Uri targetUri;
      object asyncState;
      volatile bool done;
    }
  }

The basic idea of my XmlContentDriverContentLoader is that it makes use of a separate class XmlNavigationData which needs to load the XML definition asynchronously.

So, when we get a BeginLoad event we may need to asynchronously load our NavigationData or we may not ( i.e. we only try and load it once ). So, I jump through a hoop or two to make sure that if it needs loading we load it asynchronously. When the async stuff is done ( which might be immediately if that data is already loaded ) we’ll get a call to EndLoad and the link between these 2 functions is the IAsyncResult that travels from one to the other – in my case it’s an XmlDrivenAsyncResult and it makes sure to “carry” the targetUri that was requested in BeginLoad through to EndLoad as that’s where I’ll be needing it.

So, EndLoad is where the work actually happens and in all but the initial instances ( where the XmlNavigationData needs loading asynchronously ) it’d be called immediately from BeginLoad.

In EndLoad I do a very basic attempt to;

  • Split the targetUri requested apart into Path/Verb/Param1/Param2/Param3/…/ParamN using a helper class called ParsedUri
  • Ask my XmlNavigationData class to find a matching definition from the XML file, instantiate the UserControl referenced by that file for that URI and set any parameters on it as necessary from the URI.
  • Return a LoadResult to the caller with that UserControl ( and, it needs to be a UserControl at the time of writing ) to the framework that called EndLoad

In the event of failure, I simply return from EndLoad with an ErrorPage UserControl which looks like;

<UserControl x:Class="SilverlightApplication67.ErrorPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
    
    <Grid x:Name="LayoutRoot" Background="White">
        <Viewbox>
            <TextBlock
                Text="Error"
                Foreground="Red" />
        </Viewbox>
    </Grid>
</UserControl>

That XmlNavigationData class ( and its EventArgs derived friend ) look like;

  public class XmlLoadAsyncCompletedEventArgs : AsyncCompletedEventArgs
  {
    public XmlLoadAsyncCompletedEventArgs(Exception error,
      bool cancelled, object state)
      : base(error, cancelled, state)
    {
    }
    public XmlNavigationData LoadedData { get; set; }
  }
  public class XmlNavigationData
  {
    public static event EventHandler<XmlLoadAsyncCompletedEventArgs> LoadFromXmlCompleted;

    class Path
    {
      public string Name { get; set; }
      public List<Verb> Verbs { get; set; }
    }
    class Verb
    {
      public string Name { get; set; }
      public Type ControlType { get; set; }
      public List<Parameter> Parameters { get; set; }
    }
    class Parameter
    {
      public string Name { get; set; }
      public Type Type { get; set; }
    }
    public static void LoadFromXmlAsync(Uri xmlUri)
    {
      WebClient client = new WebClient();
      client.OpenReadCompleted += OnOpenReadCompleted;
      client.OpenReadAsync(xmlUri);
    }
    public UserControl FindUserControlForParsedUri(ParsedUri parsedUri)
    {
      UserControl control = null;

      Path path = this.paths.FirstOrDefault(p => p.Name == parsedUri.Path);

      if (path != null)
      {
        Verb verb = path.Verbs.FirstOrDefault(
          v => ((v.Name == parsedUri.Verb) &&
            (v.Parameters.Count == parsedUri.Parameters.Count)));

        if (verb != null)
        {
          // Looks like a basic match but can we do the type conversions?
          bool valid = true;
          List<object> convertedValues = new List<object>();

          for (int i = 0; valid && (i < verb.Parameters.Count); i++)
          {
            Parameter p = verb.Parameters;
            string value = parsedUri.Parameters;

            try
            {
              convertedValues.Add(Convert.ChangeType(value, p.Type, null));
            }
            catch (InvalidCastException)
            {
              valid = false;
            }
          }
          if (valid)
          {
            control = MakeUserControl(verb, convertedValues);
          }
        }
      }
      return (control);
    }
    static UserControl MakeUserControl(Verb verb, List<object> parameterValues)
    {
      UserControl uc = (UserControl)Activator.CreateInstance(verb.ControlType);

      for (int i = 0; i < verb.Parameters.Count; i++)
      {
        PropertyInfo propInfo = verb.ControlType.GetProperty(verb.Parameters.Name);
        propInfo.SetValue(uc, parameterValues, null);
      }
      return (uc);
    }
    static void OnOpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
    {
      Exception error = e.Error;
      bool cancelled = e.Cancelled;
      object state = e.UserState;
      XmlNavigationData data = null;

      if ((error == null) && (!cancelled))
      {
        try
        {
          try
          {
            XElement doc = XElement.Load(e.Result);

            data = new XmlNavigationData();

            data.paths = new List<Path>(
              from p in doc.DescendantsAndSelf("path")
              select new Path()
              {
                Name = (string)p.Attribute("name"),
                Verbs = (
                          from v in p.DescendantsAndSelf("verb")
                          select new Verb()
                          {
                            Name = (string)v.Attribute("name"),
                            ControlType = Type.GetType((string)v.Attribute("controlType")),
                            Parameters =
                            (
                              from pa in v.DescendantsAndSelf("parameter")
                              select new Parameter()
                              {
                                Name = (string)pa.Attribute("name"),
                                Type = Type.GetType((string)pa.Attribute("type"))
                              }
                            ).ToList()
                          }).ToList()
              });
          }
          finally
          {
            e.Result.Close();
          }
        }
        catch (Exception ex)
        {
          error = ex;
        }
      }
      if (LoadFromXmlCompleted != null)
      {
        LoadFromXmlCompleted(null,
          new XmlLoadAsyncCompletedEventArgs(error, cancelled, state)
          {
            LoadedData = data
          });
      }
    }
    List<Path> paths;    
  }

and so it really just has a little functionality to load up the definitions of Paths/Verbs/Parameters from the XML file ( async. using LINQ to XML ) and also to attempt to search those definitions for the right type of UserControl to match a parsed URI that’s passed into it.

The only missing bit is the simple ParsedUri class;

  public class ParsedUri
  {
    public static ParsedUri Parse(Uri uri)
    {
      ParsedUri parsedUri = new ParsedUri();

      string[] components = uri.ToString().Split('/');

      if (components.Length < 2)
      {
        throw new ArgumentException("uri");
      }
      parsedUri.Path = components[0];
      parsedUri.Verb = components[1];
      parsedUri.Parameters = new List<string>();

      if (components.Length > 2)
      {
        for (int i = 2; i < components.Length; i++)
        {
          parsedUri.Parameters.Add(components);
        }
      }
      return (parsedUri);
    }
    public string Path { get; set; }
    public string Verb { get; set; }
    public List<string> Parameters { get; set; }
  }

Too Much Code – So What Does All This Mean?

With all of that stuff in place ( and working with the original navigationData.xml file I listed at the top of the post ) what I can do is to set up a frame that uses my custom INavigationContentLoader;

    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.Resources>
            <local:XmlDrivenContentLoader
                x:Key="xmlContentLoader" />
        </Grid.Resources>
        <navigation:Frame
            Name="frame1" 
            ContentLoader="{StaticResource xmlContentLoader}"/>
    </Grid>

Now if I run up that app and navigate using a bad URI like this one then I’ll see my ErrorPage;

image

If I was to add a new UserControl to the project like this CustomerDisplay one with code/XAML as;

<UserControl x:Class="SilverlightApplication67.CustomerDisplay"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
    
    <Grid x:Name="LayoutRoot" Background="White">
        <Viewbox>
            <StackPanel>
            <TextBlock
                Text="Customer Display" />
                <TextBlock
                    Text="{Binding Path=CustomerName,StringFormat=Name \{0\}}" />
                <TextBlock
                    Text="{Binding Path=CustomerId,StringFormat=Id \{0\}}" />
                <TextBlock
                    Text="{Binding Path=CustomerDOB,StringFormat=DOB \{0\}}" />
            </StackPanel>
        </Viewbox>
    </Grid>
</UserControl>

namespace SilverlightApplication67
{
  public partial class CustomerDisplay : UserControl
  {
    public CustomerDisplay()
    {
      InitializeComponent();

      this.Loaded += (s, e) =>
        {
          this.DataContext = this;
        };
    }
    public string CustomerName { get; set; }
    public int CustomerId { get; set; }
    public DateTime CustomerDOB { get; set; }
  }
}

then I can navigate to see that control and pass it parameters on the URI;

image

so notice that the navigation using that;

#customer/display/Bob Jones/12176/01-03-1945

is being handled entirely dynamically by the implementation of INavigationContentHandler which is looking up what to do about it in the XML file and finding the right UserControl and then attempting to pass those parameters to it based on the XML definition ( yes, the date handling looks wrong ).

Whilst my implementation is pretty limited and only tries to dynamically create types from the local assembly, it could easily be extended to reach out to other assemblies and load those dynamically at runtime.

Hmmm….that sounds like a job for the Managed Extensibility Framework or MEF and I can definitely see a strong link here between navigation and extension assemblies contributing new UserControls and navigation routes around them via MEF rather than just my little toy XML example.

Note – I also built the CustomerEdit and AddressAdd user controls as you can see when I navigate to them below but I left them blank other than a piece of text to say what they were;

image image