Silverlight 3.0 ha introdotto l’oggetto Frame grazie al quale è possibile navigare le pagine che compongono l’intera applicazione, l’utilizzo di Frame è abbastanza semplice, supponendo infatti di avere la pagina principale della nostra applicazione (MainPage.xaml) definita in questo modo:

<Grid x:Name="LayoutRoot">
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="2*" />
        </Grid.RowDefinitions>
 
        <navigation:Frame Grid.Row="1"  Name="frame1" />
 
        <Button Content="Local" Height="33"  HorizontalAlignment="Left"
                Margin="12,8,0,0"
                Width="69"
                Click="button1_Click" />
        <Button Content="External" Height="33" HorizontalAlignment="Left"
                Margin="97,8,0,0"
                VerticalAlignment="Top"
                Width="69"
                Click="button2_Click" />
    </Grid>

e supponendo di avere nell’applicazione due pagine Local.xaml e External.xaml, per navigare da una pagina all’altra è sufficiente scrivere:

private void button1_Click(object sender, RoutedEventArgs e)
{
   frame1.Navigate(new Uri("/LocalPage.xaml", UriKind.Relative));
}
 
private void button2_Click(object sender, RoutedEventArgs e)
{
   frame1.Navigate(new Uri("/ExternalPage.xaml", UriKind.Relative));
}

Come sapete, quando le dimensioni dell’applicazione diventano ‘importanti’ è bene componentizzare affinchè le varie parti vengano scaricate on-demand evitando il download di parti inutilizzate e migliorando la velocità di startup. Tornando al nostro esempio, sarebbe interessante se la pagina ExternalPage.xaml che per come è stata organizzata la nostra applicazione risiede in uno xap separato, venisse scaricata esclusivamente quando l’utente la richiede e successivamente cachata attraverso un meccanismo trasparente e riutilizzabile: ecco dove entra in gioco INavigationContentLoader.

Implementando questa interfaccia e associandone un istanza alla proprietà ContentLoader è possibile gestire autonomamente l’elaborazione della richiesta delle varie pagine da visualizzare all’interno del frame.
Modifichiamo quandi la parte di xaml di MainPage relativa al frame in questo modo:

<navigation:Frame Grid.Row="1" Name="frame1">
    <navigation:Frame.ContentLoader>
        <local:MyContentLoader />
    </navigation:Frame.ContentLoader>
</navigation:Frame>

MyNavigationContentLoader è una classe che implementa INavigationContentLoader ed è riportata di seguito:

public class MyContentLoader : INavigationContentLoader
{
   private Dictionary<string, Assembly> assemblies = new Dictionary<string, Assembly>();
 
   public MyContentLoader()
   {
      //Adds current to list of loaded assemblies
      Assembly currentAssembly = Assembly.GetExecutingAssembly();
      this.AddAssembly(currentAssembly);
   }
 
   public IAsyncResult BeginLoad(Uri targetUri, Uri currentUri, AsyncCallback userCallback, object asyncState)
   {         
      var iar = new MyAsyncResult(asyncState);
      //Tries to load type from already loaded assemblies
      iar.Result = this.TryLoad(targetUri);
      //Type not loaded yet, download it asyncronously
      if (iar.Result == null)
      {
         string assemblyName = targetUri.ToString().Split('.')[0];
         //Probably it'a an external page not loaded yet...
         WebClient client = new WebClient();
         client.OpenReadCompleted += (s, e) =>
            {                  
               Uri assemblyUri = new Uri(assemblyName + ".dll", UriKind.Relative);
               StreamResourceInfo xapPackageSri = new StreamResourceInfo(e.Result, null);
               StreamResourceInfo assemblySri = Application.GetResourceStream(xapPackageSri, assemblyUri);
               //Load it
               Assembly assembly = new AssemblyPart().Load(assemblySri.Stream);
               iar.Result = assembly.CreateInstance(targetUri.ToString());
               //Add assemblies to list of loaded ones
               this.AddAssembly(assembly);
               //Page loaded, end asyncronous operation
               userCallback(iar);
            };
 
         client.OpenReadAsync(new Uri(assemblyName + ".xap", UriKind.Relative));
      }
      else
      {
         userCallback(iar);
      }
      return iar;
   }
 
   public bool CanLoad(Uri targetUri, Uri currentUri)
   {
      return true;
   }
 
   public void CancelLoad(IAsyncResult asyncResult) { }
 
   public LoadResult EndLoad(IAsyncResult asyncResult)
   {
      return new LoadResult((asyncResult as MyAsyncResult).Result);
   }
 
   private void AddAssembly(Assembly assembly)
   {
      string name = assembly.FullName.Split(',')[0];
      this.assemblies.Add(name, assembly);
   }
 
   private object TryLoad(Uri targetUri)
   {
      string assemblyName = targetUri.ToString().Split('.')[0];
      if (this.assemblies.ContainsKey(assemblyName))
         return this.assemblies[assemblyName].CreateInstance(targetUri.ToString());
      else
         return null;
   }
}

Il principio di funzionamento è abbastanza semplice, ci sono una serie di metodi CanLoad,CancelLoad, BeginLoad etc, che vengono invocati passando la Uri da caricare, la parte più complessa è il fatto che il caricamento potrebbe avvenire in maniera asincrona ecco perchè tra i paramentri viene passata anche IAsyncResult.
Nel nostro caso tutta l’implementazione è racchiusa nel metodo CanLoad il quale in TryLoad verifica se la pagina è già disponibile altrimenti attraverso WebClient la scarica e la mette nella “cache” interna a MyContentLoader.
Terminata l’operazione viene invocato il metodo callback passando un implementazione di MyAsyncResult che espone la pagina caricata attraverso la proprietà Result:

public class MyAsyncResult : IAsyncResult
{
   public MyAsyncResult(object asyncState)
   {
      this.AsyncState = asyncState;
      this.AsyncWaitHandle = new ManualResetEvent(true);
   }
 
   public object Result { get; set; }
 
   public object AsyncState { get; private set; }
 
   public WaitHandle AsyncWaitHandle { get; private set; }     
 
   public bool CompletedSynchronously
   {
      get { return true; }
   }
 
   public bool IsCompleted
   {
      get { return true; }
   }      
}

il risultato della chiamata è l’invocazione del metodo EndLoad nel quale viene creato e restituito un oggetto LoadResult contenente la pagina richiesta.

L’esempio fa uso della convenzione per la quale la Uri da passare è nella forma “Assembly.PageName” e presuppone che lo xap contenente la pagina si chiami esattamente come il nome dell’assembly:

private void button1_Click(object sender, RoutedEventArgs e)
 {
    frame1.Navigate(new Uri("CustomContentLoaderDemo.LocalPage", UriKind.Relative));
 }
 
 private void button2_Click(object sender, RoutedEventArgs e)
 {
    frame1.Navigate(new Uri("ExternalPages.ExternalPage", UriKind.Relative));
 }

A questo punto, supponendo che la nostra applicazione Silverlight 4.0 sia organizzata in questo modo (notate la presenza di ExternalPages.xap)

image

Premendo i vari buttons, la prima volta che cercheremo di visualizzare ExternalPage questa verrà prima scaricata e poi visualizzata.

Questo è solo una delle possibili applicazioni, ogni volta che volete avere il controllo totale della navigazione all’interno di un applicazione Silverligt 4.0 ricordatevi di questa interfaccia che è la stessa implementata da PageResourceContentLoader ovvero il gestore di navigazione predefinito dell’oggetto Frame.

Buona navigazione!