Category Archives

4 Articles

Posted by .Ronald on

How to create a zip archive and download it in ASP.NET

In a previous post How to download multiple files in ASP.NET, I explained how to generate multiple documents and offer them as separate downloads in ASP.NET. One of the options I had when looking for a solution to offer multiple downloads, was adding all the documents to 1 single zip archive container, and offer that as download to the user. This solution didn’t completely satisfy the end-users, but is also offered for those who want to use it.

In this post I will explain, how I take the same list of documents, and offer them as a zip archive to download. Starting from the multiple download solution, this only required 1 extra step in the process, namely, creating a zip archive and adding all the documents to it. The rest of the process is as described in the previous post.

The method takes the same argument as when creating separate download links, namely an a list of byte arrays. Each byte array in its turn contains the binary content of the document. I use the SharpZipLib from ICSharpCode, which can be downloaded here: The Zip, GZip, BZip2 and Tar Implementation For .NET. This is what this method looks like:


Private Function ZipDocuments(ByVal reports As IList(Of byte())) As Boolean

' Add documents to 1 ZIP file, and open in browser
Using zipOutMemoryStream As New MemoryStream()
Using zipOutStream As New ZipOutputStream(zipOutMemoryStream)

'Add documents to Zip File.
Dim cnt As Integer = 1
For Each buffer As byte() In reports
Dim entry As New ZipEntry(String.Format("{0}_{1}.pdf", "GeneratedFile", cnt))

zipOutStream.PutNextEntry(entry)
zipOutStream.Write(buffer, 0, buffer.Length)
zipOutStream.CloseEntry()
cnt += 1
Next

zipOutStream.Finish()
zipOutStream.Close()
zipOutMemoryStream.Close()

Dim responseBytes As Byte() = zipOutMemoryStream.ToArray()

'Return Null on Empty Zip File
Const ZIP_FILE_EMPTY As Integer = 22
If responseBytes.Length <= ZIP_FILE_EMPTY Then
Return Nothing
End If

RegisterDocumentDownload(Guid.NewGuid().ToString(), responseBytes, ContentTypes.ZIP)

End Using
End Using
End Function

I first create a (binary) memorystream (zipOutMemoryStream) to contain the content of the zip file (zipOutStream).
Then I loop over the list of documents (or files), create an entry in the zip file (entry as ZipEntry), and write the binary content to the zip entry.
After adding the files to the zip and cleaning up, I can use the same RegisterDocumentDownload() method from the previous post, and the zip archive will be added to the user and opened in the browser.

And that’s it…

Posted by .Ronald on

How to download multiple files in ASP.NET

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.

Posted by .Ronald on

Softwaredocumentatie met Sandcastle

[DUTCH:]

Waarom verschijnt er altijd een ongemakkelijke glimlach op het gezicht van een ontwikkelaar als er gevraagd wordt of zijn software gedocumenteerd is? Hoewel deze vraag niet wordt beantwoordt in dit artikel, zal er worden uitgelegd hoe het op eenvoudige wijze mogelijk is om (technische) documentatie van software automatisch te laten genereren.

Atos Application Development & IntegrationSoftwaredocumentatie met Sandcastle

Posted by .Ronald on

Copy project files to another location after build

For a (web-)project I am working on, I needed to copy the content of 1 website to a sub-folder of another website after the project was built. This enables the 2 projects being separately developed and deployed, but also allows the one project being deployed as a sub-site of the other one. Got it? 🙂

This is how I did this:
In the project file of the sub-project:

  1. In the project file I defined a few variables to make things easier
    <PropertyGroup>
    <MyOutputFolder>C:ProjectOutputFolder</MyOutputFolder>
    <OutputSubFolder>SubFolder</OutputSubFolder>
    </PropertyGroup>
  2. I defined what files to copy, with the binary output (dll) separately from the website content:
    <ItemGroup>
    <MyCopyItems Include="$(MSBuildProjectDirectory)***.aspx" />
    <MyCopyItems Include="$(MSBuildProjectDirectory)***.htm?" />
    <MyCopyItems Include="$(MSBuildProjectDirectory)***.js" />
    <MyCopyItems Include="$(MSBuildProjectDirectory)***.gif" />
    <MyCopyItems Include="$(MSBuildProjectDirectory)***.jp?g" />
    <MyCopyItems Include="$(MSBuildProjectDirectory)***.png" />
    <MyCopyBin Include="$(MSBuildProjectDirectory)bin***.dll" />
    </ItemGroup>
  3. I created a new target that performs 2 copy commands, one for the binaries and one for the content:
    <Target Name="MyPostBuildTarget">
    <Copy SourceFiles="@(MyCopyItems)"
    DestinationFiles="@(MyCopyItems->'$(MyOutputFolder)$(OutputSubFolder)%(RecursiveDir)%(Filename)%(Extension)')" />
    <Copy SourceFiles="@(MyCopyBin)"
    DestinationFolder="$(MyOutputFolder)bin" />
    </Target>
  4. I added this target to the BuildDependsOn property so that it is executed everytime the project is built:
    <BuildDependsOn>$(BuildDependsOn);MyPostBuildTarget</BuildDependsOn>

This gives something like:

<!-- Copy project output to defined folder -->
<PropertyGroup>
<BuildDependsOn>$(BuildDependsOn);MyPostBuildTarget</BuildDependsOn>
<MyOutputFolder>C:ProjectOutputFolder</MyOutputFolder>
<OutputSubFolder>SubFolder</OutputSubFolder>
</PropertyGroup>
<ItemGroup>
<MyCopyItems Include="$(MSBuildProjectDirectory)***.aspx" />
<MyCopyItems Include="$(MSBuildProjectDirectory)***.htm?" />
<MyCopyItems Include="$(MSBuildProjectDirectory)***.js" />
<MyCopyItems Include="$(MSBuildProjectDirectory)***.gif" />
<MyCopyItems Include="$(MSBuildProjectDirectory)***.jp?g" />
<MyCopyItems Include="$(MSBuildProjectDirectory)***.png" />
<mycopybin include="$(MSBuildProjectDirectory)bin***.dll" />
</ItemGroup>
<Target Name="MyPostBuildTarget">
<Copy SourceFiles="@(MyCopyItems)"
DestinationFiles="@(MyCopyItems->'$(MyOutputFolder)$(OutputSubFolder)%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="@(MyCopyBin)"
DestinationFolder="$(MyOutputFolder)bin" />
</Target>

Some references on copying project files with MSBuild:
MSDN MSBuild Reference – Copy Task
Channel 9 Wiki: Copy Built Output Of A Visual Studio Project
SharpDevelop Community – Copy config post build
How To: Insert Custom Process at Specific Points During Build
How To: Add Custom Process at Specific Points During Build (Method #2)