The project I’m currently assigned to, already has an option to generate reports (pdf) which are just streams the binary output of the report generator to the response output stream. Something like this:


Dim binReader As New System.IO.BinaryReader(report.ExportToStream())

With Response
.ClearContent()
.ClearHeaders()
.ContentType = "application/pdf"
.AddHeader("Content-Disposition", "inline; filename=AankondigingControlesEnGevolgen.pdf")
.BinaryWrite(strStream.ReadBytes(CInt(strStream.BaseStream.Length)))
.Flush()
.Close()
End With

This piece of code streams the binary output of the report to the response object, and setting the right ContentType and Header, it opens the document in the user’s browsers. Works like a charm.

But now I was asked to create a form where the user can select multiple reports to download and open them in the browser. My first answer was: We can’t do that (easily). But then I started to look at the options we have when working in ASP.NET and generating output to the client browser.

The solution I ended up with was that easy, that I found myself kind of stupid that I didn’t think about it earlier. This is what I did:

  1. Generate the documents and store them (binary), together with a unique key, in a session variable
  2. Generate download links with that unique key as parameter
  3. Open the links with clientside javascript
  4. In the download page, retrieve the content from the session variable and stream it to the client browser

Let’s take a look at that in detail.

1. Generate the documents and store them (binary), together with a unique key, in a session variable

I created a custom class to hold the binary document content, together with extra information that can be helpful when generating the download:


Private Class ContentTypes
Public Const PDF As String = "application/pdf"
Public Const ZIP As String = "application/zip"
End Class

<Serializable()> _
Private Class Download
Public Name As String
Public Content() As Byte
Public ContentType As String
End Class

Name: The name of the file that is generated and is used when the user downloads the file (save to disk)
Content: The binary content of the file
ContentType: Because I don’t want to be limited to 1 specific file type, I include the content type with the download

Currently I’m only using 2 types of documents, but as you can see, this can be easily extended.

2. Generate download links with that unique key as parameter

For each document I created and stored, together with a unique key, in a session variable, I generated the client-side script to open a new window with the download link. Because I use the same page to download the document, I can create a URL starting with the querystring question mark:


Private Sub RegisterDocumentDownload(ByVal key As String, ByVal content() As Byte, ByVal contentType As String)
Dim script As String = String.Format("window.open('?key={0}');", key)
Dim download As New Download()
download.Content = content
download.ContentType = contentType
download.Name = key

Session.Add(key, download)
ScriptManager.RegisterStartupScript(Me, Me.GetType(), "Download_" & key, script, True)
End Sub

3. Open the links with clientside javascript

The JavaScript that is generated, will look something like this (when generating 3 downloads):


<script type="text/javascript">
//<![CDATA[
window.open('?key=ee00cb06-81f6-48d7-bed2-6cf0af90d5f8');
window.open('?key=a05d2567-5ab8-4c41-a000-bb0bd16498ca');
window.open('?key=ea7bb0aa-6686-442e-be6f-b14ac14aacf5');
//]]>
</script>

4. In the download page, retrieve the content from the session variable and stream it to the client browser

Because I use the same page to download the file as well, I added code to the Page_Load() event that checks for the “key” parameter


If Not Request.QueryString("key") Is Nothing Then
StreamDownload(Request.QueryString("key"))
Exit Sub
End If

This calls the StreamDownload() method which takes the download from the session, streams the content to the browser client and cleans up everything before ending processing


Private Sub StreamDownload(ByVal key As String)

Guard.ArgumentNotNull(Session(key), "download")

Dim download As Download = DirectCast(Session(key), Download)
Dim stream As New MemoryStream()
Dim formatter As New BinaryFormatter()

formatter.Serialize(stream, Session(key))
With Response
.Clear()
.ContentType = download.ContentType

Select Case download.ContentType
Case ContentTypes.ZIP
.AppendHeader("Content-Disposition", String.Format("filename={0}.zip", download.Name))
End Select

.BinaryWrite(download.Content)
.Flush()
End With

' cleanup temporary objects
Session.Remove(key)
Session.Remove(key & "_download")

Response.End()

End Sub

As you can see, I also have the possibility to generate zip archives. This is to offer the functionality of downloading multiple documents in 1 zip archive container. I could easily immediately offer this zip download from within the page. But I prefer to use this generic solution, even if I’m only offering 1 file to download. This also gives me the possibility to offer other file formats as well. I just need to add a new content type, and alter the code where needed in the StreamDownload() method.

In a next post, I will show how I created 1 zip archive which contains 3 documents, and offer this as a download to the user.