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

| Comments

Two great events are happening here in the Valley of the Sun (yes, it’s still 105°F outside here folks) are coming up soon that you should consider taking part in.

Startup Weekend Phoenix

Startup Weekend is a weekend long event (starting on Friday at about 6pm and ending on Sunday at 6pm) where local techies, entrepreneurs, project managers, or interested people of all types gather in teams to actually create companies that could be viable in today’s marketplace.  This isn’t just vapor, past Startup Weekends have actually gotten to the point of initial prototypes, branding, marketing strategies within this weekend.  The goal is to gather the local community brains, have fun, and innovate.  It sounds like a really, really great time.

The event is not a free event, but only costs $39.99 and the funds are going only to directly support the event…the intention is not to ‘make money’ off these registration fees, but cover the costs beyond the sponsors have provided.  This event is not organized by one individual, one organization and there are no hidden agendas by any sponsors.  Local entrepreneurs and community folks are arranging this and excited to try to ignite the innovation in the Phoenix area and come up with something great!

Register hereInfo here.  October 17-19, 2008.

PodcampAZ 2008

I went to this event last year and it was full of energy!  Organized by some great folks, namely Brent Spore, this is the place to be in November if you are involved locally in social media.  The speaker line up is looking fun and Chris Pirillo and iJustine will be in attendance. This event is free and if you are available that weekend you should definitely go to see what is happening in social media in our area.

The event is two days, held at the UAT campus in Tempe/Phoenix (I can never tell exactly where that is located, but it’s close enough to Ted’s BBQ that I call it Tempe).  Come out and join the fun.

Info here.  No registration required.  Only fun is required.  Last year shirts were available with a “This one time, at Podcamp” theme – I can only imagine something equally as sarcastic will be made this year.

It’s fun to see things like this happening in Phoenix.  Let’s get everyone to show up and support our technical communities!

| Comments

The Expression Encoder team has announced the plans for SP1 of their product (announced, not released).  You can read all the details on their team blog for more information, but two things caught my attention that are probably of most interest: H.264 encoding and a Silverlight 2 template in the box.

The H.264/AAC encoding support is a result of customer feedback (as well as preparing for future Silverlight support).  While the profiles in this service pack will be limited and may not meet the needs of everyone immediately, it is a great step and will probably meet the needs of most.

The templates produced by Encoder 2 right now have been Silverlight 1.0 templates.  This has been a sore spot for some wanting to move to Silverlight 2 and remove themselves from all the javascript that the templates have.  There have been a lot of solutions popping up (shameless plug: SL2VideoPlayer on Codeplex), but people still want the template in the box (note: you can do this with your SL2 players today as well).  The team announced they will be providing SL2 templates based on a core control called MediaPlayer which supports a wide range of features.  As you could imagine the template will support basic and advanced playback capabilities and will be able to be styled as well as advanced property manipulation in Expression Blend.  This one feature caught my eye and makes me happy:

Byte-range seeking: When a user clicks forward on the timeline into a non-downloaded region, the player will cancel the current progressive download and start a new one from the point that has been seeked to.

Sweet.  I can’t wait.  The SP1 will happen by the end of the year and will be a free upgrade to existing Encoder 2 customers.

| Comments

As you may know, most wheel support is detected at the browser level.  In Silverlight, this makes adding mouse wheel support an interop action with the HTML host.  While people have implemented it, it has mostly been for the use of DeepZoom applications.  In fact, the latest DeepZoom Composer tool actually adds this support now if you choose to have a Silverlight project as a part of the output of the collection.

But what about other controls, namely ScrollViewer?  Having content in ScrollViewer enables ScrollBar functionality but doesn’t automatically respond to mouse wheel actions.  Adam Cooper has solved this gap problem by adding a helper class to which you can attach to your ScrollViewer object.  Let’s say our XAML is this:

   1: <ScrollViewer x:Name="MyScroller" Width="300" Height="100" Background="AliceBlue">
   2:     <TextBlock TextWrapping="Wrap">
   3:         Even if you use my workaround ...
   4:     </TextBlock>
   5: </ScrollViewer>

