Tuesday, October 11, 2011

Building A Custom ActionResult in MVC 2 To Download Excel Files

I've seen this question asked several times on the forums, and that is how do you download files when you're working with MVC. Because of URL routing, you shouldn't point the user to your file, you should point them to a URL route and let the controller do the work for you. One way you can do this is to create your own custom ActionResult simply by creating a class that inherits from ActionResult. By the end of this article you'll have an MVC action that look like this:
C#
public ExcelResult GetExcelFile()
{
      return new ExcelResult
                  {
                        FileName = "sample.xls", Path = "~/Content/sample.xls"
                  };
}
VB.NET
Public Function GetExcelFile() As ExcelResult
      Return New ExcelResult
                               "sample.xls", Path = "~/Content/sample.xls"
                               FileName = "sample.xls", Path
End Function
And the end result to the user will look like this. They'll have the option to either save or open the file:
OpeningTests
Ok let's get started. Create a new MVC web application. For this sample I'm using MVC2. If you haven't got it you can download it from here. I want to create a new custom class that will change the content-disposition of the response. This will cause the pop-up to appear. I've called my class ExcelResult. The two things you need to do is inherit from ActionResult and override the ExecuteResult method:
C#
public class ExcelResult : ActionResult
{
      public string FileName { get; set; }
      public string Path { get;set; }
 
      public override void ExecuteResult(ControllerContext context)
      {
            context.HttpContext.Response.Buffer = true;
            context.HttpContext.Response.Clear();
            context.HttpContext.Response.AddHeader("content-disposition", "attachment; filename=" + FileName);
            context.HttpContext.Response.ContentType = "application/vnd.ms-excel";
            context.HttpContext.Response.WriteFile(context.HttpContext.Server.MapPath(Path));   
      }
}
VB.NET (Converted code)
Public Class ExcelResult
      Inherits ActionResult
      Private privateFileName As String
      Public Property FileName() As String
            Get
                  Return privateFileName
            End Get
            Set(ByVal value As String)
                  privateFileName = value
            End Set
      End Property
       Private privatePath As String
       Public Property Path() As String
             Get
                   Return privatePath
             End Get
             Set(ByVal value As String)
                   privatePath = value
             End Set
       End Property
 
       Public Overrides Sub ExecuteResult(ByVal context As ControllerContext)
             context.HttpContext.Response.Buffer = True
                  context.HttpContext.Response.Clear()
                  context.HttpContext.Response.AddHeader("content-disposition", "attachment; filename=" & FileName)
                  context.HttpContext.Response.ContentType = "application/vnd.ms-excel"
                  context.HttpContext.Response.WriteFile(context.HttpContext.Server.MapPath(Path))
       End Sub
End Class
 
The code above basically means that when the return type of an action is ExcelResult, it will change the content-disposition and write the contents of the file to the HTTP response output stream. To see this code in action, I've created an MVC action and set the return type to ExcelResult:
C#
public ExcelResult GetExcelFile()
{
      return new ExcelResult
                  {
                        FileName = "sample.xls", Path = "~/Content/sample.xls"
                  };
}
VB.NET (Converted code)
Public Function GetExcelFile() As ExcelResult
      Return New ExcelResult
                               "sample.xls", Path = "~/Content/sample.xls"
                               FileName = "sample.xls", Path
End Function
 
In my view I have created an ActionLink to call this action:
 
<%= Html.ActionLink("Download Excel", "GetExcelFile", "Home")%>
 
If you ran the project now and click on the Download Excel link, the open file dialog will appear asking if you want to save or open the file. 
 
This will work for any type of file. All you need to change is the ContentType of the response object. You can find the full list of content types here.
 
This is one way of implementing this functionality. As with anything there are multiple ways of doing this, but it works for me. The entire source code of this article can be downloaded over here