Response.Write/Response.BinaryWrite and File Download problem in UpdatePanel

(Solution to Sys.WebForms.PageRequestManagerParserErrorException)

You  may face problem when you write download file logic in aspx page code behind while the the control that fires the event to download file is in update panel. If you use Response.Write() or Response.BinaryWrite() method as shown below in async postback you will encounter Sys.WebForms.PageRequestManagerParserErrorException.

clip_image002

Page’s markup




    
         >
    

 

Page’s Code behind


protected void ButtonDownload_Click(object sender, EventArgs e)
        {
            string fileName = "song.wav";
            string filePath = "Audio/song.wav";
            Response.Clear();
            Response.ContentType = "audio/mpeg3";
            Response.AppendHeader("Content-Disposition", "attachment; filename=" + fileName);
            string fileSeverPath = Server.MapPath(filePath);
            if (fileSeverPath != null)
            {

                byte[] fileBytes = GetFileBytes(fileSeverPath);

                Response.BinaryWrite(fileBytes);

                Response.Flush();
            }
        }
        protected byte[] GetFileBytes(string url)
        {
            WebRequest webRequest = WebRequest.Create(url);
            byte[] fileBytes = null;
            byte[] buffer = new byte[4096];
            WebResponse webResponse = webRequest.GetResponse();
            try
            {
                Stream stream = webResponse.GetResponseStream();
                MemoryStream memoryStream = new MemoryStream();
                int chunkSize = 0;
                do
                {
                    chunkSize = stream.Read(buffer, 0, buffer.Length);
                    memoryStream.Write(buffer, 0, chunkSize);
                } while (chunkSize != 0);
                fileBytes = memoryStream.ToArray();

            }
            catch (Exception ex)
            {
                Response.Write(ex.Message);
            }

            return fileBytes;
        }

The file download logic is simple. I have considered and audio file (say song.wav) to download that’s placed in Audio folder in root of my website. I have written separate method that takes URL for the file to be downloaded and return the bytes of that file.

Resolution of the Problem

There are several ways to fix this problem each fix has specific scops.

  1. Register ButtonDownload PostBackTrigger control in Triggers child tag of the update panel as shown below.

    
        
        
    
    
    
 

  1. You can also use RegisterPostBackControl method of the ScriptManager control in Page_Load as shown below.
protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                ScriptManager1.RegisterPostBackControl(ButtonDownload);
            }
        }
  1. The drawback of these two approaches is obviuos i..e ultimatley you have to resort a full postback whenver you will need downlaod a file while the rest of the controls on the page are posting back asynchronously. This may lead to serious problems if you are using multiple update panels and you want them work simulatneolusly. Think of a scenario if you have a player in page and that is in a separate update panel. When it is playing file , you click on download file button. What will happen? A postback will occur that will close the player as complete page has to be posted back. In such cases we have to sort out some other way. You might have heard about HttpHandlers. If not , then its time to know their strength; and in such a scenario you can learn their usefullness. So it lets have a look briefly. An HttpHanlder is a class that impliments IHttppHanlder and can be set traget of a particular request (HttpRequest). They can be thought of like ISAPI extenions that are basically dlls mapped against particular file extensions that all happens at IIS level. An HttpHanlder handles request with particular extensions after HttpModule completes their working on it. An HttpHanlder takes an httpcontext and handels it in a way you have told it to do. They are best when you want to isloate or separate an HttpRequest from main HttpContext to handle it in custom manner. Like our scenario, we want our download file request to be separated from main context so that it won’t affect other ongoing requests/reponses of main httpcontext. There are built in HttpHandlers that automatically get registered in web.config. When we write a new httpHandler we have to reigister it but there are is support of generic handler with extension .ashx(asp handler x) that does not require regsiteration. Lets add a new generic hanlder. Right click on your web site and chose Add new item. and then select Generic Handler as shown below

clip_image004

Lets have a look on the file added

