Document Toolkit: using the WebPackageReader
This is part two of a series of blog posts on Document Toolkit. Document Toolkit is a Silverlight library offering a range of features that enable easy document access and document display in Silverlight 2 and Silverlight 3 applications. In this post I will demonstrate the WebPackageReader which enables partial document downloads.
Related Links
Architecture
An XPS document is in fact a ZIP archive using the Open Packaging Convention, which contains the files which make up the document. These include an XML markup file for each page, text, embedded fonts, raster images, 2D vector graphics, etc. Each entry in a ZIP archive is identified as a so-called part.
The logic for loading XPS documents is implemented in the XpsClient class (availabe in the FirstFloor.Documents.IO namespace). XpsClient provides methods for asynchronously loading the document structure, pages and document resources. XpsClient knows exactly what parts to load, but has no clue on how to load the parts. Here is where the IPackageReader interface comes in. Classes implementing IPackageReader know how to retrieve a particular XPS part.

Document Toolkit comes with two IPackageReader implementations, DefaultZipPackageReader and WebPackageReader. DefaultZipPackageReader is able to load part from a locally available ZIP stream. WebPackageReader is able to load package parts over HTTP. See the online documentation for a more detailed overview of the architecture of Document Toolkit.
WebPackageReader
Since an XPS document is a ZIP archive, it is fairly easy to just load the parts from the ZIP that are needed. There is no need to download the entire ZIP archive to display a single page. This is good news if you want to display a single page from a large XPS document of 100Mb that is located on the server. And this is exactly what the WebPackageReader provides; partial downloading of document parts. The client-side of things are all available in Document Toolkit. On the server, we need to do some work in order to serve XPS document parts.
Serving XPS document parts
So we need some sort of service that is capable of returning ZIP parts that are requested by the WebPackageReader. The sample in this post is based on ASP.NET technology, but any server-side technology such as PHP or Java can be used. The following code sample provides the ProcessRequest implementation of our service. The service itself is a plain ASP.NET IHttpHandler written in C#.
public void ProcessRequest(HttpContext context)
{
try {
string request = HttpUtility.UrlDecode(context.Request.QueryString.ToString());
int index = request.IndexOf('/');
if (index == -1) {
throw new ArgumentException("Invalid request");
}
string xpsFileName = Path.Combine(context.Server.MapPath("Documents"), request.Substring(0, index));
string partName = request.Substring(index + 1);
if (!File.Exists(xpsFileName)) {
throw new ArgumentException(string.Format("File '{0}' not found", xpsFileName));
}
// TODO: read part from ZIP
}
catch (Exception e) {
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.Write(e.Message);
}
}
The WebPackageReader uses HTTP querystring to encode its requests, so a basic querystring parser is implemented. The querystring contains the XPS document name and the path to the requested part inside the ZIP archive. In the code sample it is assumed that the requested XPS document is available in the /Documents folder on disk. You may choose to modify this and you can even read your XPS documents from a database if you wish.
Once it has been determined that the requested XPS document exists, we use the excellent SharpZipLib to open the file and read the requested part.
using (FileStream stream = File.OpenRead(xpsFileName)) {
ZipFile file = new ZipFile(stream);
ZipEntry entry = file.GetEntry(partName);
if (entry != null) {
using (Stream entryStream = file.GetInputStream(entry)) {
// TODO: set mime-type as defined in XPS package
context.Response.ContentType = "application/octet-stream";
byte[] buffer = new byte[2 << 14]; // write blocks of 32768 bytes
int read;
while ((read = entryStream.Read(buffer, 0, buffer.Length)) > 0) {
context.Response.OutputStream.Write(buffer, 0, read);
}
}
}
else {
// return 404 Not Found
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
}
When the requested XPS document part has been found, all that is needed is to return the bytes of the part to the HTTP response output stream. For now a general purpose content type is used.
Optimizations
The demonstrated sample provides a basic XPS service that is able to handle the requests of the WebPackageReader. With Document Toolkit on the client and the service in place, we have a complete implementation of a partial document loader over HTTP.
The demonstrated implementation is rather sub-optimal. There is room for improvement, to name a few:
- For every request, a ZIP archive is opened and searched. This is an disk IO intensive operation and should be avoided
- The content type is set to some general purpose 'application/octet-stream'. We want to use correct mimetypes.
- Use HTTP client-cache headers to ensure parts are not requested over and over again.
- And more...
The next blogpost in this series will discuss the various optimizations in detail and provide solutions.
Summary
The WebPackageReader enables partial document loading over HTTP. A relatively simple service is required to serve document parts. The service does not need to be implemented using .NET technology.
Published: May 13, 2009