Providing file download capability in a web application is not as clear cut as linking to a file in the file system of the application. Web browsers have a mind of their own when it comes to handling files. For example, clicking on a link to an image file results in the browser opening up that image in the browser window. So how do you gain control over that workflow and get the web browser to conform to your desired action in an ASP.NET MVC 3 application?
To craft a sample solution for controlling the file download process in a MVC 3 application we are going to need some test files, some helper code for working with the file data, a basic controller and corresponding view to render links to the files, and a controller to provide the file downloads.
Working with the files
We begin by creating a sub directory named Files under the Content directory of the solution where we will place some sample files. To validate that the solution can support some common file types, the Files directory will contain a PDF, a TXT, a JPG, and a CSV file (available in the sample code download).
The application will need to find these files based on a requested file name and read the file data into a
byte array that can be delivered to the response stream. An extension method can be created to handle the logic for finding the file and getting the data. A new class file named
ExtensionMethods.cs will reside in the Models directory and will contain a method named
Listing 1: ExtensionMethods.cs
This method takes in the file name and path value, builds the full file path, and if the file is found it will read the file bytes and returns the array. If the file does not exist it throws a new
System.IO.FileNotFoundException. The application will need to use the
HttpServerUtilityBase.MapPath method to generate the base path to the files in the Content/Files directory in order for the
System.IO.File class to access the file in the file system. Since an instance of the
HttpServerUtilityBase class is made available via the
Server property in the
System.Web.Mvc.Controller class, the application will need to generate the base file path in the controller action logic and pass it in as an argument to the extension method so that the extension method will not be dependent on the
Server object. While the code in this extension method could be placed directly in the action method, it is handy to encapsulate it in this method so it can easily be reused in other controller actions across the application (or even reused in other applications). This extension method will be able to be called off of a
string variable that contains the name of a file.
To trigger a file download in a user agent (web browser) the HTTP response needs to contain header information indicating how the user agent should treat the content. The
Content-Disposition needs to be set to "attachment" and given a "filename" value. The
Content-Type needs to be set to "application/force-download". Finally, the file data needs to be written to the HTTP response stream. This logic can be handled by creating a class that inherits from the
System.Web.Mvc.ContentResult class and returning an instance of that class from a controller action. The name of this class will be
FileDownloadResult and it will be placed in the Models directory.
Listing 2: FileDownloadResult.cs
The class has a single constructor that takes in the file name and data values since it will need both values to deliver the file data to the response stream. The
ExecuteResult method overrides the base
ContextResult method and builds out the response context as needed. Prior to building out the response it verifies that it has values for the file name and the file data and throws some basic exceptions if not.
Controller and actions
The main controller that handles the file download logic is called
FilesContoller. It has a single action method to take in a requested file name, call the extension method to get the
byte array file data, and return an instance of
FileDownloadResult. The code to get the file data and return the result is wrapped in a try/catch block, and in the case of a
FileNotFoundException the code will throw a
System.Web.HttpException to trigger a 404 status code that could then be handled by a common HTTP error architecture.
Listing 3: FilesController.cs
The application also needs a simple
HomeController to provide the initial page with links to the file downloads. This class has an action method named
Index that returns the view that will contain the links.
Listing 4: HomeController.cs
The view (
Views/Home/Index.cshtml) that the
HomeController.Index method uses is fairly trivial. It consists of links to the files that are rendered using the
HtmlHelper.ActionLink extension method.
Listing 5: Index.cshtml
These links could be placed in any view in the application.
HtmlHelper.ActionLink helper relies upon the routing table to craft the correct URL syntax to the requested action. Without a route the helper will render the links in the following format:
The file variable will get appended on as a GET URL query string parameter. Adding the following route to the route table will instruct the helper on how to format the URL.
With the addition of the route, everything is in place and the file download solution is complete. Running the application and clicking on the links to the files in the web browser will result in a prompt to save or open the file. The same action will occur for all of the file types, including the image (JPG) file.
Taking the next step
Putting the control of the file delivery in ASP.NET MVC 3 controller actions instead of direct linking to files provides the ability to do many interesting server side things. For instance, the same code to package up the file data and deliver it via a
ContentResult object can be used on file data from any source, not just a read of a file in the file system. File data can be pulled from a data source record and fed straight to the response stream. Data displayed in a table can have an action method that will query the same data, write the data to a string in a CSV format, convert it to a
byte array, and then send that stream to the code that generates the
ContentResult to offer users the ability to export data to a CSV file. New solutions are only a download away.
I have been entrenched in web application development for quite a while and have traversed the syntactic jungles of PHP, classic ASP, Visual Basic, VB.NET, and ASP.NET Web Forms. However, I have found a guilty pleasure in ASP.NET MVC since its beta launch and have since refactored my web stack focus...
This author has published 3 articles on DotNetSlackers. View other articles or the complete profile here.
Please login to rate or to leave a comment.