<%@ WebHandler Language="C#" Class="DownloadHandler" %>
        using System;
        using System.Web;
        public class DownloadHandler : IHttpHandler
        {
            public void ProcessRequest (HttpContext context) 
            {
                context.Response.ContentType = "text/plain";
                context.Response.Write("Hello World");
            }

            public bool IsReusable {
                get {
                return false;
            }
        }


It has a method ProcessRequest that takes httpcontext in parameter and allows us to handle it in custom manner and a property IsReusable used to flag whether or not the current instance of the handler is resuable for the following similar requests.

Let add our download logic here in ProcessRequest method. Now after complete implementation our hanlder looks like this

<%@ WebHandler Language="C#" Class="DownloadHandler" %>
        using System;
        using System.Web;
        public class DownloadHandler : IHttpHandler
        {
            public void ProcessRequest (HttpContext context) 
            {
              
                string fileName = context.Request.QueryString["fileName"];

                string filePath = context.Request.QueryString["filePath"];
                context.Response.Clear();
                context.Response.ContentType = "audio/mpeg3";
                context.Response.AppendHeader("Content-Disposition", "attachment; filename=" + fileName);
                string fileSeverPath = context.Server.MapPath(filePath);
                if (fileSeverPath != null)
                {
                    byte[] fileBytes = GetFileBytes(fileSeverPath);
                    context.Response.BinaryWrite(fileBytes);
                    context.Response.Flush();
                }
            }

            public bool IsReusable {
                get {
                return false;
            }
            }

            protected byte[] GetFileBytes(string url)
            {
                WebRequest webRequest = WebRequest.Create(url);
                byte[] fileBytes = null;
                byte[] buffer = new byte[4096];
                WebResponse webResponse = webRequest.GetResponse();
                try
                {
                    Stream stream = webResponse.GetResponseStream();
                    MemoryStream memoryStream = new MemoryStream();
                    int chunkSize = 0;
                do
                {
                    chunkSize = stream.Read(buffer, 0, buffer.Length);
                    memoryStream.Write(buffer, 0, chunkSize);
                } while (chunkSize != 0);

                fileBytes = memoryStream.ToArray();

                }
                catch (Exception ex)
                {

                   // log it somewhere

                }
               return fileBytes;

             }
        }

You might have noticed that the whole code is same as we used in the page just with few chnages , now all response, request are of the context being passed in paramater and we are getting file path and file name from query sting.

Lets come to last part of the story using this handler. You can call this handler by setting url of any url property supporting control and rasing its click hanlder programmterically or in windows.open in javascript. You might wonder , why I am not using Response.Redirect. Well you can use it, but in current scenario we cant use it as it will cause a postback. Remove download file logic from page code behind. All changes are in page’s mark up. After chnages here is it.


< script type="text/javascript">

        function downloadFile(fileName, filePath) {
            var downloadLink = $get('downloadFileLink');
            downloadLink.href = "DownloadHandler.ashx?fileName=" + fileName + "&filePath=" + filePath;
            downloadLink.style.display = 'block';
            downloadLink.style.display.visibility = 'hidden';
            downloadLink.click();

            return false;
        }

</script>


<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Test.aspx.cs" Inherits="Test" %>



   

Important point to note is that I have added and anchor in the page with id downloadFileLink and have resgister OnClientClick of the button with downloadFile javscript method that takes target file name and virtual relative path of the file (if used in GridView these may come from database) . When button is click downloadFile method is called that set the href proiperty of the anchor with the url to call DownloadHandler.

downloadLink.href = "DownloadHandler.ashx?fileName=" + fileName + "&filePath=" + filePath;

After setting it anchor is clicked programmatically and as result DownloadHandler is called and after processing the request shows following popup to save the file.

clip_image006

Here is it. Enjoy!

9 comments:

kenk said...

Mahr,

I am attempting to use your code to perform file downloads. Using the first example of the code you supplied, I was successful in downloading files, but at the end of each file, the entire source code of my home page was also included.

I am now trying to use the generic handler example that you provided. I have declared 2 public variables in the homepage code-behind(FileName and FilePath) that I would like to pass to the function downloadFile(fileName, filePath) routine. Can you offer me some insight how this is done? By the way, your written code is clear and very easy to read.

Thanks.....Ken

Anonymous said...

Mahr,
Have you tested your code in Mozilla Firefox? It seams that command downloadLink.click(); works only in IE.
thanks,
Roko

Brian said...

I just wanted to add that if you have a screen update in addition to the file download, you can use the following line instead of directly calling the downloadLink.click() function: window.setTimeout('downloadLink.click()', 1000);
This will allow the screen to refresh since the file download will clear the response.

Bebandit said...

Hey!
This worked perfectly when I was trying to call a PDF document that was stored in SQL Server for archiving purposes. The "Reponse.xxx"'s don't work inside an Update Panel. Thaks for posting this. I once was asked a HTTPHandler question in an interview and now I can say that I wrote one :). Thanks again!

daniloquio said...

Thanks a lot, you helped me a lot.

The anchor's click doesn't work in FF or Chrome. You have to replace

downloadLink.click();

with

if (downloadLink.click)
{
downloadLink.click()
} else if(document.createEvent)
{
if(event.target !== downloadLink)
{
var evt = document.createEvent("MouseEvents");
evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
var allowDefault = downloadLink.dispatchEvent(evt);
return false;
}
}

Anonymous said...

Handler File ?

Anonymous said...

Hi, the blog is really helpful.
Need your help for the below given case.

Actually I wanted to display the Loading message when I m fetching data from backend and putting in .CSV file, when I was using HttpContext, I was getting error -
Microsoft JScript runtime error: Sys.WebForms.PageRequestManagerParserErrorException:The message received from the server could not be parsed.

To avoid the above error I wrote the script in the PageLoad(see below),and now I m not getting the above error but the hide() is not working. I have given the code below.

protected void Page_Load(object sender, EventArgs e)
{
ScriptManager scriptManager = ScriptManager.GetCurrent(this.Page);
scriptManager.RegisterPostBackControl(this.btnDownloadReport1);
}

protected void btnDownloadReport_OnClick(object sender, EventArgs e)
{
GetPCNMEReportInCSV();
ScriptManager.RegisterStartupScript(this, typeof(string), "script", "$('.loading').hide();", true);
}

Anonymous said...

Thanks a lot! Saved my day

Anonymous said...

Thnak you. Good job keep it up.

Post a Comment