If we run the application our content is scrollable, but not with the wheel.  Add Adam’s control reference and these lines of code in our Page.xaml.cs class:

   1: using Cooper.Silverlight.Controls;
   2:  
   3: public Page()
   4: {
   5:     InitializeComponent();
   6:  
   7:     ScrollViewerMouseWheelSupport.Initialize(this);
   8:  
   9:     MyScroller.AddMouseWheelSupport();
  10: }

and we have automatic wheel scroll support.  Great job Adam!  His post talks about various other methods of nesting and how it actually works.

It would be nice if you could just create MyCustomScrollViewer but unfortunately you cannot subclass ScrollViewer for some reason. 

Some might be also wondering how they could add this functionality to ListBox, since the default control template for ListBox contains a ScrollViewer.  Good question I thought as well and I went about trying.  There are some challenges to using the default ListBox.  First, the visual tree of the template is only loaded after the layout has been updated.  I’m not sure why it isn’t in Loaded, but in LayoutUpdated it is available.  So from this event you can use VisualTreeHelper.GetChild(ListBox, 0) to get the root node of the template.  From there you can use FindName(“ScrollViewer”) to get to the ScrollViewer in the default control template.

Here’s where it gets funky.

While you can add Adam’s extension method just fine with no exceptions, it doesn’t exactly work when rendered.  Here’s the code I used:

   1: void DefaultList_LayoutUpdated(object sender, EventArgs e)
   2: {
   3:     // root node
   4:     Grid rootGrid = VisualTreeHelper.GetChild(DefaultList, 0) as Grid;
   5:     
   6:     // find the ScrollViewer
   7:     ScrollViewer scroller = rootGrid.FindName("ScrollViewer") as ScrollViewer;
   8:     
   9:     // add the mouse wheel support
  10:     scroller.AddMouseWheelSupport();
  11:  
  12:     // stop looking
  13:     DefaultList.LayoutUpdated -= new EventHandler(DefaultList_LayoutUpdated);
  14: }

And while no errors occur, I believe other events in the ItemTemplate are eating my events.  If anyone has any wise thoughts, post the comments here.  But either way, thanks Adam for this great utility for ScrollViewer!

| Comments

A few weeks ago my wife and I traded “up” to a Canon EOS 40D digital SLR camera.  We also invested pretty heavily (for non-professional, borderline rookies) in lenses that we knew we’d use most often.  When the Canon Digital Rebel (EOS300D) first came out, I bought it…what can I say, I’m a geek!  I paid over USD $1K for the kit and it was/is a good camera.  Still, it was the first and probably not even considered ‘prosumer’ grade. 

Advance about 3 years (maybe 4?), for the same price (actually a little less), I’m in a 40D with amazing lenses, functionality and quality.  The quality/price ratio has been getting exponentially higher as this digital photography progresses.  And then I hear about the Canon 5D Mark II camera with video capture.  I’m not going to run out and buy this one (at USD $2700 for the body only it is a bit much, but maybe in a year :-)), but this story is amazing!  Vincent Laforet is a 33 year old photographer in New York.  His work is impressive.  Recently he convinced Canon for a little try-before-buy deal.  He asked to borrow the 5D Mark II for 72 hours.  He would use his own lenses and shoot some photos and video.  The resulting images/video would be royalty free to use (I think that was the deal).  Using nothing but his existing lenses and the new camera he produced an amazing video (with no re-touching) which he calls Reverie.

You must absolutely spend the time to let this video download and watch it.  The quality is amazing…and that’s with some compression so it didn’t chew up amazing bandwidth on the web.  Wow.  Simply wow.  More details on the story and a behind-the-scenes video at Vincent’s blog.