|
An ordinary VB developer shares his own successes and failures |
|
| Home News Articles Resources Tips Downloads About me | ||
Manual File Downloads In ASP.NETFighting Response.End and the infamous ThreadAbortException while trying to implement manual file downloading in an ASP.NET application.In this article, I'm going to talk about a simple ASP.NET application I recently created for the Slovak parliament. The application allows browsing and displaying transcripts of parliamentary debates (see http://www.nrsr.sk/appbin/net/nrozprava/; Slovak language only).The transcripts are stored as plain text files. Each text file contains transcript for a given time frame, which is encoded in the file's name. All the text files are stored in a single file system folder. The job of the ASP.NET application is, among others, to display the list of transcripts with associated time frames and allow the user to view the contents of selected transcripts.In order for the transcript files to be more readable to the user, the application applies some basic HTML formatting to the transcript's plain text before it is sent to the browser for viewing. For various reasons, I've designed the formatting process to take place dynamically whenever a user requests a given transcript file:
First, I've defined an
Public Interface IDownloadService
Sub SendItemToBrowser( _
ByVal itemID As String, _
ByVal response As HttpResponse)
End Interface
The itemID argument identifies what should be
downloaded. In the case of transcript files, it is
the file name. The response argument references
the current HttpResponse instance.
Second, I've implemented a generic
IDownloadService
implementation to a RozpravaRaw class ("rozprava"
is a Slovak term for "debate"
), which already
handled other aspects of the transcript files
processing:
Public Class RozpravaRaw
Implements IDownloadService
...
Private Sub SendItemToBrowser( _
ByVal itemID As String, _
ByVal response As System.Web.HttpResponse) _
Implements IDownloadService.SendItemToBrowser
' Generate content dynamically (Response.Write etc.)
End Sub
End Class
The processing goes as follows:
Private Sub Page_Load( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
Try
...
' Parse the query string - "Ref" and "Type" variables are mandatory.
Dim TypeName, Ref As String
Me.ParseQueryString(TypeName, Ref)
' Create an IDownloadService dynamicaly and delegate to it.
Dim Service As IDownloadService = CreateDownloadService(TypeName)
Service.SendItemToBrowser(Ref, Response)
Catch ex As Exception
SetError(ex.ToString())
Trace.Warn(ex.ToString())
Diagnostics.Trace.WriteLine(ex.ToString())
End Try
End Sub
Private Sub ParseQueryString( _
ByRef typeName As String, _
ByRef ref As String)
typeName = Request.QueryString("Type")
If typeName Is Nothing Then
typeName = String.Empty
End If
' If typeName contains a space, it is just HTML-encoded "+" sign,
' which the CLR uses when naming nested classes.
typeName = typeName.Replace(" "c, "+"c)
ref = Request.QueryString("Ref")
If ref Is Nothing Then
ref = String.Empty
End If
End Sub
Private Shared Function CreateDownloadService( _
ByVal typeName As String) As IDownloadService
Try
Dim Assm As System.Reflection.Assembly = System.Reflection.Assembly.GetExecutingAssembly()
Return DirectCast(Assm.CreateInstance(typeName, True), IDownloadService)
Catch ex As Exception
Throw New ApplicationException( _
String.Format("Unknown request ({0}).",
typeName), ex)
End Try
End Function
I've designed it this way because, besides the
dynamic transcript file download, the application
had to support downloading other file types
(namely .DOC and .MP3 files). This way, the
download link is always the same and the actual
download content generation logic is placed within
the particular IDownloadService implementation.
Here is the complete Download.aspx.vb code. Well, everything seemed to work fine until I accidentally looked at the HTML source code of a downloaded transcript file. Take look at this URL, for example; open the link in the browser and view the HTML source code. Do you see what's wrong? ...I'm waiting...
Now you've got it - there are two <HTML> blocks in
the file! The first block is the one I've
generated dynamically in the So there it was - my quest for a solution began.
The first one that came to mind was to use the
Private Sub Page_Load( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
Try
...
' Parse the query string - "Ref" and "Type" variables are mandatory.
Dim TypeName, Ref As String
Me.ParseQueryString(TypeName, Ref)
' Create a IDownloadService dynamically and delegate to it.
Dim Service As IDownloadService = CreateDownloadService(TypeName)
Service.SendItemToBrowser(Ref, Response)
Response.End()
Catch ex As Exception
SetError(ex.ToString())
Trace.Warn(ex.ToString())
Diagnostics.Trace.WriteLine(ex.ToString())
End Try
End Sub
The problem with this approach is that the
Response.End call generates a
ThreadAbortException. This exception is a very
lusty beast. Even if you catch it, it is still
rethrown at the end of the Catch block (or Finally
block if there is any). The only way one can
"swallow" the ThreadAbortException is by calling
Thread.ResetAbort. But...that call will prevent
the thread from being aborted, which is what we're
trying to achieve in the first place.
In addition, good practice recommends that exceptions should not be thrown in the normal course of operation, and I wanted to stick with the rules. Another solution explained in this MSKB article is to use theHttpApplication.CompleteRequest method, instead of
the Response.End method. Unfortunately, it doesn't
help either - the "default" ASP.NET HTML code is
still appended to the end of the dynamically
generated content even if you call the
CompleteRequest method.
Yet another solution that I came acrross on my quest was to completely remove all the HTML code off of the 'download' page (see the post at the end). Although it might work (I haven't tried), I don't like the solution because it feels like a hack (I mean, it is completely undocumented). There is no guarantee that future ASP.NET versions won't put some HTML content to the response buffers even if the associated aspx page is empty. At this point, I finally realized that what I really need is to have complete control over the content sent to the browser -IHttpHandler comes
to save the day.
Because I didn't want to change the
I've excluded the Here is the new Download custom HTTP handler source code.
In order to actually associate the
<httpHandlers>
<add verb="*" path="Download.aspx"
type="NRozprava.Download, NRozprava"/>
</httpHandlers>
Now, whenever a browser requests the Download.aspx
URL, the ASP.NET plumbing instantiates our
Download class and calls the
IHttpHandler.ProcessRequest method. What is
written by the method implementation to the
response buffer, exactly that is sent back to the
browser - nothing more, nothing less!
The conclusion is simple: If you need to generate
custom HTTP responses in your ASP.NET application,
use the © Palo Mraz, Tuesday, September 30, 2003 |
||
|
|
||
| ©2003-2007 Palo Mraz. All Rights Reserved. See my 'new browser window' policy | ||