Most of the examples out there covering the creation of XPS documents use the file system as a backing store. This isn't optimal; in a server situation its downright stupid. So, how do you create an XPS document in memory?
I've created a sample project (VS 2005) with the code in this article. Jump down to the bottom of the post to download it and follow along.
A word (or many) about Packages
XPS documents use the Open Packaging Conventions, which are a shared description of how content can be organized within a package. A package, in this case, is a collection of other content types that can be addressed as a single object. Content can be accessed within a package via URIs, loaded on demand rather than all at once. An advantage of this can be seen when accessing a document over a network.
Office 2007 documents adhere to these conventions. For example, a word document contains not only the text of the document but the fonts, images, edit history, etc. All of these parts are different, but they are combined together within the package to create the whole document.
XPS documents are the same. The in-memory representation of an XPS document works closely with its Package to manage its content. Packages, as mentioned before, are collections of parts. These parts may be local, or they may be located on another server. Because of this, loading of package parts can be slow. To speed up loading of package parts, the PackageStore is used to manage references to these parts and perform local caching.
So, when creating XPS documents in memory, what we are actually doing is saving parts of the XPS document to a package managed by the PackageStore. It is due to the flexibility and the power of the design that our job is a bit more complex than just new-ing up an XPS document and adding children to it.
Enough, how about some code?
The first steps to creating an XPS document in memory are to create an XpsDocument and a MemoryStream. The XpsDocument object controls how parts are added to the package, and the package stores these parts in the MemoryStream. Remember, the MemoryStream must be disposed when you are done with it, and you are done with it when you are done with the XPS document; either because you are discarding it or because you have written it to whatever backing store you are using (disk, database, etc).
XpsDocument doc;
ms = new MemoryStream();
Next, we open a Package on the MemoryStream. We must tell the Package that we are creating a new document on this stream, and that we wish to be able to read and write to the document. I wish these'd be called StreamMode and StreamAccess, but since streams came from the file system originally, these enums bear the marks of their heritage. In addition, we create a PackageStore to manage our Package. This uses a URI to reference our XPS document, so we create one using the "pack:" URI scheme, since the document is located in a local Package. The same URI marks our XPS document
Package p = Package.Open(ms, FileMode.Create, FileAccess.ReadWrite);
Uri DocumentUri = new Uri("pack://document.xps");
PackageStore.AddPackage(DocumentUri, p);
doc = new XpsDocument(p,
CompressionOption.NotCompressed,
DocumentUri.AbsoluteUri);
Creating our DOM in memory is the next step. Its pretty simple to understand, for the most part.
XPS documents contain one or more FixedDocuments within a "fixed document sequence," which is an ordered collection of individual FixedDocuments. Each FixedDocument contains one or more pages, represented by PageContent objects. Each PageContent can have one single child; since XPS documents are designed for fixed layouts, the most obvious type to use as a child is the FixedPage. For some reason, you cannot add a FixedPage directly to a PageContent; you must first explicitly cast the content page as IAddChild. I'm not sure exactly why this is. If anybody has a clue, a comment would be nice!
FixedDocument fd = new FixedDocument();
PageContent pc = new PageContent();
fd.Pages.Add(pc);
FixedPage fp = new FixedPage();
((IAddChild)pc).AddChild(fp);
TextBlock tb = new TextBlock();
tb.Text = "Page one";
fp.Children.Add(tb);
Once the FixedDocument is constructed in memory it can be serialized to the package's stream via the XpsDocumentWriter.
XpsDocumentWriter dw =
XpsDocument.CreateXpsDocumentWriter(doc);
dw.Write(fd);
And that's it. The only thing to worry about is our MemoryStream object. Once its disposed, our PackageStore cannot access the parts within the XPS document package. This means that as long as you wish to manipulate the XPS document you've just created (including viewing it), you must keep the stream open. Once you are done with the XPS document (e.g., its being discarded or it has been saved to disk) you can dispose of the stream.
The code for this post is included in a working sample project here: