| Comments

Someone posed this question (“Can you use IsolatedStorage in Silverlight as a more reliable browser caching technique?”) to me and I answered with my usual optimistic “in theory, yes” answer.  Of course I had never tried it which is horrible to answer that to someone without trying it.  In working on creating some Silverlight business application samples, I figured I should probably look at this scenario to see if a) it would work and b) it makes sense.  I’ll at least try to answer “a” here.

The Setup

Let’s look at the setup.  The goal here is to have a lean startup application and only request additional components/controls/applications when needed, thus producing the best startup experience for the end user.  I decided to use an existing simple sample I already had which does dynamic XAP loading for an application.  Basically the application loads a simple interface of a search box and button.  Since the user hasn’t interacted with data yet, we didn’t want to add the payload of DataGrid into the initial application.  Once the button is clicked the application is requested, loaded into memory and displayed.  The relevant code from that sample is shown here:

   1: private void Button_Click(object sender, RoutedEventArgs e)
   2: {
   3:     panel1.Children.Clear();
   4:  
   5:     WebClient c = new WebClient();
   6:     c.OpenReadCompleted += new OpenReadCompletedEventHandler(c_OpenReadCompleted);
   7:     c.OpenReadAsync(new Uri("DynamicXapDataGrid_CS.xap", UriKind.Relative));
   8: }
   9:  
  10: void c_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
  11: {
  12:     string appManifest = new StreamReader(Application.GetResourceStream(new StreamResourceInfo(e.Result, null), new Uri("AppManifest.xaml", UriKind.Relative)).Stream).ReadToEnd();
  13:  
  14:     Deployment deploy = XamlReader.Load(appManifest) as Deployment;
  15:  
  16:     Assembly asm = null;
  17:  
  18:     foreach (AssemblyPart asmPart in deploy.Parts)
  19:     {
  20:         string source = asmPart.Source;
  21:         StreamResourceInfo streamInfo = Application.GetResourceStream(new StreamResourceInfo(e.Result, "application/binary"), new Uri(source, UriKind.Relative));
  22:  
  23:         if (source == "DynamicXapDataGrid_CS.dll")
  24:         {
  25:             asm = asmPart.Load(streamInfo.Stream);
  26:         }
  27:         else
  28:         {
  29:             asmPart.Load(streamInfo.Stream);
  30:         }
  31:     }
  32:  
  33:     panel1.DataContext = FakeData.GetCustomers(LastNameSearch.Text);
  34:  
  35:     UIElement myData = asm.CreateInstance("DynamicXapDataGrid_CS.Page") as UIElement;
  36:  
  37:     panel1.Children.Add(myData);
  38:     panel1.UpdateLayout();
  39:  
  40: }

You can see that in line 7 above we are using OpenRead to request our satellite XAP which contains our control/logic for our DataGrid, and then starting in line 18 we look for the relevant assembly to load and instantiate (line 35) and put into the visual tree (line 37).

Browser Caching The Request

If we would execute the app (clicking the button) several times, we’d expect that the first time the browser would request the satellite XAP and deliver it.  We would also generally expect the browser to cache that request for later.  So that if we click the button again, it would retrieve from cache and operate faster.

But what about corporate policies and other types of things that may prevent the browser from caching that request?  What if we want a different mechanism for caching our object requests and satellite assemblies?

An alternate method, use IsolatedStorage

So how can we refactor the above to use Silverlight’s IsolatedStorage area as a cache for our satellite assemblies?  let’s take a look.  First if you follow similar techniques as you might in ASP.NET for caching, you’d first check the cache before you load something.  If it isn’t there, you’d load it, add it to the cache and then use it for later as well.  Let’s do that here.

Instead of making the OpenRead request on the button click, we should first check to see if the satellite XAP has been loaded into IsoStore:

   1: private void Button_Click(object sender, RoutedEventArgs e)
   2: {
   3:     panel1.Children.Clear();
   4:  
   5:     using (var store = System.IO.IsolatedStorage.IsolatedStorageFile.GetUserStoreForApplication())
   6:     {
   7:         if (!store.FileExists("DynamicXapDataGrid_CS.xap"))
   8:         {
   9:             WebClient c = new WebClient();
  10:             c.OpenReadCompleted += new OpenReadCompletedEventHandler(c_OpenReadCompleted);
  11:             c.OpenReadAsync(new Uri("DynamicXapDataGrid_CS.xap", UriKind.Relative));
  12:         }
  13:         else
  14:         {
  15:             LoadData();
  16:         }
  17:     }
  18: }

Line 5 shows us initating the IsolatedStorage area.  (For more information on IsolatedStorage usage, see this video.)  We first look to see if the file exists in the store.  If it is not in there, then we use our OpenRead functionality to start the request.  If the file is there, then we call our refactored LoadData() function, which we’ll look at in a moment.  The completed event for our OpenReadAsync call looks like this:

   1: void c_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
   2: {
   3:     using (var store = System.IO.IsolatedStorage.IsolatedStorageFile.GetUserStoreForApplication())
   4:     {
   5:         System.IO.IsolatedStorage.IsolatedStorageFileStream fileStream;
   6:  
   7:         // create the file
   8:         fileStream = store.CreateFile("DynamicXapDataGrid_CS.xap");
   9:  
  10:         // write out the stream to isostore
  11:         WriteStream(e.Result, fileStream);
  12:  
  13:         fileStream.Close();
  14:     }
  15:     LoadData();
  16: }
  17:  
  18: private void WriteStream(Stream stream, System.IO.IsolatedStorage.IsolatedStorageFileStream fileStream)
  19: {
  20:     byte[] buffer = new byte[4096];
  21:     int bytesRead;
  22:  
  23:     while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) != 0)
  24:     {
  25:         fileStream.Write(buffer, 0, bytesRead);
  26:     }
  27: }

