Search code examples
asp.netexceldownloadpdf-generationhttpcontext

Exporting Using HttpContext.Current.Response is Causing Issues


I'm developing a web site using asp.net/C#/HTML5/bootstrap. One of the requirements is to have have documents export to Excel and/or PDF. I'm able to export (successfully) using the following snippet (this is the Excel snippet):

        HttpContext.Current.Response.ContentType = "application/octet-stream";
        HttpContext.Current.Response.ContentEncoding = System.Text.Encoding.UTF8;
        HttpContext.Current.Response.AddHeader("content-disposition", "attachment; filename=" + filename);
        HttpContext.Current.Response.BinaryWrite(xlsBytes);
        HttpContext.Current.Response.Flush();
        HttpContext.Current.Response.End();

The issue I'm having is that after this runs, it seems to be stopping the page life cycle dead in it's tracks. As an example, the user clicks the export button, which calls javascript that throws up a "Please wait" modal dialog and submits the form:

<script src="../Scripts/waitingFor.js"></script>

<script type="text/javascript">
    function pleaseWait() {
        waitingDialog.show("Building File<br/>...this could take a minute", { dialogSize: "sm", progressType: "warning" });

        form = document.getElementById("frm_contentMaster");
        form.submit();
    }
</script>

The javascript include file:

/**
 * Module for displaying "Waiting for..." dialog using Bootstrap
 *
 * @author Eugene Maslovich <[email protected]>
 */

(function (root, factory) {
    'use strict';

    if (typeof define === 'function' && define.amd) {
        define(['jquery'], function ($) {
            return (root.waitingDialog = factory($));
        });
    }
    else {
        root.waitingDialog = root.waitingDialog || factory(root.jQuery);
    }
}(this, function ($) {

'use strict';

/**
 * Dialog DOM constructor
 */
function constructDialog($dialog) {
    // Deleting previous incarnation of the dialog
    if ($dialog) {
        $dialog.remove();
    }
    return $(
        '<div id="waitingFor" class="modal fade" data-backdrop="static" data-keyboard="false" tabindex="-1" role="dialog" aria-hidden="true" style="padding-top:15%; overflow-y:visible;">' +
            '<div class="modal-dialog modal-m">' +
                '<div class="modal-content">' +
                    '<div class="modal-header" style="display: none;"></div>' +
                    '<div class="modal-body">' +
                        '<div class="progress progress-striped active" style="margin-bottom:0;">' +
                            '<div class="progress-bar" style="width: 100%"></div>' +
                        '</div>' +
                    '</div>' +
                '</div>' +
            '</div>' +
        '</div>'
    );
}

// Dialog object
var $dialog;

return {
    /**
     * Opens our dialog
     * @param message Custom message
     * @param options Custom options:
     *   options.headerText - if the option is set to boolean false, 
     *     it will hide the header and "message" will be set in a paragraph above the progress bar.
     *     When headerText is a not-empty string, "message" becomes a content 
     *     above the progress bar and headerText string will be set as a text inside the H3;
     *   options.headerSize - this will generate a heading corresponding to the size number. Like <h1>, <h2>, <h3> etc;
     *   options.headerClass - extra class(es) for the header tag;
     *   options.dialogSize - bootstrap postfix for dialog size, e.g. "sm", "m";
     *   options.progressType - bootstrap postfix for progress bar type, e.g. "success", "warning";
     *   options.contentElement - determines the tag of the content element. 
     *     Defaults to "p", which will generate a <p> tag;
     *   options.contentClass - extra class(es) for the content tag.
     */
    show: function (message, options) {
        // Assigning defaults
        if (typeof options === 'undefined') {
            options = {};
        }
        if (typeof message === 'undefined') {
            message = 'Loading';
        }
        var settings = $.extend({
            headerText: '',
            headerSize: 3,
            headerClass: '',
            dialogSize: 'm',
            progressType: '',
            contentElement: 'p',
            contentClass: 'content',
            onHide: null // This callback runs after the dialog was hidden
        }, options),
        $headerTag, $contentTag;

        $dialog = constructDialog($dialog);

        // Configuring dialog
        $dialog.find('.modal-dialog').attr('class', 'modal-dialog').addClass('modal-' + settings.dialogSize);
        $dialog.find('.progress-bar').attr('class', 'progress-bar');
        if (settings.progressType) {
            $dialog.find('.progress-bar').addClass('progress-bar-' + settings.progressType);
        }

        // Generate header tag
        $headerTag = $('<h' + settings.headerSize + ' />');
        $headerTag.css({ 'margin': 0 });
        if (settings.headerClass) {
            $headerTag.addClass(settings.headerClass);
        }

        // Generate content tag
        $contentTag = $('<' + settings.contentElement + ' />');
        if (settings.contentClass) {
            $contentTag.addClass(settings.contentClass);
        }

        if (settings.headerText === false) {
            $contentTag.html(message);
            $dialog.find('.modal-body').prepend($contentTag);
        }
        else if (settings.headerText) {
            $headerTag.html(settings.headerText);
            $dialog.find('.modal-header').html($headerTag).show();

            $contentTag.html(message);
            $dialog.find('.modal-body').prepend($contentTag);
        }
        else {
            $headerTag.html(message);
            $dialog.find('.modal-header').html($headerTag).show();
        }

        // Adding callbacks
        if (typeof settings.onHide === 'function') {
            $dialog.off('hidden.bs.modal').on('hidden.bs.modal', function () {
                settings.onHide.call($dialog);
            });
        }
        // Opening dialog
        $dialog.modal();
    },
    /**
     * Closes dialog
 */
    hide: function () {
        if (typeof $dialog !== 'undefined') {
            $dialog.modal('hide');
        }
    }
};
}));

