4
\$\begingroup\$

Recently I had the need to make some ajax calls within a MVC 3.0 with Razor and jQuery application. After a bit of trial and error and refactoring it was discovered that a number of different needs were required. These included:

  1. The serverside code would be responsible for creating any redirect urls and we wanted to stick with the inherited controller RedirectToAction() method where possible in our controller actions.
  2. We are using jQuery unobtrusive validation and some situations meant that new forms were rendered onto the view via ajax. That meant we needed to ensure those new form fields contained and validated when required.
  3. We wanted the flexibility to return html (PartialViews), json or tell the client side it was a redirect and the URL to redirect to (initiated via 1 above).
  4. Before page redirects at times we wanted to show div popups such as a Success confirmation. Hence although we were using RedirectToAction() in the backend we wanted the javascript to actually perform the redirect.

With these in place we did a bit of serverside code that ensured we hijacked the RedirectToAction() result and returned a json object instead. I'm fairly happy that code works. What I'm not sure about is the javascript code that I use to perform the result.

Some notes:

  1. The $.validator.unobtrusive.parseDynamicContent method will ensure the new html elements are rebound for validation. I did not write that code so have not added it for review.
  2. For options 1, 2 and 3 above we didn't want the person writing the ajax to always have to worry about these so esentially wanted them to just be handled so to speak.

I'm definitely pretty raw in the Javascript department so any comments or improvements on the code below would be greatly appreciated.

$.ajaxWithRedirect = function (options) {

    // If dataType wasn't specified in the options, default to 'html'
    var dataType = (options.dataType !== undefined) ? options.dataType : 'html';

    // jQuery AJAX object
    $.ajax({

        // Normal properties
        type: options.type,
        url: options.url,
        data: options.data,
        dataType: dataType,
        cache: false,

        // Global beforeSend wrapper with user defined function
        beforeSend: function () {

            // Execute user defined method
            if (typeof options.beforeSend === 'function') {
                options.beforeSend();
            }
        },

        // Global success wrapper which will redirect if url specified is a json object
        // with the RedirectUrl tag
        success: function (data) {

            var jData;
            var redirected = false;

            try {

                if (data) {
                    jData = $.parseJSON(data);

                    if (jData && (typeof jData.RedirectUrl !== 'undefined' && jData != null && jData.RedirectUrl.length > 0)) {

                        var performRedirect = true;

                        if (typeof options.beforeRedirect !== 'undefined') {
                            // beforeRedirect returns true if the redirect is still to occur otherwise false
                            performRedirect = options.beforeRedirect(jData);
                        }

                        if (performRedirect) {
                            redirected = true;

                            // Using replace based off SO - http://stackoverflow.com/questions/503093/how-can-i-make-a-redirect-page-in-jquery-javascript
                            window.location.replace(jData.RedirectUrl);
                        }
                    }
                }
            } catch (e) {
                // not json
            }

            // Execute user defined method
            if (!redirected) {

                if ((options.success && typeof options.success === 'function')) {
                    options.success(data);
                }

                // always done after the success to ensure any dyanamic elements have been loaded
                if (typeof options.dynamicValidation != 'undefined') {
                    $.validator.unobtrusive.parseDynamicContent(options.dynamicValidation);
                }
            }
        },
        error: function (xhr, status, err) {
            if (typeof options.error === 'function') {
                options.error(xhr, status, err);
            }
            if (xhr.status == 400) {
                alert(err);
            }
        }
    });
};

Example of usage is (Methods such as addUploadTableSpinner just included for examples):

$.ajaxWithRedirect({
    beforeSend: addUploadTableSpinner(),
    type: "POST",
    url: '/MyUrl/Delete' + '?sessionId=' + sessionId,
    dataType: "html",
    beforeRedirect: function (data) {
        closePopup();

        // show the user success message
        showDeleteSuccessfulPopup(data.RedirectUrl);
        showSuccessfulPopup = false;

        // don't do the redirect
        return false;
    },
    success: function (data) {
        if (data.length == 0) {
            deleteRow.remove();
        } else if (showSuccessfulPopup) {
            deleteRow.html(data);

            closePopup();
            showDeleteSuccessfulPopup();
        }
        removeUploadTableSpinner();
    }
});
\$\endgroup\$
3
\$\begingroup\$