What we see here is that in our completed event is where we actually create the file and put it in our IsoStore location using a little helper method to write the stream data out.  Now we have a satellite assembly in our IsolatedStorage for our application.

You can see that in the completed event we also call LoadData().  This is really the same core code we previously had that we refactored into a common method:

   1: private void LoadData()
   2: {
   3:     using (var store = System.IO.IsolatedStorage.IsolatedStorageFile.GetUserStoreForApplication())
   4:     {
   5:         IsolatedStorageFileStream fileStream = store.OpenFile("DynamicXapDataGrid_CS.xap", FileMode.Open, FileAccess.Read);
   6:  
   7:         #region Original Code
   8:         StreamResourceInfo sri = new StreamResourceInfo(fileStream, "application/binary");
   9:  
  10:         Stream manifestStream = Application.GetResourceStream(sri, new Uri("AppManifest.xaml", UriKind.Relative)).Stream;
  11:         string appManifest = new StreamReader(manifestStream).ReadToEnd();
  12:  
  13:         Deployment deploy = XamlReader.Load(appManifest) as Deployment;
  14:  
  15:         Assembly asm = null;
  16:         
  17:         foreach (AssemblyPart asmPart in deploy.Parts)
  18:         {
  19:             string source = asmPart.Source;
  20:  
  21:             // next line is a hack for Beta 2 and is not needed in Silverlight 2 release
  22:             fileStream = store.OpenFile("DynamicXapDataGrid_CS.xap", FileMode.Open, FileAccess.Read);
  23:  
  24:             StreamResourceInfo streamInfo = Application.GetResourceStream(new StreamResourceInfo(fileStream, "application/binary"), new Uri(source, UriKind.Relative));
  25:  
  26:             if (source == "DynamicXapDataGrid_CS.dll")
  27:             {
  28:                 asm = asmPart.Load(streamInfo.Stream);
  29:             }
  30:             else
  31:             {
  32:                 asmPart.Load(streamInfo.Stream);
  33:             }
  34:         }
  35:  
  36:         panel1.DataContext = FakeData.GetCustomers(LastNameSearch.Text);
  37:  
  38:         UIElement myData = asm.CreateInstance("DynamicXapDataGrid_CS.Page") as UIElement;
  39:  
  40:         panel1.Children.Add(myData);
  41:         panel1.UpdateLayout();
  42:         #endregion
  43:  
  44:     }
  45: }

You’ll note that we pull the file out of IsoStore and do our same loading from there.  Please note the comment for line 22.  This is a bug in Silverlight 2 Beta 2 where the Stream is closed too early and thus you’d get an exception on line 24 because the Stream was already closed in line 10.  The code above is a hack for sure (and inefficient).  We could have subclassed Stream and modified the Close functionality, but since I know it works in the release code I wasn’t going to bother you with some unnecessary huge workaround at this point, but line 22 can be removed at release time.

The Results

So putting it all together let’s look at how the requests look.  Let’s wipe out our IsolatedStorage so that nothing is in there:

Now when we click on our button in our application, we can see that since there is nothing in our IsoStore, the HTTP traffic goes through:

and we now have something in our IsoStore as evident by the preferences dialog:

or more specifically by spelunking to the actual location for IsolatedStorage, we can see our XAP located there:

Sweet.  Now future button clicks show no more HTTP traffic because it is pulling it out of IsolatedStorage.  If we clear our browser cache, it is still there.  If we delete our cookies, the XAP is still there.  Now when our app users return, they’ll have the satellite assemblies/XAPs we need for our apps already on their machine.

Caution and Thoughts

Okay, while this “works” there are definitely some cautionary decisions you’d have to make from an application architecture point of view.  Here are some thoughts…

    • What about versioning of your satellite assemblies and controls?  If you use the exact method here, any update would never be retrieved.  Some mechanism of adding data to the IsoStore information should be considered so you know your users have what your application expects.
    • What about the size of the XAP and the available storage space in IsoStore?  You should know the sizes of things you are going to place into IsoStore, and in the code above before placing anything in there, you should look at the available space and request a quota increase if needed.  Information on how this can be done is explained here.
    • What about new browser functionality like InPrivate and In Cognito windows from browsers?  Since both of these functions are in browsers that are beta, it is hard to say how their final implementations will effect the use of IsolatedStorage, but it is something to consider and test.

At any rate, an interesting pattern that you may want to consider for your application if it makes sense.  Is it more reliable than the browser cache?  I don’t think you can universally answer that because almost all client configurations for stuff like that seem to vary…but it is an alternative!  Here’s the code for the completed solution I modified.

Please enjoy some of these other recent posts...

Comments