I'm using NPOI to create the Excel file in the code behind (simplified function):

using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;

protected void exportExcel()
{
    XSSFWorkbook wb = new XSSFWorkbook();

    XSSFSheet sh = (XSSFSheet)wb.CreateSheet("Legend");

    //*****************************************
    //* Workbook Download & Cleanup
    //*****************************************
    MemoryStream stream = new MemoryStream();
    wb.Write(stream);
    stream.Dispose();

    var xlsBytes = stream.ToArray();
    string filename = "Behavior Stats YTD.xlsx";

    MemoryStream newStream = new MemoryStream(xlsBytes);

    HttpContext.Current.Response.ContentType = "application/octet-stream";
    HttpContext.Current.Response.ContentEncoding = System.Text.Encoding.UTF8;
    HttpContext.Current.Response.AddHeader("content-disposition", "attachment; filename=" + filename);
    HttpContext.Current.Response.BinaryWrite(xlsBytes);
    HttpContext.Current.Response.Flush();
    HttpContext.Current.Response.End();
}

This creates the Excel file and pushes it to the user, but the life cycle of the code behind does not continue -- it stops immediately after the End command. If I comment out the HttpContext lines, obviously the Excel sheet does not get created, but the life cycle of the page continues -- the rest of the code behind runs, the page refreshes, and the modal Please Wait dialog goes away.

So am I using this wrong? Most examples that I've seen of how to export use this method. Is there another way that I can export that is cleaner and/or safer? Is there just a simple tweak I need to make to this, that will let the life cycle continue? Who created liquid soap, and why?

Any help you can provide would be greatly, greatly appreciated.


Solution

  • It's working exactly as it should. You are telling the browser that the response is a file. The response can only be one thing at a time. You cannot have page content and a file in the same response.

    You can separate the the two by downloading the file in an IFrame. First put your file download C# code in its own page. Then call that page from an IFrame with a JavaScript function.

    function DownloadExcel() {
        var downloadFrame = document.createElement("IFRAME");
    
        if (downloadFrame != null) {
            downloadFrame.setAttribute("src", '/DownloadExcel.aspx');
            downloadFrame.style.width = "0px";
            downloadFrame.style.height = "0px";
            document.body.appendChild(downloadFrame);
        }
    }