hexadecimate

What's a wedding? Webster's dictionary describes it as the act of removing weeds from one's garden.

Browsing XAP contents in Silverlight 2

The Silverlight 2 runtime doesn’t have an easy way to browse the contents of the XAP file.  This is quite unfortunate if one wants to browse embedded resources and XAML within the XAP. 

The XAP is essentially a ZIP file renamed.  One easy way to browse the contents of a XAP manually is to rename the file and tack on “.zip” at the end.  (Technically, you don’t need to do this to read the contents—but some folks who depend on file extensions will find this useful.)

Now let’s try to do something interesting.  Let’s try to view the contents of a XAP using Silverlight 2 at runtime!  There are a few ways that center around the following ideas:

  1. Download the XAP and have the contents ready in a Stream.
  2. Read the contents by using some implementation of the ZIP file format.  (See http://www.pkware.com/documents/casestudies/APPNOTE.TXT)

There is a useful ZIP implementation available over at http://www.silverlightcontrib.org/.  I also happened to come across a light weight implementation by Jason Cooke (of Microsoft) and I’ll show you that here.  This implementation will not actually decompress the resources within the XAP—it will merely allow you to enumerate through them as strings.  Using those streams,  you can load the resources by using Application.GetResourceStream(<string>).

WebXapFileLoader (courtesy of Jason Cooke)

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Windows.Browser;

namespace SLFileIO
{
    public static class WebXapFileLoader
    {
        private static Dictionary<object, WebClient> clients = new Dictionary<object, WebClient>();
        private static Dictionary<object, FileListCallback> callbacks = new Dictionary<object, FileListCallback>();
        
        public static void Load(FileListCallback callback)
        {
            Uri originalUrl = HtmlPage.Document.DocumentUri;
            string xamlFile = (string)HtmlPage.Plugin.GetProperty("source");
            if (xamlFile == null)
                throw new Exception("Could not get xap source");
            Uri xamlLocation = new Uri(originalUrl, xamlFile);

            WebClient client = new WebClient();
            Guid token = Guid.NewGuid();
            clients.Add(token, client);
            callbacks.Add(token, callback);
            client.OpenReadCompleted += XapReloaded;
            client.OpenReadAsync(xamlLocation, token);
        }
    
        private static void XapReloaded(object sender, OpenReadCompletedEventArgs e)
        {
            try
            {
                callbacks[e.UserState](e.Result);
            }
            finally
            {
                callbacks.Remove(e.UserState);
                clients.Remove(e.UserState);
            }
        }
    }

    public delegate void FileListCallback(Stream xapFileStream);
}

XapInspector (courtesy of Jason Cooke)

using System.Collections.Generic;
using System.IO;
using System.Text;

namespace SLFileIO
{
    public static class XapInspector
    {
        public static IList<string> GetFileNames(Stream stream)
        {
            var ret = new List<string>();
            using (var archiveStream = new BinaryReader(stream))
            {
                while (true)
                {
                    string file = GetFileName(archiveStream);
                    if (file == null) break;
                    ret.Add(file);
                }
            }
            return ret;
        }

        private static string GetFileName(BinaryReader reader)
        {
            // Info from http://www.pkware.com/documents/casestudies/APPNOTE.TXT
            var headerSignature = reader.ReadInt32();  // local file header signature     4 bytes  (0x04034b50)
            if (headerSignature != 0x04034b50)
                return null; // Not a local file header
            reader.ReadInt16();                        // version needed to extract       2 bytes
            reader.ReadInt16();                        // general purpose bit flag        2 bytes
            reader.ReadInt16();                        // compression method              2 bytes
            reader.ReadInt16();                        // last mod file time              2 bytes
            reader.ReadInt16();                        // last mod file date              2 bytes
            reader.ReadInt32();                        // crc-32                          4 bytes 
            var compressedsize = reader.ReadInt32();   // compressed size                 4 bytes
            reader.ReadInt32();                        // uncompressed size               4 bytes
            var filenamelength = reader.ReadInt16();   // file name length                2 bytes
            var extrafieldlength = reader.ReadInt16(); // extra field length              2 bytes
            var fn = reader.ReadBytes(filenamelength); // file name                    (variable size)
            var filename = UTF8Encoding.UTF8.GetString(fn, 0, filenamelength);
            reader.ReadBytes(extrafieldlength);        // extra field                  (variable size)
            reader.ReadBytes(compressedsize);          // compressed data              (variable size)
        
            return filename;
        }
    }
}

Putting it together

I put together a quick Silverlight application and added a ListBox called “XapFileList”.  I was able to bind that to the list of files inside of the XAP using the following simple line of code.

private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
{
    WebXapFileLoader.Load(
        xapFileStream => 
            this.XapFileList.ItemsSource = XapInspector.GetFileNames(xapFileStream));
}

Very simple.  :)  Thanks Jason!

Posted: Jan 15 2009, 00:18 by warren | Comments (3) RSS comment feed |
  • Currently 3/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: