|Delphi Clinic||C++Builder Gate||Training & Consultancy||Delphi Notes Weblog||Dr.Bob's Webshop|
The universe is a nice place, with rules, laws and curiosities. Take light for example. Using a torch or flashlight, you can send a straight beam of light from the torch to where you're aiming it. Straight, since a light beam cannot be bend (around a corner for example). Unless you're near a black hole or other sufficient large gravity force which will even bend light in a corner.
ASP.NET is a whole world in its own, with also its own set of rules and apparent limitations. One such "limitation" would be the fact that ASP.NET is bound to serving pages, so-called web forms, and cannot be used otherwise. For example, for producing different kinds of output like images. In this article, I'll explain that this is not the case, since we can easily bend ASP.NET to do what we want (without the need for a black hole in the mix).
All kidding aside, the actual need for a solution was born out of a real-world scenario. A lot of the Windows ISPs these days run Windows 2003 Server with Internet Information Server 6.0. Windows 2003 is one of the most locked-down versions of Windows that I've ever had the pleasure of working with. In fact, even the Web Edition is so locked down, you need to spend some time to turn it into an actual web server. One of the nice things of IIS6 is that it recognises a number of "web extensions", which can be enabled or disabled (for additional security for example). In fact, CGI and ISAPI extensions are disabled by default. So is ASP.NET, but that extension is usually enabled again, since it only requires scripting rights for ASP.NET applications. CGI and ISAPI applications also require the "execute" right to be set for the (virtual) directory holding these applications, which is something not every ISP will want to do. Not when there's the ASP.NET alternative.
Anyway, to cut a long story short: one of my clients was in the situation where his existing ISAPI applications would no longer be allowed to run, and he was told he should migrate to ASP.NET (or find another ISP - unfortunately, the ISP was the network department of his own company, as the application was for an intranet/extranet environment, so switching ISPs was not really an option). One of the ISAPI applications that my client maintained was an application returning specific content types, namely PDF documents, word documents, images and other types. He needed to find a way to use an ASP.NET solution to produce different content types, but only found examples of ASP.NET applications producing HTML with these web forms. He tried redirecting from the .aspx file to another page, or the actual document, but none of the solutions he tried was very successful (so he turned to me in the end).
Fortunately, there is an easy solution to this problem, although I must admit it's not something you may think of right away. To demonstrate the solution, let's now build step-by-step a little application that will return images (or any other content type for that matter). You can use any version of Delphi for .NET for this.
First of all, start Delphi 2005 and do File | New - ASP.NET Web Application - Delphi for .NET to produce a new ASP.NET Web Forms application. Feel free to use the default suggested name for the application, or pick your own name (I've used DG for this demo application).
An ASP.NET application consists of several files, including a web form .aspx and corresponding .pas source file. If you want to produce output other than HTML, you should simply leave the designer area empty, which will result in an almost empty .aspx file as well (almost, apart from the page directive and basic HTML tag for the page. In fact, you could remove everything from the .aspx file except for the first line with the page directive (which indicates to the ASP.NET worker which web form class to instantiate to handle the incoming request). However, clearing out the .aspx file will make the HTML designer feel a bit uncomfortable, so just leave everything in there (it doesn't really matter).
We should move to the .pas page, and in the Page_Load we can write our code to override the normal behaviour of the ASP.NET web form. As you may know, the Response (and Request) objects are available inside the ASP.NET web form, and we can use this Response object to set the ContentType property to something other than the default value. If we do that, then there will be no more rendering, but we need to fill the content ourselves and call the End method to ensure that the request is ended.
One of the ways to fill the Response is to use the WriteFile method, which can be used to load a file of any time (bitmap, PDF file, Word document, whatever) and place it in the Response output stream. Obviously, we should ensure that the actual contents correspond to the specified ContentType. Based on the ContentType definition, the client will optionally open a special application to handle the received response. For a ContentType of image/jpeg (or image/gif), the browser can be used to view the image, which I'm doing in the following code snippet.
procedure TWebForm1.Page_Load(sender: System.Object; e: System.EventArgs); begin Response.ContentType := 'image/jpeg'; Response.WriteFile('BobSwart.jpg'); Response.&End end;Note that a ContentType of image/jpg is not recognised (we must use jpeg), and also note that we have to prefix the End method with an ampersand, to ensure that the compiler doesn't treat it as a keyword, but as the actual method of the Response object.
http://localhost/DG/WebForm1.aspxAnd it will return an image. You can also place it inside an img tag as follows:
Returning specific documents
Apart from sending out image files, we can also use this technique to allow our ASP.NET application to return PDF documents, for example PDF agenda files for the Developers Group meetings. This can actually be a two-step process: first, I want to show a page where you can enter a specific date, and then the ASP.NET web page will return the PDF file for the first Developers Group meeting on or after that specified date.
Using the current example application as starting point, we have to go to the Design tab first (good thing if you didn't clear out the .aspx contents, yet). Place a Label, Calendar and Button on the web form. Use the Label to explain the meaning of the Calendar and Button, for example as follows:
The implementation for the Button's Click event is the place where we can return the custom content type, based on the give date.
Since the agenda's are all stored in the meeting (and meetings) directory with a name of YYYYMMDD (easily sorted!), it's a matter of starting with the selected date and moving onwards until we've found a hit.
To avoid problems, I've added a limit of 365 days, which means that the search will end one way or another, even if you want to look for a meeting in the far future.
Searching for a file means checking if a given filename exists. Based on the selected date from the Calendar, we should first convert that date to the filename in the YYYYMMDD format. This is done by using the format string yyyyMMdd (a bit funny that I need to specify uppercase M here). We then need to add the .pdf extension, as well as the \meeting\ directory name prefix. Finally, we should add the physical path of the application directory, which can be found by calling Server.MapPath('.'), so we can use a fully qualified path to find and send the agenda PDF file.
procedure TWebForm1.Button1_Click(sender: System.Object; e: System.EventArgs); const max = 365; dir = '\meeting\'; ext = '.pdf'; Trim: array[0..1] of Char = ('/','-'); var Path,FileName: String; counter: Integer; begin Path := Server.MapPath('.'); // physical path counter := 0; repeat FileName := Calendar1.SelectedDate.Add( TimeSpan.Create(counter,0,0,0)).ToString('yyyyMMdd'); Inc(counter) until System.IO.&File.Exists(path+dir+FileName+ext) or (counter > max); if System.IO.&File.Exists(path+dir+FileName+ext) then begin Response.ContentType := 'application/pdf'; Response.WriteFile(path+dir+FileName+'.pdf'); Response.&End end else begin Response.Write('Sorry, no meeting found within a year of ' + Calendar1.SelectedDate.ToString('yyyy/MM/dd')); Response.Write('<br>Please try again!<p>'); end end;Finally, we need to modify the Page_Load event handler, removing the previous code that set the ContentType, and instead initialise the Calendar to the current date (so the user of the application can find any Developers Group meeting from today on). This is done as follows:
procedure TWebForm1.Page_Load(sender: System.Object; e: System.EventArgs); begin if not IsPostBack then Calendar1.SelectedDate := DateTime.Today end;If we run the application, we can see the output as expected. Selecting a date far into the future (or last far enough to have no agenda PDF files for the meeting available), produces a friendly error message. Selecting a date that has a meeting with PDF file within a year's distance, will produce the PDF file for that meeting in the browser window. Nice and shiny, without the need for any CGI or ISAPI DLLs, and also without any redirection of any kind.
I've shown how we can use ASP.NET applications to produce just about any output, using the ContentType property of the Response object. It wasn't that hard after all, was it? Come to think of it, bending light around a corner is not that hard either if you happen to use a mirror in the right place (it sure beats having to catch a black hole).
Afterword: it wasn't until after we had implemented this solution, that we encounter the phenomenon called custom HTTP Handlers, which proved to be an even easier solution (and which will be covered in the January 2006 issue of The Delphi Magazine).