It seems to me that you're duplicating some of what $.ajax() will do for you (such as calling beforeSend) and you're also leaving the door open for using the deferred promise methods (then, done, fail, etc.) that jQuery's XHR object provides.

Here's a version that focusses solely on the redirection (i.e. it doesn't worry about the data type, the validation stuff, or doing its own error handling). It's just a transparent layer on top of the normal $.ajax() function.

The code's below, and here's a demo

$.ajaxWithRedirect = function (options) {
  "use strict";

  var deferred = $.Deferred(),
      successHandler,
      xhr;

  // force no-cache
  options.cache = false;

  // get a copy of the success handler...
  successHandler = options.success;

  // ... and replace it with this one
  options.success = function (data) {
    var contentType = xhr.getResponseHeader("Content-Type"),
        performRedirect = true,
        redirectUrl = null,
        args = [].slice.call(arguments, 0),
        json;

    // If response isn't there or isn't json,
    // skip all the redirect logic
    if( data && (/json/i).test(contentType) ) {
      // If json was requested, and json received, jQuery will
      // have parsed it already. Otherwise, we'll have to do it
      if( options.dataType === 'json' ) {
        redirectUrl = data.RedirectUrl;
      } else {
        try {
          json = $.parseJSON(data);
          redirectUrl = json.RedirectUrl;
        } catch(e) {
          // no-op
        }
      }

      // check the redirect url
      if( redirectUrl && typeof redirectUrl === 'string') {
        // Is there a beforeRedirect handler?
        if( typeof options.beforeRedirect === 'function' ) {
          // pass all the arguments to the beforeRedirect handler
          performRedirect = options.beforeRedirect.apply(null, args);
        }

        // unless strictly false, go ahead with the redirect
        if( performRedirect !== false ) {
          location.replace(redirectUrl);
          // and stop here. No success and/or deferred handlers
          // will be called since we're redirecting anyway
          return;
        }
      }
    }

    // no redirect; forward everything to the success handler(s)
    if( typeof successHandler === 'function' ) {
      successHandler.apply(null, args);
      deferred.resolve.apply(null, args);
    }
  };

  // Make the request
  xhr = $.ajax(options);

  // Forward the deferred promise method(s)
  xhr.fail(deferred.reject);
  xhr.progress(deferred.notify);

  // Replace the ones already on the xhr obj
  deferred.promise(xhr);

  return xhr;
};

Point is that you should be able use $.ajaxWithRedirect exactly like you'd use vanilla $.ajax, including all the shiny deferred promise stuff. The only thing it does different from the normal version is set options.cache = false, and that it has a beforeRedirect callback.

\$\endgroup\$
  • \$\begingroup\$ Thanks looks good (I think :)). I'm not sure where the validation re-binding would be plugged in for this though. Would you suggest still creating a wrapper around this to handle that on success, as I've done? The idea is for this stuff to just happen rather than putting it in for every $.ajaxWithRedirect call. \$\endgroup\$ – dreza Oct 24 '12 at 19:04
  • 1
    \$\begingroup\$ @dreza Yes, I'd say make a wrapper for that. The point is that the validation code is orthogonal to the redirection code; one does not necessitate or depend on the other. But you can build another layer on top of ajaxWithRedirect that takes care of validation, setting dataType = 'html' and maybe the error handling too. \$\endgroup\$ – Flambino Oct 25 '12 at 2:31
  • \$\begingroup\$ Thats great. I will try something and repost as an answer or edit my question when I've sussed it. \$\endgroup\$ – dreza Oct 25 '12 at 4:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.