[SOLVED] Best way to deal with potentially very large form creating/posting using jquery $.download

Issue

I have a process as follows:

  1. User does a complex search that is done ajaxly that returns a bunch of ids (could be 1, could be 10000)

  2. Once they have there users, they can select a few things and then they download a file (which is a report based on the ids, and the things they select)

To accomplish this, I use a highly modified version of $.download

seen here:

jQuery.download = function (url, data, method, loadingHolderDivId) {
if (url && typeof data == 'object') {
    //for this version, data needs to be a json object.  
    //loop through the data object..
    $('#' + loadingHolderDivId).html($('#LoadingScreen').html());
    
    var theForm = $('<form></form>').attr('action', url).attr('method', method).attr('id', 'jqueryDownloadForm').attr('target', 'iframeX');

    $.each(data, function (propertyName, propertyVal) {
        if (propertyVal != null) {
            if (typeof propertyVal == 'object') {

                //HANDLE ARRAYS!
                for (var i = 0, len = propertyVal.length; i < len; ++i) {
                    theForm.append($("<input />").attr('type', 'hidden').attr('id', propertyName + i.toString).attr('name', propertyName).val(propertyVal[i]));
                }
            }
            else {
                theForm.append($("<input />").attr('type', 'hidden').attr('id', propertyName).attr('name', propertyName).val(propertyVal));
            }
        }
    });


    var iframeX;
    var downloadInterval;

    // remove old iframe if has
    $("#iframeX").remove();
    // create new iframe
    iframeX = $('<iframe src="javascript:false;" name="iframeX" id="iframeX"></iframe>').appendTo('body').hide();

    if ($.browser.msie) {
        downloadInterval = setInterval(function () {
            // if loading then readyState is “loading” else readyState is “interactive”
            if (iframeX && iframeX[0].readyState !== "loading") {
                $('#' + loadingHolderDivId).empty();
                clearInterval(downloadInterval);
            }
        }, 23);
    }
    else {
        iframeX.load(function () {
            $('#' + loadingHolderDivId).empty();
        });
    }


    theForm.appendTo('body').trigger('submit').remove();
    return false;
}
else {
    //they didn't fill in the params.  do nothing
}


};

Basically what is does is parses what’s in data, and builds a form out of it. this works great, when there isn’t a lot of ids. but when there is 8000, it takes 5 or 10 seconds in IE, no surprise really, it’s well know IE sucks at dom manipulation.

The other issue is, in IE. the $('#' + loadingHolderDivId).html($('#LoadingScreen').html()); won’t actually happen until after it’s done building the form. I am guessing this is because it takes a second to do that, and before it can finish it’s already to busy building the form.

The reason I am building out the form this way is so that the default model binder will be happy and bind my form right into a lovely model. The list of ids is being bound to an ilist (of integer)

Here is a sample of what the controller action looks like:

Function ExportUsers(ByVal model As ExportUsersPostModel) As ActionResult

and here’s an example of what the model looks like:

<Serializable()> _
Public Class ExportUsersPostModel
    Public Property FilterUserIds As IList(Of Integer) = New List(Of Integer)
    Public Property FilterColumnIds As IList(Of Integer) = New List(Of Integer)
    public property ShowThis as boolean
    public property OtherStuff as string = string.empty
    Public Property FormatId As Integer
End Class

so the actual question is two fold:

  1. How do I make my "loading" message show up before it begins the horribly slow form building of death?

  2. How can I speed up the form building, or build the form in a way that won’t be slow, but that will still keep the model binder happy?

Solution

If you’re able to pass the model across as JSON, you can create a custom ModelBinder to handle mapping the JSON to your data structure. I did that recently for an object type that could not be mapped automatically. Json.Net provides a class called JObject which takes a JSON string and maps it to a dynamic C# object. You can then map the dynamic object to your strongly typed object.

To create a custom ModelBinder, simply create a class that inherits from IModelBinder and implement the BindModel method. Here is a copy of my implementation. Yours will obviously vary slightly:

internal class FilterBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        if (controllerContext == null)
            throw new ArgumentNullException("controllerContext");
        if (bindingContext == null)
            throw new ArgumentNullException("bindingContext");

        if ((controllerContext.HttpContext.Request.Form.Count > 1 || (controllerContext.HttpContext.Request.Form.Count == 1 && !string.IsNullOrWhiteSpace(controllerContext.HttpContext.Request.Form.AllKeys[0]))) || (controllerContext.HttpContext.Request.QueryString.Count > 1 || (controllerContext.HttpContext.Request.QueryString.Count == 1 && !string.IsNullOrWhiteSpace(controllerContext.HttpContext.Request.QueryString.AllKeys[0]))))
        {
            ValueProviderResult val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            string value = val == null || string.IsNullOrEmpty(val.AttemptedValue) ? string.Empty : val.AttemptedValue;
            if (string.IsNullOrEmpty(value)) return null;
            dynamic obj = JObject.Parse(value);
            return new FilterSet(obj);
        }
        else
            return null;
    }
}

I have a bunch of checks to make sure what I’m getting is valid, which you may or may not need. Then, after getting the JObject, I pass it along to my constructor which does the mapping.

Answered By – esteuart

Answer Checked By – Cary Denson (BugsFixing Admin)

Leave a Reply

Your email address will not be published. Required fields are marked *